4046 字
20 分钟
降噪为什么会抹掉细节?从高斯滤波走到双边滤波

把相机对准一面均匀的灰墙,固定机位、光圈和快门,连续拍几十张照片。墙没有改变,同一个像素的数字却会轻微上下跳动;提高 ISO 或观察阴影,这种跳动会更明显。

降噪要从这些不稳定的测量中估计出更接近真实场景的信号。困难在于,算法看到的只有一串数字:一条纤细的头发和一次随机波动都可能表现为相邻像素之间的差异。把变化全部抹平,噪声确实消失了,细节也一起消失了。

本文从一个最简单的传感器噪声模型出发,比较均值、高斯、中值和双边滤波。重点不是宣布某个滤波器“最好”,而是看清三个问题:哪些邻居参与估计、各自占多少权重、算法怎样区分噪声与结构。

本文只建立经典空间降噪的基线,不展开 NLM、BM3D、时域降噪与神经网络。示例使用灰度或亮度数据,是为了隔离核心机制;彩色图像还要处理通道相关性、CFA 与色度噪声。

一块均匀区域为什么不均匀#

相机输出不是场景光强的完美抄本。即使场景与曝光都不变,光子到达、电子读出与数字量化仍会带来随机变化。

教学中常把线性 RAW 的噪声方差近似成:

σ2(x)=ax+b\sigma^2(x)=ax+b

其中 xx 是期望信号:

  • axax 表示与信号相关的部分,主要对应 shot noise;
  • bb 表示与信号关系较弱的底噪,可以近似包含 read noise;
  • 标准差是 σ(x)=ax+b\sigma(x)=\sqrt{ax+b}

有时也把读出噪声标准差记为 σr\sigma_r,写成:

σ2(x)=ax+σr2\sigma^2(x)=ax+\sigma_r^2

一块均匀灰,为什么每个像素都不同

同一信号重复采样 72 次。Shot noise 随信号上升,read noise 在暗部仍然存在;所有点由固定种子生成,拖回同一参数会得到同一结果。

连续重复采样 →
期望信号0.220
模型 σ0.0896
实测 σ0.0895
σ²(x) = 0.035 × x + 0.018²

这是常用的异方差高斯近似。真实传感器还会有行噪声、固定图样噪声、量化与裁剪,本文先隔离最基本的信号相关噪声。

组件中的折线不是一段固定图片,而是同一信号的 72 次确定性模拟采样。增大信号以后,shot noise 的绝对标准差会上升;但信号本身也在上升,所以信噪比仍可能改善。把画面简单地说成“越亮噪声越大”或“越暗噪声越大”,都混淆了绝对波动与相对可见度。

Shot noise:光子本来就不是整齐到达的#

一段曝光时间内到达感光位置的光子数具有随机性。对理想的 Poisson 过程,若平均计数是 NN,方差约为 NN,标准差约为 N\sqrt{N},因此信噪比近似:

SNR=NN=NSNR=\frac{N}{\sqrt{N}}=\sqrt{N}

接收到更多光子,随机波动的绝对数目会上升,但相对误差下降。这也是“向右曝光”在不饱和的前提下可以改善阴影信噪比的原因之一:关键是传感器真正收到了更多光,而不是把已有数字在后期乘大。

Read noise:没有光也不等于没有波动#

传感器读出、电荷转换与模拟前端也会引入噪声。即使输入接近黑电平,仍可能看到一定宽度的数值分布。它通常不像 shot noise 那样与信号成简单正比,因此在暗部占比尤其显著。

真实相机还可能出现:

  • 固定图样噪声与像素响应差异;
  • 行噪声、列噪声和条带;
  • 暗电流与热像素;
  • ADC 量化和数字截断;
  • 模拟增益、数字增益与不同读出模式带来的模型变化。

一个方差公式不可能包含所有现象,但它能帮助我们理解:降噪强度不应该只由像素亮度或 ISO 标签机械决定,最好建立与传感器模式对应的噪声模型。

降噪到底在估计什么#

设观测图像是:

y=x+ny=x+n

xx 是未知的干净信号,nn 是噪声,我们只拿到了 yy。如果允许重复拍摄同一个静止场景,把无限多帧求平均可以让独立随机噪声趋近于零;单帧降噪却没有这种奢侈条件,只能利用空间或颜色上的先验。

最常见的空间先验是:附近像素往往来自同一个平滑表面,所以它们可以互相提供信息。 问题也随之出现:物体边缘两侧明明属于不同表面,却同样“离得很近”。

均值滤波:每个邻居都有一票#

半径为 1 的方形均值滤波使用 3×33\times3 邻域:

x^i,j=19u=11v=11yi+u,j+v\hat{x}_{i,j}=\frac{1}{9}\sum_{u=-1}^{1}\sum_{v=-1}^{1}y_{i+u,j+v}

如果九个样本来自同一个均匀表面、噪声相互独立,平均可以降低方差。但窗口跨过黑白边缘时,黑侧与白侧也被平等混合,边缘就被拖成一段灰色坡道。

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 核根据空间距离分配权重:

ws(p,q)=exp(pq22σs2)w_s(p,q)=\exp\left(-\frac{\lVert p-q\rVert^2}{2\sigma_s^2}\right)

输出是归一化加权平均:

x^p=qΩws(p,q)yqqΩws(p,q)\hat{x}_p=\frac{\sum_{q\in\Omega}w_s(p,q)y_q}{\sum_{q\in\Omega}w_s(p,q)}

距离中心越近,权重越大。Gaussian 比同尺寸均值核更平滑地衰减,对高频噪声很有效;但它仍然只知道距离,不知道两个像素是否位于同一物体上,所以照样会模糊边缘。

二维 Gaussian 可以拆成先横向、再纵向两次一维卷积,计算量从 O(k2)O(k^2) 降到 O(2k)O(2k)。这也是许多实时管线喜欢可分离核的重要原因。

中值滤波:不求平均,直接做排序#

中值滤波把邻域值排序,选择中间一项:

[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 更重视近处,中值直接选择排序后的中间数。切换斜边和纹理,误差下降并不保证细节更自然。

含噪输入
滤波结果
输入 MAE ×25525.9
输出 MAE ×25515.2
窗口3 × 3
滤波方法
测试结构
半径

中值滤波特别擅长孤立脉冲,却不是所有传感器噪声的最佳模型。半径越大,平坦区越干净,跨边缘混合也越严重。

在斜边与细纹理之间来回切换。一个参数可能让平均绝对误差下降,却把周期纹理的峰谷压扁。指标只回答它定义的问题,不会自动知道你更在意皮肤平滑、文字边缘还是织物纹理。

为什么普通平滑会破坏边缘#

均值和 Gaussian 的权重只依赖坐标。只要两个像素在窗口里,即使一个接近黑色、另一个接近白色,也会参与平均。

要保留边缘,算法还需要第二个判断:邻居的数值是否与中心相似。双边滤波正是把空间距离与数值距离结合起来。

双边滤波:既要离得近,也要长得像#

双边滤波的空间权重仍然是:

ws(p,q)=exp(pq22σs2)w_s(p,q)=\exp\left(-\frac{\lVert p-q\rVert^2}{2\sigma_s^2}\right)

另外增加范围权重:

wr(p,q)=exp((ypyq)22σr2)w_r(p,q)=\exp\left(-\frac{(y_p-y_q)^2}{2\sigma_r^2}\right)

最终权重是两者乘积:

x^p=qΩws(p,q)wr(p,q)yqqΩws(p,q)wr(p,q)\hat{x}_p= \frac{ \sum_{q\in\Omega}w_s(p,q)w_r(p,q)y_q }{ \sum_{q\in\Omega}w_s(p,q)w_r(p,q) }

同一侧的像素既靠得近、亮度也相似,权重大;边缘另一侧虽然坐标接近,数值差很大,范围权重会把它压低。

双边滤波为什么知道哪里是边缘

空间权重问“离我多远”,范围权重问“和我多像”。只有两个问题都通过的邻居,才会对当前像素产生较大影响。

暗区亮区
含噪Gaussian双边
边缘对比0.510
空间 σ2.2
范围 σ0.12

把 σr 拉大,亮暗两侧会越来越愿意互相平均,双边滤波逐渐接近普通空间平滑;拉得太小,真实噪声也会被当成需要保护的细节。

σs\sigma_s 控制空间范围,σr\sigma_r 控制愿意跨越多大的数值差:

  • σr\sigma_r 很大时,数值差异几乎不起作用,双边滤波接近普通 Gaussian;
  • σr\sigma_r 很小时,连随机噪声造成的差异也可能被当成边缘,降噪变弱;
  • 噪声强度变化时,固定的 σr\sigma_r 不一定适合整幅图。

一份直接但较慢的 Python 实现#

import math
import 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。工程中可能采用近似、查表、降采样引导图、网格方法或专用硬件加速。

边缘保持不等于细节保持#

一条高对比黑白边缘很容易识别,低对比纹理却和噪声处于相近幅度。把范围阈值设得很小,算法可能把每一个噪点都保护下来;设得很大,细小纹理又被并入平坦区。

更干净,还是更有细节?

同一个强度不会同时最优地处理平坦区、周期纹理和边缘。调节半径与混合强度,观察残余噪声下降时,纹理对比也怎样被压低。

残余 σ0.0759
细节对比保留101%
MAE ×25515.6
Gaussian 半径
观察结果

MAE 最小的参数不一定最好看:误差指标会奖励平滑,却不知道哪些微小变化是有意义的纹理。真实调参还要分别检查平坦区、边缘和重复结构。

这个实验故意使用细密周期纹理。提高平滑强度会让残余噪声下降,同时输出最大值与最小值之间的差也缩小。所谓“塑料感”,往往不是所有边缘都消失,而是皮肤、织物和头发中的低对比微结构被过度压平。

Bayer 域降噪与 RGB 域降噪#

RAW Bayer 上的每个位置只测量一个颜色。Bayer 域降噪可以在 Debayer 之前阻止噪声被插值传播,但邻域里交错排列着不同通道,不能把相邻 RAW 数字当成同一种灰度直接平均。

以 RGGB 为例,红样本在水平和垂直方向通常相隔两个像素。逐通道 Bayer 降噪要根据 CFA Pattern 寻找同色邻居,或使用了解 CFA 结构的联合模型。

RGB 域降噪面对的是完整颜色像素,空间结构更直观,也可以把亮度与色度分别处理。但 Debayer 已经让一个 RAW 噪声样本影响多个输出像素,噪声出现相关性,假色与插值伪影也混在其中。

二者不是非此即彼。实际 ISP 可能先做轻量 Bayer 降噪,再在 RGB/YUV 域处理残余亮度与色度噪声。

白平衡为什么会改变噪声#

如果蓝通道白平衡增益是 gBg_B,信号与噪声都会一起放大:

B=gB(B+nB)=gBB+gBnBB'=g_B(B+n_B)=g_BB+g_Bn_B

因此不同光源下,同一个传感器通道的输出噪声可能不同。降噪参数若在白平衡前标定,却直接用于白平衡后的 RGB,就要把增益对方差的影响考虑进去:

Var(gBnB)=gB2Var(nB)\operatorname{Var}(g_Bn_B)=g_B^2\operatorname{Var}(n_B)

这也解释了为什么低照度暖光下的蓝色阴影常常特别难处理:原始蓝信号弱,需要较大增益,噪声同时被显著抬起。

在线性域还是 Gamma 域降噪#

传感器噪声模型通常建立在线性 RAW 上。Gamma 或 sRGB 编码会非线性地拉伸暗部、压缩亮部,同样幅度的线性噪声经过曲线后会变成不同的编码波动。

这并不意味着编码域绝对不能降噪;许多图片应用只能得到 JPEG,也会在亮度/色度或感知空间工作。但参数和噪声假设必须与数据域一致。把在线性 RAW 上估计的 a,ba,b 原封不动套到 sRGB 数值上,模型就失去了原来的意义。

降噪与锐化谁先做#

锐化会增强局部高频变化,而噪声也属于高频。如果先强锐化再降噪,噪点会被扩大成更难抑制的斑点或边缘;多数管线会先降低噪声,再有控制地恢复清晰感。

但降噪过强以后再锐化,并不能找回已经被抹掉的真实纹理,只会增强剩余轮廓,甚至制造 halo。两者应联合调参:降噪保留足够结构,锐化只补偿成像与滤波造成的合理模糊。

怎样评价降噪效果#

如果有干净参考图,可以计算均方误差:

MSE=1Ni(x^ixi)2MSE=\frac{1}{N}\sum_i(\hat{x}_i-x_i)^2

以及常见的 PSNR:

PSNR=10log10MAX2MSEPSNR=10\log_{10}\frac{MAX^2}{MSE}

但指标必须结合测试数据解释:

  • 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 再问“谁离我更近”,双边滤波继续问“谁和我更像”。每多一个假设,都可能更好地保护某类结构,也可能在假设不成立时制造新的伪影。

一套可信的降噪实现,应当说清楚噪声模型、数据域、邻域、权重、边界和评价方法。只有这些条件透明,“更干净”才不会悄悄变成“信息更少”。

降噪为什么会抹掉细节?从高斯滤波走到双边滤波
https://cloudsir.top/posts/how-image-denoising-preserves-edges/
作者
CloudSir
发布于
2026-08-24
许可协议
CC BY-NC-SA 4.0
画面四角为什么更暗?从增益网格理解 Lens Shading Correction
使用Gitee + Webhook自动化部署网站