最近在看用微单拍的夜景照片的时候发现,总体看还行放大了噪点还是不少的,如下图所示。 所以就想着能不能用什么办法降个噪。降噪首先想到的就是对影像进行滤波了,相关知识在这篇博客中有所介绍。 但如果仅仅是使用简单的低通滤波,如均值、中值等,不可避免地影像的细节会丢失。双边滤波可以较好的保留边缘细节信息。 这篇博客从另一个角度研究下保留影像边缘信息的平滑。
很简单的想法就是是不是可以先把边缘部分提取出来,然后在进行滤波的时候跳过这些部分。答案是肯定的。这也是这篇博客主要的方法。 具体而言,首先用各种边缘检测算子,如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)参数问题
实际的效果依赖于很多计算参数,而不同影像这些参数如何找到最合适的值也是值得思考的问题。
本文作者原创,未经许可不得转载,谢谢配合