4. 经典去噪算法(二):双边滤波、非局部均值去噪(NLM)的原理与调参技巧
上一章我们聊了均值滤波和高斯滤波,说白了就是「周围像素取平均」。这招对付高斯噪声还行,但一碰到边缘就露馅了——边缘也被抹平了。今天要讲的这两个算法,才是真正有点「智能」的去噪方法。
我个人习惯把双边滤波叫做「带脑子的高斯滤波」,而NLM则是「全局找相似」的暴力美学。嗯,咱们一个一个来。
4.1 双边滤波:边缘保留的平滑
先想想高斯滤波的问题在哪?它只考虑了空间距离——离中心越近的像素权重越大。但边缘两侧的像素,虽然空间上挨着,像素值却差很多。强行平均,边缘就糊了。
双边滤波的改进很简单:再加一个像素值相似度的权重。也就是说,不仅要看距离近不近,还要看颜色像不像。
核心公式:
输出像素 = 邻域内每个像素的加权平均
权重 = 空间高斯权重 × 像素值高斯权重
空间权重:离中心越近,权重越大。
值域权重:像素值与中心越接近,权重越大。
这样一来,在平坦区域,大家像素值都差不多,值域权重接近1,效果跟高斯滤波差不多。但在边缘处,边缘另一侧的像素值差异大,值域权重变得很小,几乎不影响中心像素——边缘就保住了。
4.1.1 两个关键参数
| 参数 | 符号 | 作用 | 典型范围 |
|---|---|---|---|
| 空间标准差 | σspace | 控制空间邻域大小 | 1~10 |
| 值域标准差 | σrange | 控制像素值相似度的敏感度 | 10~100(8位图) |
调参经验:
- σspace 越大,平滑范围越大,但计算量也越大
- σrange 越小,边缘保护越强,但去噪能力下降
- σrange 太大,双边滤波退化成高斯滤波
我的经验:处理自然图像时,我一般先固定 σspace=3,然后调整 σrange。从30开始试,如果边缘还太锐利就加大,如果噪声没去干净就减小。说白了就是个平衡游戏。
4.1.2 代码示例
import cv2
import numpy as np
def bilateral_filter_demo(img, d=9, sigma_color=50, sigma_space=3):
"""
d: 邻域直径
sigma_color: 值域标准差(对应 σ_range)
sigma_space: 空间标准差(对应 σ_space)
"""
result = cv2.bilateralFilter(img, d, sigma_color, sigma_space)
return result
# 使用示例
img = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)
denoised = bilateral_filter_demo(img, d=9, sigma_color=50, sigma_space=3)
cv2.imwrite('bilateral_result.jpg', denoised)
注意:双边滤波的计算量跟邻域大小直接相关。d=9时,每个像素要计算81个权重。如果图像很大,建议先缩小再处理,或者用OpenCV的快速近似版本。
4.2 非局部均值去噪(NLM)
双边滤波虽然保边,但它只看了局部邻域。你想想看,图像里很多结构是重复的——比如墙上的纹理、草地上的图案。这些重复区域,其实可以互相帮忙去噪。
NLM的核心思想就是:在整个图像中寻找与当前像素块相似的块,然后加权平均。说白了,就是「你不是一个人在战斗」。
4.2.1 算法流程
- 以当前像素为中心,取一个小块(比如7×7)
- 在整个图像(或搜索窗口内)搜索相似块
- 计算每个相似块与当前块的相似度(通常用欧氏距离)
- 根据相似度给每个块分配权重,加权平均得到去噪结果
关键点:相似度不是比单个像素,而是比整个块的像素分布。这样即使有噪声,块的整体结构也能匹配上。
4.2.2 三个核心参数
| 参数 | 说明 | 典型值 |
|---|---|---|
| 搜索窗口大小 | 在多大范围内找相似块 | 21×21 |
| 块大小 | 用于比较的块尺寸 | 7×7 |
| 滤波强度(h) | 控制权重的衰减速度 | 10~30 |
调参技巧:
- 搜索窗口越大,找到相似块的机会越多,但计算量暴增
- 块大小太小,区分度不够;块太大,找不到相似块
- h值越大,平滑越强,但细节丢失也越多
避坑指南:我曾经在一个纹理丰富的图像上用了默认参数,结果等了半分钟才出结果。后来把搜索窗口从21×21缩小到11×11,速度提升了4倍,效果几乎没差别。记住:不是搜索范围越大越好,够用就行。
4.2.3 代码示例
import cv2
def nlm_denoise_demo(img, h=10, templateWindowSize=7, searchWindowSize=21):
"""
h: 滤波强度,越大平滑越强
templateWindowSize: 块大小(必须是奇数)
searchWindowSize: 搜索窗口大小(必须是奇数)
"""
result = cv2.fastNlMeansDenoising(img, None, h, templateWindowSize, searchWindowSize)
return result
# 使用示例
img = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)
denoised = nlm_denoise_demo(img, h=15, templateWindowSize=7, searchWindowSize=21)
cv2.imwrite('nlm_result.jpg', denoised)
4.3 双边滤波 vs NLM:怎么选?
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 边缘清晰的图像(如文字、图标) | 双边滤波 | 速度快,边缘保护极好 |
| 纹理丰富的图像(如布料、草地) | NLM | 能利用重复纹理,去噪更干净 |
| 实时处理需求 | 双边滤波 | NLM太慢,不适合实时 |
| 高噪声图像 | NLM | 全局搜索能获得更多有效信息 |
重要提醒:NLM的OpenCV实现默认只处理单通道灰度图。如果是彩色图,需要用 fastNlMeansDenoisingColored,它会分别处理YCrCb空间的亮度通道和色度通道。
4.4 知识体系结构图
嗯,到这里双边滤波和NLM的核心内容就讲完了。这两个算法在实际项目中非常实用,尤其是当你需要保留边缘细节的时候。我个人建议初学者先从双边滤波入手,参数少、效果好、速度快。等你对去噪有了感觉,再挑战NLM——你会发现它处理纹理噪声时确实有两把刷子。