保留影像细节信息的平滑滤波算法

Jul 1,2019   3482 words   13 min


最近在看用微单拍的夜景照片的时候发现,总体看还行放大了噪点还是不少的,如下图所示。 所以就想着能不能用什么办法降个噪。降噪首先想到的就是对影像进行滤波了,相关知识在这篇博客中有所介绍。 但如果仅仅是使用简单的低通滤波,如均值、中值等,不可避免地影像的细节会丢失。双边滤波可以较好的保留边缘细节信息。 这篇博客从另一个角度研究下保留影像边缘信息的平滑。

很简单的想法就是是不是可以先把边缘部分提取出来,然后在进行滤波的时候跳过这些部分。答案是肯定的。这也是这篇博客主要的方法。 具体而言,首先用各种边缘检测算子,如Canny等对影像进行边缘检测,得到初步的掩膜,对该掩膜进行形态学膨胀,拓展范围。 最后以膨胀后的结果作为掩膜,对影像进行滤波。若为边缘部分则直接赋值,否则再进行滤波,均值、中值等。

1.代码

部分核心代码如下。

def getWindow(img, i, j, win_size):
    # 获得指定范围、大小的窗口内容
    if win_size % 2 == 0:
        win = None
        return win
    half_size = win_size / 2
    start_x = i - half_size
    start_y = j - half_size
    end_x = i + half_size + 1
    end_y = j + half_size + 1
    win = img[start_x:end_x, start_y:end_y]
    return win


def smoothImg(img, canny_th1=128, canny_th2=255, kernel_size=5, dilate_iter=1, flag=1, method=1, win_size=5,
              win_offset=1):
    """

    :param img: 输入影像,单波段或RGB
    :param canny_th1: Canny算子低阈值,默认128
    :param canny_th2: Canny算子高阈值,默认255
    :param kernel_size: 膨胀运算卷积核大小,默认为5
    :param dilate_iter: 膨胀迭代次数,默认为1
    :param flag: 是否开启边缘强化平滑,1-开启,0-关闭,默认为1
    :param method: 平滑方法,1-均值滤波,2-中值滤波,默认为1
    :param win_size: 平滑窗口大小,默认为5
    :param win_offset: 平滑窗口移动步长,默认为1
    :return: 平滑后的影像
    """

    kernel = np.ones((kernel_size, kernel_size), np.uint8)
    if len(img.shape) == 2:
        # single band
        edges = cv2.Canny(img, canny_th1, canny_th2)
        mask = cv2.dilate(edges, kernel, iterations=dilate_iter)
        cv2.imwrite("mask.jpg", mask)
        img_smooth = smooth_core(img, mask, flag=flag, method=method, win_size=win_size, win_offset=win_offset)
        return img_smooth
    else:
        # BGR band
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(img_gray, canny_th1, canny_th2)
        mask = cv2.dilate(edges, kernel, iterations=dilate_iter)
        cv2.imwrite("mask.jpg", mask)
        band_b = img[:, :, 0]
        band_g = img[:, :, 1]
        band_r = img[:, :, 2]
        print 'processing band blue...'
        band_b_s = smooth_core(band_b, mask, flag=flag, method=method, win_size=win_size, win_offset=win_offset)
        print 'processing band green...'
        band_g_s = smooth_core(band_g, mask, flag=flag, method=method, win_size=win_size, win_offset=win_offset)
        print 'processing band red...'
        band_r_s = smooth_core(band_r, mask, flag=flag, method=method, win_size=win_size, win_offset=win_offset)
        img_smooth = np.zeros_like(img)
        img_smooth[:, :, 0] = band_b_s
        img_smooth[:, :, 1] = band_g_s
        img_smooth[:, :, 2] = band_r_s
        return img_smooth


def smooth_core(img, mask, flag=1, method=1, win_size=5, win_offset=1):
    img_smooth = np.zeros_like(img)
    if method == 1:
        bk = cv2.blur(img, (win_size, win_size))
    elif method == 2:
        bk = cv2.medianBlur(img, (win_size, win_size))
    img_smooth[:, :] = bk
    width = img.shape[1]
    height = img.shape[0]
    safe_range = win_offset + win_size
    if flag == 1:
        for i in range(safe_range, height - safe_range):
            if i % 50 == 0:
                print i, '/', height - safe_range
            for j in range(safe_range, width - safe_range):
                val = mask[i, j]
                if val == 255:
                    img_smooth[i, j] = img[i, j]
                else:
                    win = getWindow(img, i, j, win_size)
                    if method == 1:
                        win_mean = np.mean(win)
                    elif method == 2:
                        win_mean = np.median(win)
                    img_smooth[i, j] = win_mean
    else:
        for i in range(safe_range, height - safe_range):
            if i % 50 == 0:
                print i, '/', height - safe_range
            for j in range(safe_range, width - safe_range):
                win = getWindow(img, i, j, win_size)
                if method == 1:
                    win_mean = np.mean(win)
                elif method == 2:
                    win_mean = np.median(win)
                img_smooth[i, j] = win_mean
    return img_smooths

完整代码及测试数据见Github项目,点击查看

2.测试

选取一张用微单拍摄的夜景图片如下。点击下载原图(9.47MB,6000×4000)。 分别用本博客的算法、均值滤波、中值滤波、高斯滤波和双边滤波进行对比,如下。由于全图太大,因此只截取了部分区域进行对比,选择了细节比较丰富和较少的不同区域进行对比。

均值滤波结果(kernel size 5×5)↓ 局部区域 细节对比 天空对比

中值滤波结果(kernel size 5×5)↓ 局部区域 细节对比 天空对比

高斯滤波结果(kernel size 5×5,σ=0)↓ 局部区域 细节对比 天空对比

双边滤波结果(kernel size 9×9,sigmaColor=75,sigmaSpace=75)↓ 局部区域 细节对比 天空对比

本博客算法结果(参数较多,详细参数见代码)↓ 局部区域 细节对比 天空对比

使用的边缘掩膜如下↓ 局部区域 细节对比 天空对比

可以看出总体而言,本博客算法基本满足了在一开始提出的需求。而且相比于双边滤波而言,其在细节的保留上更加完整,如湖边的栏杆、树木的轮廓等本博客算法更加清晰。

3.进一步可研究问题

(1)效率问题

由于只是从实现角度写的代码,因此效率较低,可以继续使用一些并行加速手段对代码进行优化。

(2)参数问题

实际的效果依赖于很多计算参数,而不同影像这些参数如何找到最合适的值也是值得思考的问题。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部