4. 降噪滤波器原理(下):双边滤波与NLM
上一章聊了高斯滤波和均值滤波,说白了它们都是「空间域」的平滑操作。但有个问题一直困扰着我——边缘也一起被抹掉了。你想想看,降噪和保边,这俩需求天然就是矛盾的。
今天要讲的两种滤波器,就是专门来解决这个矛盾的。它们一个比一个聪明,当然,计算量也一个比一个大。
4.1 双边滤波:空间+像素值的双重保险
双边滤波(Bilateral Filter)的核心思想很简单:不仅要看距离,还要看像素值差异。
高斯滤波只考虑空间距离——离中心近的权重大,远的权重小。但双边滤波多了一个维度:像素值差异。如果邻域内某个像素的亮度值和中心点相差太大,就算它离得再近,权重也会被大幅降低。
我刚开始做ISP时,总觉得双边滤波就是个「加了边缘保护的高斯滤波」。后来在项目中调试夜景降噪才发现,这个理解太浅了。它的参数调起来,其实挺讲究的。
4.1.1 数学原理
双边滤波的权重由两部分相乘得到:
W(i,j) = Ws(i,j) × Wr(i,j)
其中:
Ws(i,j) = exp(-[(i-x)² + (j-y)²] / (2σs²)) // 空间权重
Wr(i,j) = exp(-[I(i,j) - I(x,y)]² / (2σr²)) // 像素值权重
最终输出就是邻域内所有像素的加权平均:
I'(x,y) = ΣW(i,j) × I(i,j) / ΣW(i,j)
这里有两个关键参数:
| 参数 | 含义 | 调大效果 | 调小效果 |
|---|---|---|---|
| σs | 空间标准差(滤波半径) | 平滑更强,边缘变模糊 | 降噪不足,保留细节 |
| σr | 像素值标准差(灰度容差) | 边缘保护变弱,类似高斯 | 边缘保留好,但降噪弱 |
核心理解:σr 决定了「多大的边缘才算边缘」。σr 越小,只有像素值非常接近的点才会参与平均,边缘保留得越完整;σr 越大,双边滤波就越接近普通高斯滤波。
4.1.2 适用场景
- 轻度噪声 + 强边缘场景:比如文字图像、线条图、边缘清晰的物体
- 需要保留纹理细节的降噪:比如皮肤纹理、织物纹理
- 作为边缘感知滤波的前处理:很多高级算法会先用双边滤波做保边平滑
我曾经踩过的坑:双边滤波对高斯噪声效果不错,但对椒盐噪声几乎无效。有一次我在项目中直接用双边滤波处理传感器原始数据,结果噪声没去掉,反而把坏点扩散成了「小亮点团」。后来才意识到,椒盐噪声的像素值差异太大,双边滤波的像素值权重直接把噪声点当成了「边缘」来保护。
4.1.3 代码实现
OpenCV 里直接调用就行,但参数得自己试:
import cv2
import numpy as np
# 读取图像
img = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)
# 双边滤波
# 参数:d=9 邻域直径,sigmaColor=75 像素值标准差,sigmaSpace=75 空间标准差
filtered = cv2.bilateralFilter(img, d=9, sigmaColor=75, sigmaSpace=75)
# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Bilateral', filtered)
cv2.waitKey(0)
我个人习惯的调参思路:先固定 σs(一般取 5-15),然后从 σr=30 开始往上调,观察边缘保留和降噪效果的平衡点。
4.2 非局部均值滤波(NLM)
双边滤波虽然保边,但它有个硬伤——只考虑局部邻域。如果噪声比较强,局部邻域里全是噪声,那再怎么加权平均也救不回来。
NLM 的思路就完全不一样了。它认为:图像里有很多相似的局部块。比如一块纯色天空,左上角和右下角的小块纹理应该是一样的。把这些相似块找出来一起平均,降噪效果会好得多。
我第一次看到 NLM 的时,觉得这个想法太优雅了。但实际跑起来……嗯,慢得让人怀疑人生。
4.2.1 核心原理
NLM 的流程可以概括为三步:
- 提取参考块:以当前像素为中心,取一个固定大小的块(比如 7×7)
- 搜索相似块:在整张图像(或一个搜索窗口内)寻找与参考块相似的块
- 加权平均:相似度越高的块,权重越大,最终加权平均得到输出
相似度的计算方式:
w(i,j) = exp(-||v(Ni) - v(Nj)||² / h²)
其中:
v(Ni) 和 v(Nj) 分别是以像素 i 和 j 为中心的图像块
||·||² 是两个块的欧氏距离平方
h 是滤波强度参数
参数 h 的作用:h 越大,相似度权重分布越均匀,降噪越强但细节丢失越多;h 越小,只有非常相似的块才会被纳入平均,细节保留好但降噪不足。我一般从 h=10 开始试,根据噪声水平上下调整。
4.2.2 适用场景
| 场景 | 推荐度 | 说明 |
|---|---|---|
| 高斯噪声 | ★★★★★ | NLM 对高斯噪声效果极好,尤其是中等强度噪声 |
| 纹理丰富的图像 | ★★★★☆ | 能保留纹理细节,但计算量大 |
| 实时视频处理 | ★☆☆☆☆ | 原始 NLM 太慢,需要优化版本 |
| 医学图像 | ★★★★★ | 对细节要求高,NLM 是经典选择 |
4.2.3 代码实现
OpenCV 提供了 fastNlMeansDenoising 系列函数:
import cv2
import numpy as np
# 读取图像
img = cv2.imread('noisy_image.jpg')
# NLM 降噪
# 参数:h=10 滤波强度,templateWindowSize=7 模板块大小,searchWindowSize=21 搜索窗口大小
denoised = cv2.fastNlMeansDenoisingColored(img, None, h=10,
templateWindowSize=7,
searchWindowSize=21)
# 显示结果
cv2.imshow('Original', img)
cv2.imshow('NLM Denoised', denoised)
cv2.waitKey(0)
性能警告:searchWindowSize 每增加一倍,计算量增加四倍。我建议在移动端或嵌入式平台上,searchWindowSize 不要超过 21,templateWindowSize 不要超过 7。否则帧率会掉到没法看。
4.3 三种滤波器对比
我把高斯滤波、双边滤波、NLM 放在一起对比一下,方便你选型:
| 特性 | 高斯滤波 | 双边滤波 | NLM |
|---|---|---|---|
| 保边能力 | 差 | 好 | 极好 |
| 降噪强度 | 中等 | 中等 | 强 |
| 计算速度 | 极快 | 中等 | 慢 |
| 参数数量 | 1个(σ) | 2个(σs, σr) | 3个(h, template, search) |
| 适用噪声 | 高斯噪声 | 高斯噪声 | 高斯噪声、均匀噪声 |
| 实时性 | ★★★★★ | ★★★☆☆ | ★☆☆☆☆ |
4.4 知识结构图
下面这张图帮你理清今天讲的内容:
4.5 选型建议
在实际项目中,我一般这样选:
- 实时性要求高(比如视频预览):用高斯滤波,或者优化版的双边滤波
- 需要保边降噪(比如拍照后处理):用双边滤波,参数调好效果不错
- 噪声特别强(比如暗光环境):上 NLM,虽然慢但效果好
- 移动端/嵌入式:别用原始 NLM,可以考虑积分图加速的双边滤波
我的个人习惯:在 ISP pipeline 里,我会把双边滤波放在去马赛克之后、锐化之前。这样既能降噪,又不会把噪声放大。NLM 我一般只用在最终输出前的后处理阶段,或者离线处理场景。
好了,这一章的内容就到这里。双边滤波和 NLM 都是「保边降噪」的经典方法,理解它们的原理和参数含义,后面调优的时候会顺手很多。