空间域降噪算法(一):均值滤波与高斯滤波、中值滤波与双边滤波、非局部均值(NLM)降噪、BM3D算法原理与实现
各位好,欢迎来到第四章。
前面我们聊了噪声模型和图像质量评价。从这章开始,要动真格的了——我们正式进入降噪算法的核心地带。空间域降噪,说白了就是直接在像素点上做文章。它不依赖频域变换,思路更直观,但门道一点都不少。
我个人习惯把空间域算法分成两类:局部滤波和非局部滤波。局部滤波看的是像素周围一个小窗口,非局部滤波则在整个图像里找相似块。这章我们把这几个经典算法串起来讲,你会发现它们之间有一条清晰的进化脉络。
4.1 均值滤波与高斯滤波
先聊最基础的。均值滤波,就是把窗口内所有像素值加起来求平均。你想想看,如果窗口里有噪声点,周围像素一平均,噪声确实被压下去了,但边缘也糊了。为什么?因为边缘两侧的像素值差异很大,平均之后边缘就变成了平滑过渡。
高斯滤波稍微聪明一点。它给窗口中心附近的像素更高的权重,离中心越远权重越低。这个权重分布服从高斯函数。我在项目中遇到过一个问题:用高斯滤波处理星空夜景,星星全被抹成了光晕。后来发现是sigma参数设太大了。
| 特性 | 均值滤波 | 高斯滤波 |
|---|---|---|
| 权重策略 | 均匀权重 | 距离高斯权重 |
| 边缘保护 | 差 | 较差 |
| 计算复杂度 | O(k²) | O(k²) |
| 主要问题 | 振铃效应 | 细节丢失 |
4.2 中值滤波
中值滤波的思路完全不同。它不求和,不平均,而是把窗口内所有像素值排序,取中间那个值。这招对付椒盐噪声特别管用。为什么?因为椒盐噪声是极端值,排序后要么在最前要么在最后,永远选不到中间。
我曾经在调试一款安防摄像头的ISP时,发现图像上全是白点。一开始以为是sensor坏了,后来发现是传输链路上有干扰,产生了椒盐噪声。中值滤波一上,干净了。但要注意,中值滤波对高斯噪声效果一般,而且会抹掉细线条——比如电线、树枝这些。
// 中值滤波核心实现(C语言风格伪代码)
unsigned char median_filter(unsigned char *window, int size) {
// 1. 将窗口像素复制到临时数组
unsigned char temp[size * size];
memcpy(temp, window, size * size);
// 2. 排序(这里用简单的冒泡,实际会用快速选择)
for (int i = 0; i < size * size - 1; i++) {
for (int j = 0; j < size * size - 1 - i; j++) {
if (temp[j] > temp[j + 1]) {
unsigned char t = temp[j];
temp[j] = temp[j + 1];
temp[j + 1] = t;
}
}
}
// 3. 返回中值
return temp[size * size / 2];
}
4.3 双边滤波
双边滤波是我个人非常喜欢的一个算法。它解决了高斯滤波的一个核心痛点:边缘模糊。怎么做呢?它同时考虑两个因素:空间距离和像素值差异。
空间距离权重和高斯滤波一样——离中心近的权重大。像素值差异权重呢?如果邻域像素和中心像素值相差很大,就认为它属于另一个区域,权重给得很小。这样一来,在平坦区域,双边滤波就像高斯滤波一样平滑;在边缘处,它只取同侧像素做平均,边缘就保住了。
嗯,这里要注意一个坑。双边滤波的参数有三个:空间sigma(σs)和值域sigma(σr)。σs控制空间范围,σr控制像素值差异的敏感度。我调试夜景照片时,σr设太小,噪声去不掉;设太大,边缘又糊了。后来总结了一个经验公式:σr取图像噪声标准差的两倍左右。
BF[I]p = (1/Wp) · Σq∈S Gσs(||p-q||) · Gσr(|Ip-Iq|) · Iq
其中Wp是归一化因子,Gσ是高斯函数。
4.4 非局部均值(NLM)降噪
双边滤波虽然能保边,但它还是局限在一个小窗口里。NLM的思路就大胆多了——它认为图像里有很多重复的纹理和结构,可以在整张图里找相似块来做平均。
具体怎么做?对于每个像素点,以它为中心取一个块(比如7x7),然后在全图搜索相似的块。相似度用块之间的欧氏距离衡量。距离越小,权重越大。最后把所有相似块加权平均,得到去噪后的像素值。
我在做手机夜景降噪时试过NLM。效果确实好,纹理保留得比双边滤波强很多。但代价也大——计算量太大了。一张1200万像素的照片,全图搜索基本跑不动。后来用了搜索窗口限制(比如21x21),再加上积分图加速,才勉强在工程上可用。
| 算法 | 搜索范围 | 相似度度量 | 计算量 | 去噪质量 |
|---|---|---|---|---|
| 双边滤波 | 局部窗口 | 像素值差 | 低 | 中等 |
| NLM | 全局/大窗口 | 块间欧氏距离 | 高 | 好 |
| BM3D | 全局+变换域 | 块匹配+协同滤波 | 很高 | 极好 |
4.5 BM3D算法原理与实现
终于到BM3D了。这个算法全称是Block-Matching and 3D Filtering,块匹配三维滤波。它可以说是空间域降噪的集大成者,也是很多ISP降噪模块的参考基准。
BM3D的核心思想分两步:分组和协同滤波。
第一步,分组。和NLM类似,BM3D也在图像里搜索相似块。但BM3D把这些相似块叠在一起,形成一个三维数组。你想想看,每个块是二维的,叠起来就是三维的——两个空间维度加一个块索引维度。
第二步,协同滤波。对这个三维数组做三维变换(通常是DCT或小波),在变换域里做硬阈值或维纳滤波,再反变换回去。为什么这么做?因为相似块在变换域里,噪声是随机分布的,而真实信号是相关的。阈值一砍,噪声就没了。
BM3D有两个阶段:基础估计和最终估计。基础估计用硬阈值滤波,得到一个初步的去噪结果。最终估计用维纳滤波,基于基础估计的结果做更精细的滤波。
// BM3D算法流程(伪代码)
void BM3D(float *image, float *denoised, int width, int height) {
// 阶段一:基础估计
for each reference block at position (i, j) {
// 1. 块匹配:在全图搜索相似块
List blocks = block_matching(image, i, j, block_size, search_window);
// 2. 分组:将相似块叠成3D数组
float group[block_size][block_size][blocks.count];
for each matched block {
group[:, :, k] = matched_block;
}
// 3. 协同硬阈值滤波
// 3.1 3D变换(DCT + 小波)
// 3.2 硬阈值处理
// 3.3 3D反变换
float filtered_group = collaborative_hard_threshold(group, sigma);
// 4. 聚合:将滤波后的块加权放回原图
aggregate(filtered_group, i, j, weight);
}
// 阶段二:最终估计(使用维纳滤波)
for each reference block {
// 使用基础估计结果做维纳滤波
float final_group = collaborative_wiener_filter(
original_group, basic_estimate_group, sigma);
aggregate(final_group, i, j, weight);
}
}
- 计算量极大,在移动端ISP上基本跑不动实时。我见过有人用GPU加速,或者只对关键帧做BM3D。
- 参数敏感:块大小、搜索窗口、阈值、步长,每个参数都影响最终效果。
- 纹理重复度低的图像(比如纯风景),BM3D的优势不明显。
4.6 算法对比与选型建议
讲了这么多,到底在实际项目中怎么选?我根据自己的经验给一个参考:
- 实时预览场景(30fps以上):用高斯滤波或双边滤波,配合查表加速。BM3D想都别想。
- 夜景多帧合成:先做帧间对齐,然后用NLM或BM3D做单帧降噪。我一般用NLM,因为BM3D太慢了。
- 椒盐噪声:中值滤波是首选,3x3窗口足够。
- 高ISO照片(ISO 3200以上):BM3D效果最好,但要做好计算资源规划。
- 边缘保护要求高:双边滤波或NLM,别用均值/高斯。