3. 中值滤波:原理、对椒盐噪声的抑制效果、窗口大小选择策略
聊完均值滤波,咱们来看看另一种非常实用的非线性滤波器——中值滤波。说实话,在我刚入行那会儿,总觉得中值滤波不就是排个序取中间值嘛,有啥好研究的?直到我在一个工业相机项目里被椒盐噪声折磨得够呛,才发现这玩意儿是真香。
3.1 中值滤波的核心原理
中值滤波的原理,说白了就三步:
- 开窗:在图像上滑动一个固定大小的窗口(比如3×3)。
- 排序:把窗口内所有像素的灰度值从小到大排个队。
- 赋值:用排好队的中间那个值,替换掉窗口中心像素的原始值。
举个例子,一个3×3的窗口里像素值是:
[[ 12, 15, 18],
[ 10, 255, 14],
[ 11, 13, 16]]
中间那个255明显是个椒盐噪声点(白噪点)。排序后得到:10, 11, 12, 13, 14, 15, 16, 18, 255。中间值是14。于是中心像素从255被替换成14。你看,噪声就这么被干掉了。
关键区别:均值滤波是“算平均”,中值滤波是“选中间”。这个差异决定了它们对付不同噪声的能力完全不同。
3.2 对椒盐噪声的抑制效果
椒盐噪声是什么?就是图像上随机出现的白点(盐)和黑点(椒)。这些点的像素值要么是极大值(255),要么是极小值(0),跟周围像素格格不入。
为什么中值滤波对付椒盐噪声特别有效?原因很简单:
- 椒盐噪声的像素值在排序后,一定会被挤到序列的两端。
- 中间位置的值,大概率是周围正常像素的某个值。
- 所以无论噪声多极端,只要窗口内正常像素占多数,中值就不会被污染。
我记得有一次做PCB板缺陷检测,图像上全是椒盐噪声,用均值滤波试了好几次,噪声是淡了,但焊盘的边缘也糊了。换成中值滤波后,噪声几乎消失,边缘还保持得挺好。嗯,从那以后,只要看到椒盐噪声,我第一反应就是中值滤波。
个人经验:中值滤波对椒盐噪声的抑制效果几乎是“免疫”级别的。只要噪声密度不超过窗口内像素总数的一半,它就能完美恢复。但如果是高斯噪声那种每个像素都带点小毛病的,中值滤波反而效果一般。
3.3 窗口大小选择策略
窗口大小怎么选?这是中值滤波里最让人纠结的问题。我踩过不少坑,总结下来就几条原则:
| 窗口大小 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 3×3 | 轻度椒盐噪声、细节丰富的图像 | 细节保留好,计算快 | 对密集噪声效果有限 |
| 5×5 | 中等密度椒盐噪声 | 去噪效果适中,边缘尚可 | 细节有一定损失 |
| 7×7及以上 | 重度椒盐噪声 | 去噪能力强 | 细节模糊严重,图像变“油画” |
你可能会问:那直接用大窗口不就行了?我曾经也这么想,结果在一个指纹识别项目里,用了7×7的中值滤波,噪声是没了,但指纹的脊线和谷线都快分不清了。所以窗口大小不是越大越好,得看你的应用场景。
避坑指南:我曾经在一个实时视频处理项目里,用了5×5的中值滤波,结果帧率直接掉了一半。为啥?因为排序操作本身很耗时,窗口越大,排序的数据量呈平方级增长。如果对实时性有要求,3×3通常是性价比最高的选择。
3.4 自适应窗口策略
有没有一种方法,既能去噪又能保细节?有,那就是自适应窗口。思路是这样的:
- 先判断当前像素是不是噪声点(比如看它跟周围像素的差异是否过大)。
- 如果是噪声,用稍大的窗口做中值滤波。
- 如果不是噪声,用小的窗口甚至不做滤波。
我习惯用这种策略处理医学影像,因为病灶区域的细节太重要了,不能因为去噪把病灶给抹了。
3.5 中值滤波的变种
除了标准的中值滤波,还有几个变种在实际项目中很实用:
- 加权中值滤波:给窗口内的像素赋予不同权重,中心像素权重高一些,边缘低一些。适合在去噪的同时保留更多细节。
- 多级中值滤波:先用小窗口滤波,再用大窗口滤波,最后取两者的结果做融合。我曾在卫星图像处理中用过,效果不错。
- 快速中值滤波:利用直方图来加速排序过程,适合实时系统。说白了就是用空间换时间。
3.6 核心逻辑流程图
下面这张图帮你理清中值滤波的完整流程和窗口选择逻辑:
3.7 代码示例:OpenCV实现
最后,给一段实际可跑的代码。我用OpenCV实现中值滤波,顺便对比不同窗口大小的效果:
import cv2
import numpy as np
# 读取图像并添加椒盐噪声
img = cv2.imread('lena.jpg', 0)
noise_img = img.copy()
num_noise = 5000 # 噪声点数量
coords = [np.random.randint(0, i-1, num_noise) for i in img.shape]
noise_img[coords[0], coords[1]] = 255 # 盐噪声
coords = [np.random.randint(0, i-1, num_noise) for i in img.shape]
noise_img[coords[0], coords[1]] = 0 # 椒噪声
# 不同窗口大小的中值滤波
median_3 = cv2.medianBlur(noise_img, 3)
median_5 = cv2.medianBlur(noise_img, 5)
median_7 = cv2.medianBlur(noise_img, 7)
# 显示结果
cv2.imshow('Noise', noise_img)
cv2.imshow('Median 3x3', median_3)
cv2.imshow('Median 5x5', median_5)
cv2.imshow('Median 7x7', median_7)
cv2.waitKey(0)
cv2.destroyAllWindows()
跑一下这段代码,你会直观地看到:3×3窗口去噪干净,细节保留最好;7×7窗口虽然噪声全没了,但图像也变模糊了。这就是窗口大小选择的权衡。
小技巧:在实际项目中,我习惯先用3×3试跑一帧,看看噪声残留情况。如果还有明显噪点,再逐步增大窗口。别一上来就用大窗口,否则细节丢了就再也找不回来了。