3. 降噪算法原理:噪声模型与分类

做ISP调试这么多年,我始终觉得一个道理没变过——不懂噪声,就别谈降噪。你连对手长什么样都不知道,怎么打赢它?

这一章,咱们就把噪声的底细摸清楚。从物理源头到数学模型,再到三类主流降噪思路,我会结合项目里踩过的坑,给你讲透。

3.1 噪声从哪来?——三大物理源头

传感器上的噪声,说白了就三个来源。我习惯把它们分成“天生随机型”和“固定死板型”。

噪声类型 物理根源 统计特性 与信号关系
光子噪声 (Shot Noise) 光子到达的量子随机性 泊松分布 √信号
读出噪声 (Read Noise) 模拟电路、ADC量化 高斯分布(近似) 与信号无关
固定模式噪声 (FPN) 像素工艺偏差、暗电流不均匀 空间固定、帧间不变 与信号弱相关

关键认知:光子噪声是“信号越强,噪声越大”;读出噪声是“信号再弱,它都在那”;FPN是“每颗传感器都有自己的指纹”。

我在调试一款48M sensor时遇到过一件事:暗光下画面出现明显的竖条纹,一开始以为是读出噪声,后来用黑帧减法才发现是FPN。嗯,从那以后我养成了一个习惯——拿到新sensor,先拍黑帧,再拍灰阶卡,把噪声底子摸清楚再动手。

3.2 噪声模型怎么建?

实际工程中,我们不会用纯理论模型去算。高通平台给了一套很实用的近似公式:

// 高通平台常用噪声模型(简化版)
// total_noise = sqrt(shot_noise^2 + read_noise^2) + fpn

// 光子噪声:与信号强度平方根成正比
shot_noise = gain * sqrt(signal * quantum_efficiency)

// 读出噪声:与增益成正比,与信号无关
read_noise = gain * read_noise_floor

// FPN:空间固定,可通过校准消除
fpn = dark_signal + pixel_gain_mismatch * signal

你想想看,这个模型告诉我们什么?增益越高,读出噪声和光子噪声都被放大。这就是为什么暗光下噪点特别明显——不是噪声变多了,是信号太弱,信噪比掉下去了。

我的调试习惯:在3A收敛后,我会先抓一帧raw图,用高通工具拆出R/G/B通道,分别计算每个通道的噪声标准差。如果R通道噪声明显高于G/B,十有八九是白平衡增益太大——这时候别急着调降噪,先调AWB。

3.3 空域降噪——Bilateral与NLM

空域降噪,说白了就是“看邻居”。但怎么个看法,差别很大。

3.3.1 Bilateral Filter(双边滤波)

Bilateral的核心思想很简单:既要看空间距离,也要看像素值差异。空间近、值也近的像素,权重就大;空间远或者值差异大的,权重就小。

// Bilateral滤波核心公式(伪代码)
for each pixel p:
    sum_weight = 0
    sum_value = 0
    for each neighbor q in window:
        w_spatial = exp(-||p-q||^2 / (2 * sigma_s^2))
        w_range  = exp(-|I(p)-I(q)|^2 / (2 * sigma_r^2))
        weight = w_spatial * w_range
        sum_weight += weight
        sum_value  += weight * I(q)
    I'(p) = sum_value / sum_weight

这里有两个关键参数:sigma_s(空间域sigma)和sigma_r(值域sigma)。

参数 作用 调大后果 调小后果
sigma_s 控制空间影响范围 模糊更重,细节丢失 降噪不足
sigma_r 控制边缘保护力度 边缘被模糊 降噪效果弱

注意:Bilateral在平坦区域效果好,但在纹理密集区域容易产生“阶梯效应”。我曾经在一个项目里把sigma_r调太大,结果人脸皮肤是光滑了,但头发丝全糊成了一片——被客户打回来重调。

3.3.2 NLM(非局部均值)

NLM的思路比Bilateral更激进——它不只看邻居,而是在全图范围内找相似的patch。你想想看,一张图里有很多相似的纹理块,比如草地、墙面、皮肤,NLM会把它们加权平均。

// NLM核心思路(简化)
for each pixel p:
    // 以p为中心的patch: P
    for each pixel q in search window:
        // 以q为中心的patch: Q
        // 计算P和Q的相似度(欧氏距离)
        d = ||P - Q||^2
        weight = exp(-d / h^2)
        // 加权平均
        I'(p) += weight * I(q)
    I'(p) /= sum(weight)

NLM的降噪效果确实比Bilateral好,但代价是计算量爆炸。高通平台一般只在低分辨率场景或者后处理阶段用NLM,实时预览基本扛不住。

我的建议:如果平台算力有限,优先用Bilateral做空域降噪。NLM可以放在拍照后处理流程里,作为“锦上添花”的一步。

3.4 时域降噪——3DNR与运动补偿

时域降噪的原理很简单:同一位置,多帧平均。但现实很残酷——相机在动,物体也在动。

3.4.1 3DNR(三维降噪)

3DNR把时间轴当作第三维。当前帧和参考帧做加权平均:

// 3DNR简化模型
output_frame = alpha * current_frame + (1 - alpha) * reference_frame

// alpha由运动检测结果决定
// 运动大 -> alpha大(更多信任当前帧)
// 运动小 -> alpha小(更多利用历史帧)

这里有个坑:alpha怎么算?高通平台一般用帧间差异图来估计运动量。差异大的区域,alpha拉高;差异小的区域,alpha降低。

3.4.2 运动补偿(MC)

运动补偿是3DNR的升级版。它先做运动估计(ME),找到物体在帧间的位移矢量,然后做运动补偿(MC),把参考帧对齐到当前帧,再做时域滤波。

核心流程:ME(运动估计)→ MC(运动补偿)→ 时域滤波 → 输出

我记得在调试一个视频防抖+降噪的项目时,ME模块的搜索范围设得太小,导致快速移动的物体出现“鬼影”。后来把搜索范围从16x16扩大到32x32,鬼影消失了,但功耗涨了10%。嗯,这就是工程上的取舍。

3.5 频域降噪——小波与DCT

频域降噪的思路是:信号和噪声在频域里是分开的。噪声通常集中在高频,信号集中在低频(边缘和纹理也在高频,这是个麻烦)。

3.5.1 小波降噪

小波变换能把图像分解成不同尺度和方向的子带。噪声在每个子带里都有分布,但主要集中在高频子带。做法是:

  1. 对图像做小波分解(比如3层)
  2. 对高频子带做阈值处理(软阈值或硬阈值)
  3. 逆小波变换重建图像
// 小波软阈值处理
if |coeff| < threshold:
    coeff = 0
else:
    coeff = sign(coeff) * (|coeff| - threshold)

3.5.2 DCT降噪

DCT在JPEG压缩里用得最多。降噪时,把图像分块(比如8x8),对每个块做DCT,保留低频系数,丢弃或衰减高频系数,再逆DCT。

注意:DCT降噪容易产生块效应。我建议在DCT降噪后加一个去块效应滤波器,或者用重叠块处理(overlap-add)来减轻边界伪影。

3.6 本章知识体系

下面这张图把本章的核心逻辑串起来了。你可以把它当作一张“降噪算法选型地图”。

降噪算法知识体系 降噪算法知识体系 噪声模型与分类 光子噪声(泊松分布) 读出噪声(高斯分布) 固定模式噪声(FPN) 空域降噪 时域降噪 频域降噪 Bilateral NLM 3DNR MC+3DNR 小波 DCT sigma_s/r patch相似度 alpha融合 运动矢量 阈值处理 块效应 实际ISP Pipeline中,空域+时域+频域降噪通常组合使用 例如:先做3DNR(时域),再做Bilateral(空域),最后小波(频域)精修

核心总结:没有一种降噪算法是万能的。空域降噪保边缘但可能丢纹理,时域降噪效果好但怕运动,频域降噪灵活但容易引入伪影。实际调试中,组合使用、分场景调参才是王道。

我个人习惯在调试时先确定噪声类型(光子主导还是读出主导),再选主降噪方案。如果是光子噪声主导(暗光场景),优先上时域降噪;如果是读出噪声主导(中等亮度),空域降噪更合适。FPN嘛,直接在校准阶段干掉它。