把相机对准一面均匀的灰墙,固定机位、光圈和快门,连续拍几十张照片。墙没有改变,同一个像素的数字却会轻微上下跳动;提高 ISO 或观察阴影,这种跳动会更明显。
降噪要从这些不稳定的测量中估计出更接近真实场景的信号。困难在于,算法看到的只有一串数字:一条纤细的头发和一次随机波动都可能表现为相邻像素之间的差异。把变化全部抹平,噪声确实消失了,细节也一起消失了。
本文从一个最简单的传感器噪声模型出发,比较均值、高斯、中值和双边滤波。重点不是宣布某个滤波器“最好”,而是看清三个问题:哪些邻居参与估计、各自占多少权重、算法怎样区分噪声与结构。
本文只建立经典空间降噪的基线,不展开 NLM、BM3D、时域降噪与神经网络。示例使用灰度或亮度数据,是为了隔离核心机制;彩色图像还要处理通道相关性、CFA 与色度噪声。
一块均匀区域为什么不均匀
相机输出不是场景光强的完美抄本。即使场景与曝光都不变,光子到达、电子读出与数字量化仍会带来随机变化。
教学中常把线性 RAW 的噪声方差近似成:
其中 是期望信号:
- 表示与信号相关的部分,主要对应 shot noise;
- 表示与信号关系较弱的底噪,可以近似包含 read noise;
- 标准差是 。
有时也把读出噪声标准差记为 ,写成:
一块均匀灰,为什么每个像素都不同
同一信号重复采样 72 次。Shot noise 随信号上升,read noise 在暗部仍然存在;所有点由固定种子生成,拖回同一参数会得到同一结果。
这是常用的异方差高斯近似。真实传感器还会有行噪声、固定图样噪声、量化与裁剪,本文先隔离最基本的信号相关噪声。
组件中的折线不是一段固定图片,而是同一信号的 72 次确定性模拟采样。增大信号以后,shot noise 的绝对标准差会上升;但信号本身也在上升,所以信噪比仍可能改善。把画面简单地说成“越亮噪声越大”或“越暗噪声越大”,都混淆了绝对波动与相对可见度。
Shot noise:光子本来就不是整齐到达的
一段曝光时间内到达感光位置的光子数具有随机性。对理想的 Poisson 过程,若平均计数是 ,方差约为 ,标准差约为 ,因此信噪比近似:
接收到更多光子,随机波动的绝对数目会上升,但相对误差下降。这也是“向右曝光”在不饱和的前提下可以改善阴影信噪比的原因之一:关键是传感器真正收到了更多光,而不是把已有数字在后期乘大。
Read noise:没有光也不等于没有波动
传感器读出、电荷转换与模拟前端也会引入噪声。即使输入接近黑电平,仍可能看到一定宽度的数值分布。它通常不像 shot noise 那样与信号成简单正比,因此在暗部占比尤其显著。
真实相机还可能出现:
- 固定图样噪声与像素响应差异;
- 行噪声、列噪声和条带;
- 暗电流与热像素;
- ADC 量化和数字截断;
- 模拟增益、数字增益与不同读出模式带来的模型变化。
一个方差公式不可能包含所有现象,但它能帮助我们理解:降噪强度不应该只由像素亮度或 ISO 标签机械决定,最好建立与传感器模式对应的噪声模型。
降噪到底在估计什么
设观测图像是:
是未知的干净信号, 是噪声,我们只拿到了 。如果允许重复拍摄同一个静止场景,把无限多帧求平均可以让独立随机噪声趋近于零;单帧降噪却没有这种奢侈条件,只能利用空间或颜色上的先验。
最常见的空间先验是:附近像素往往来自同一个平滑表面,所以它们可以互相提供信息。 问题也随之出现:物体边缘两侧明明属于不同表面,却同样“离得很近”。
均值滤波:每个邻居都有一票
半径为 1 的方形均值滤波使用 邻域:
如果九个样本来自同一个均匀表面、噪声相互独立,平均可以降低方差。但窗口跨过黑白边缘时,黑侧与白侧也被平等混合,边缘就被拖成一段灰色坡道。
Python 教学实现如下:
import numpy as np
def box_filter(image, radius=1): """边界使用 reflect,输入应为浮点线性数据。""" size = radius * 2 + 1 padded = np.pad(image, radius, mode="reflect") output = np.empty_like(image, dtype=np.float32)
for row in range(image.shape[0]): for column in range(image.shape[1]): patch = padded[ row:row + size, column:column + size, ] output[row, column] = patch.mean() return output这段代码便于解释,不是高效实现。生产代码会利用可分离卷积、积分图、SIMD、GPU 或经过优化的库函数。
Gaussian 滤波:近处的邻居更重要
Gaussian 核根据空间距离分配权重:
输出是归一化加权平均:
距离中心越近,权重越大。Gaussian 比同尺寸均值核更平滑地衰减,对高频噪声很有效;但它仍然只知道距离,不知道两个像素是否位于同一物体上,所以照样会模糊边缘。
二维 Gaussian 可以拆成先横向、再纵向两次一维卷积,计算量从 降到 。这也是许多实时管线喜欢可分离核的重要原因。
中值滤波:不求平均,直接做排序
中值滤波把邻域值排序,选择中间一项:
[0.18, 0.20, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.98] ↓ 中值 0.23孤立的 0.98 几乎不会影响结果,所以中值滤波很适合 salt-and-pepper noise、热像素或孤立脉冲。对于近似 Gaussian 的连续波动,它未必比专门的线性或统计方法更有效;窗口增大以后,细线和小亮点也可能被当作异常值删除。
def median_filter(image, radius=1): size = radius * 2 + 1 padded = np.pad(image, radius, mode="reflect") output = np.empty_like(image, dtype=np.float32)
for row in range(image.shape[0]): for column in range(image.shape[1]): patch = padded[row:row + size, column:column + size] output[row, column] = np.median(patch) return output三种滤波器,同一份噪声
均值只认邻域,Gaussian 更重视近处,中值直接选择排序后的中间数。切换斜边和纹理,误差下降并不保证细节更自然。
中值滤波特别擅长孤立脉冲,却不是所有传感器噪声的最佳模型。半径越大,平坦区越干净,跨边缘混合也越严重。
在斜边与细纹理之间来回切换。一个参数可能让平均绝对误差下降,却把周期纹理的峰谷压扁。指标只回答它定义的问题,不会自动知道你更在意皮肤平滑、文字边缘还是织物纹理。
为什么普通平滑会破坏边缘
均值和 Gaussian 的权重只依赖坐标。只要两个像素在窗口里,即使一个接近黑色、另一个接近白色,也会参与平均。
要保留边缘,算法还需要第二个判断:邻居的数值是否与中心相似。双边滤波正是把空间距离与数值距离结合起来。
双边滤波:既要离得近,也要长得像
双边滤波的空间权重仍然是:
另外增加范围权重:
最终权重是两者乘积:
同一侧的像素既靠得近、亮度也相似,权重大;边缘另一侧虽然坐标接近,数值差很大,范围权重会把它压低。
双边滤波为什么知道哪里是边缘
空间权重问“离我多远”,范围权重问“和我多像”。只有两个问题都通过的邻居,才会对当前像素产生较大影响。
把 σr 拉大,亮暗两侧会越来越愿意互相平均,双边滤波逐渐接近普通空间平滑;拉得太小,真实噪声也会被当成需要保护的细节。
控制空间范围, 控制愿意跨越多大的数值差:
- 很大时,数值差异几乎不起作用,双边滤波接近普通 Gaussian;
- 很小时,连随机噪声造成的差异也可能被当成边缘,降噪变弱;
- 噪声强度变化时,固定的 不一定适合整幅图。
一份直接但较慢的 Python 实现
import mathimport numpy as np
def bilateral_filter(image, radius, sigma_space, sigma_range): padded = np.pad(image, radius, mode="reflect") output = np.empty_like(image, dtype=np.float32)
for row in range(image.shape[0]): for column in range(image.shape[1]): center = padded[row + radius, column + radius] weighted_sum = 0.0 weight_sum = 0.0
for dy in range(-radius, radius + 1): for dx in range(-radius, radius + 1): neighbor = padded[ row + radius + dy, column + radius + dx, ]15 collapsed lines
spatial = math.exp( -(dx * dx + dy * dy) / (2.0 * sigma_space * sigma_space) ) difference = neighbor - center value_weight = math.exp( -(difference * difference) / (2.0 * sigma_range * sigma_range) ) weight = spatial * value_weight weighted_sum += neighbor * weight weight_sum += weight
output[row, column] = weighted_sum / weight_sum return output朴素双边滤波对每个像素遍历二维窗口,计算开销明显高于可分离 Gaussian。工程中可能采用近似、查表、降采样引导图、网格方法或专用硬件加速。
边缘保持不等于细节保持
一条高对比黑白边缘很容易识别,低对比纹理却和噪声处于相近幅度。把范围阈值设得很小,算法可能把每一个噪点都保护下来;设得很大,细小纹理又被并入平坦区。
更干净,还是更有细节?
同一个强度不会同时最优地处理平坦区、周期纹理和边缘。调节半径与混合强度,观察残余噪声下降时,纹理对比也怎样被压低。
MAE 最小的参数不一定最好看:误差指标会奖励平滑,却不知道哪些微小变化是有意义的纹理。真实调参还要分别检查平坦区、边缘和重复结构。
这个实验故意使用细密周期纹理。提高平滑强度会让残余噪声下降,同时输出最大值与最小值之间的差也缩小。所谓“塑料感”,往往不是所有边缘都消失,而是皮肤、织物和头发中的低对比微结构被过度压平。
Bayer 域降噪与 RGB 域降噪
RAW Bayer 上的每个位置只测量一个颜色。Bayer 域降噪可以在 Debayer 之前阻止噪声被插值传播,但邻域里交错排列着不同通道,不能把相邻 RAW 数字当成同一种灰度直接平均。
以 RGGB 为例,红样本在水平和垂直方向通常相隔两个像素。逐通道 Bayer 降噪要根据 CFA Pattern 寻找同色邻居,或使用了解 CFA 结构的联合模型。
RGB 域降噪面对的是完整颜色像素,空间结构更直观,也可以把亮度与色度分别处理。但 Debayer 已经让一个 RAW 噪声样本影响多个输出像素,噪声出现相关性,假色与插值伪影也混在其中。
二者不是非此即彼。实际 ISP 可能先做轻量 Bayer 降噪,再在 RGB/YUV 域处理残余亮度与色度噪声。
白平衡为什么会改变噪声
如果蓝通道白平衡增益是 ,信号与噪声都会一起放大:
因此不同光源下,同一个传感器通道的输出噪声可能不同。降噪参数若在白平衡前标定,却直接用于白平衡后的 RGB,就要把增益对方差的影响考虑进去:
这也解释了为什么低照度暖光下的蓝色阴影常常特别难处理:原始蓝信号弱,需要较大增益,噪声同时被显著抬起。
在线性域还是 Gamma 域降噪
传感器噪声模型通常建立在线性 RAW 上。Gamma 或 sRGB 编码会非线性地拉伸暗部、压缩亮部,同样幅度的线性噪声经过曲线后会变成不同的编码波动。
这并不意味着编码域绝对不能降噪;许多图片应用只能得到 JPEG,也会在亮度/色度或感知空间工作。但参数和噪声假设必须与数据域一致。把在线性 RAW 上估计的 原封不动套到 sRGB 数值上,模型就失去了原来的意义。
降噪与锐化谁先做
锐化会增强局部高频变化,而噪声也属于高频。如果先强锐化再降噪,噪点会被扩大成更难抑制的斑点或边缘;多数管线会先降低噪声,再有控制地恢复清晰感。
但降噪过强以后再锐化,并不能找回已经被抹掉的真实纹理,只会增强剩余轮廓,甚至制造 halo。两者应联合调参:降噪保留足够结构,锐化只补偿成像与滤波造成的合理模糊。
怎样评价降噪效果
如果有干净参考图,可以计算均方误差:
以及常见的 PSNR:
但指标必须结合测试数据解释:
- PSNR 偏好平均意义上的像素接近,可能奖励过度平滑;
- SSIM 更关注局部结构,仍不能代表所有视觉偏好;
- 没有干净参考时,不能把单张输出的“平滑程度”冒充准确率;
- 真实噪声、合成 Gaussian 噪声和 JPEG 压缩噪声不是同一种分布。
至少要分别观察:平坦区残余噪声、斜边锐度、低对比纹理、细线、重复图案、暗部彩噪与运动区域。一个总分很难代替这些分项。
边界和裁剪同样会制造问题
滤波窗口到达图像边缘时会缺少邻居。常见策略包括复制、镜像、环绕或缩小有效输出区域。对普通照片,环绕会把右边缘错误带到左边缘,通常不是合理选择。
降噪前后也不要无意识地反复 clip。负值和超过白电平的值可能是噪声波动,也可能携带后续黑位、高光处理需要的信息。是否裁剪应由管线的数据约定决定,而不是为了让数组“看起来合法”。
工程里最容易踩的坑
1. 用一个固定强度处理所有亮度
Shot noise 与信号相关,暗部和亮部的最佳阈值通常不同。应根据噪声模型或局部置信度调整参数。
2. 把相邻 Bayer 数字直接当灰度邻居
它们可能来自不同滤光片。CFA Pattern、坐标相位和同色距离必须进入算法。
3. 只测试平坦色块
平坦区最容易降噪,却不能暴露边缘模糊、纹理消失和重复图案伪影。
4. 把中值滤波当成万能去噪
中值擅长脉冲异常,不代表它对 shot/read noise 也总是最优。
5. 在错误的数据域使用噪声模型
线性 RAW、白平衡后的 RGB、Gamma 编码 JPEG 具有不同的噪声分布与数值意义。
6. 忽略白平衡与模拟增益
通道增益改变噪声方差。相同 ISO 标签在不同传感器模式下也可能对应不同读出噪声。
7. 只追求单一指标
指标改善可能来自把所有细节都推向局部平均值。需要结合结构测试图和真实照片复核。
应该怎样选择
如果目标是去除孤立热像素或椒盐噪声,中值或专门的坏点校正通常比大范围平滑更合适。
如果噪声接近均匀 Gaussian、实时性能优先,可分离 Gaussian 是清晰、稳定的基线。它会模糊边缘,但易于分析和加速。
如果希望在小范围内兼顾平坦区平滑与强边缘保护,双边滤波是理解“空间权重 × 范围权重”的好起点。它的参数依赖噪声尺度,计算量也更高。
面对强噪声、重复纹理和复杂结构时,只依赖局部窗口很快会碰到上限。NLM 会寻找更远处的相似 patch,BM3D 在协同变换域处理相似块,学习型方法则从数据中获得更复杂的图像先验——这些值得后续单独展开,而不是塞进一个“高级模式”按钮。
最后记住:降噪不是把画面变得尽可能平,而是在不确定的测量中做估计。均值问“周围平均是多少”,Gaussian 再问“谁离我更近”,双边滤波继续问“谁和我更像”。每多一个假设,都可能更好地保护某类结构,也可能在假设不成立时制造新的伪影。
一套可信的降噪实现,应当说清楚噪声模型、数据域、邻域、权重、边界和评价方法。只有这些条件透明,“更干净”才不会悄悄变成“信息更少”。