
047、空间降噪的保边困境——NLM/Bilateral/Guided Filter各自擅长的噪声类型——从噪声模型到滤波器参数自适应的工程实现上周三晚上十一点车载项目组的老张把一段夜间行车视频怼到我屏幕上。画面里是高速匝道路侧护栏在车灯照射下高光溢出暗部区域全是颗粒感。他指着路沿说“这鬼东西降噪一开边缘全糊了路沿跟被橡皮擦蹭过似的降噪一关ISP那边又过不了主观评测。”我盯着屏幕看了三秒问他“你用的什么滤波器”他说Bilateralsigma空间设了3sigma颜色设了0.1。我叹了口气——这参数组合边缘不糊才怪。这不是老张一个人的问题。空间降噪的保边困境本质上是三个滤波器在跟三种噪声模型做博弈而绝大多数工程师只记住了“Bilateral保边”这个结论却忘了问一句保的是谁的边滤的是哪种噪先别急着调参。咱们把噪声模型掰开揉碎了看。传感器出来的噪声从来不是单一的高斯白噪声。暗光下读出噪声和光子散粒噪声混在一起前者近似高斯后者是泊松分布——但泊松在高计数时趋近高斯低计数时偏态严重拖尾明显。再加上固定模式噪声FPN和行噪声真实场景里的噪声是“高斯泊松条纹”的混合体。你拿一个假设纯高斯的滤波器去处理混合噪声就像拿一把螺丝刀去拧六角螺栓——能拧但拧不紧还容易滑丝。NLMNon-Local Means的底层逻辑是“图像里有大量重复结构”。它搜索的不是邻域而是整幅图或一个大窗口里的相似块。这个设计天然对高斯噪声有效因为高斯噪声是独立同分布的块与块之间的相似度计算不会被噪声的局部相关性干扰。但NLM有个致命弱点它对“非局部”的假设在纹理稀疏区域会失效。比如天空、墙面这种平坦区相似块一大堆平均下来噪声确实压得干净可一旦遇到重复纹理——比如砖墙、百叶窗、树叶——NLM会把纹理误判为噪声疯狂平均结果纹理变成一坨浆糊。更麻烦的是NLM的计算量是O(N×M)M是搜索窗口大小工程上不做降采样或积分图优化根本跑不动实时。Bilateral Filter走的是另一条路。它把空间距离和像素值差异同时纳入权重理论上边缘两侧的像素因为灰度差大权重趋近于零所以边缘能保住。但这里有个工程上天天踩的坑sigma_color设得太大边缘两侧的像素权重降不下来边缘照样糊设得太小平坦区域的噪声又滤不干净出现“阶梯状”伪影——噪声被压成了一个个小平台视觉上比原始噪声更恶心。老张设的0.1在8bit图像里就是25.5的灰度差这个阈值对夜间高对比场景来说太大了护栏边缘的灰度差可能只有30-40直接被当成“相似像素”平均掉了。Bilateral真正擅长的是“低噪声、高对比边缘”的场景比如白天的建筑物轮廓、文档扫描。它对付不了暗光下的高噪声因为噪声幅度一大边缘两侧的灰度差被噪声淹没保边权重形同虚设。Guided Filter的思路更聪明也更容易被误解。它假设滤波输出是引导图像的局部线性变换。这个假设意味着引导图有边缘的地方输出也有边缘引导图平坦的地方输出也平坦。所以Guided Filter的保边能力完全取决于引导图的质量。如果你拿原图自己引导自己那它本质上是一个边缘感知的平滑器保边效果比Bilateral更稳定因为它是基于最小二乘的全局优化不会出现Bilateral那种权重计算不稳定的问题。但Guided Filter有个隐蔽的坑它对“噪声类型”极其敏感。当噪声是高斯时它的局部线性模型拟合得很好当噪声是泊松暗光下时局部方差被噪声拉高正则化参数epsilon需要跟着调大否则边缘会被过度锐化出现“光晕”。更关键的是Guided Filter对条纹噪声行噪声、列噪声完全无能为力——因为条纹噪声在局部区域内是强相关的局部线性模型会把条纹当成“真实结构”保留下来。现在回到老张的问题。夜间高速匝道噪声是典型的“高斯泊松”混合且边缘是低对比度的路沿。用Bilateralsigma_color必须缩小到0.03以下约7.5灰度差才能保住路沿但此时平坦区域的噪声残留会非常明显。用NLM搜索窗口得开到15×15以上才能压住泊松噪声的拖尾但计算量直接爆炸。用Guided Filter引导图得做预处理——比如先用小半径Bilateral粗降噪再用粗降噪结果做引导图——但这样又引入了两级滤波的延迟。真正的解法不是选哪个滤波器而是先做噪声模型估计再让滤波器参数自适应。我在量产项目里常用的流程是第一步用暗帧盖上镜头盖拍的帧估计读出噪声的方差σ_r²第二步用亮帧均匀光照下的灰卡估计光子散粒噪声的增益k这样总噪声方差就是σ_total² σ_r² k × I(x,y)I是像素灰度。有了这个模型每个像素点的噪声水平就是已知的。第三步根据噪声水平动态调整滤波器参数——噪声大的区域Bilateral的sigma_color要减小因为边缘被噪声污染需要更严格的灰度差阈值但sigma_space要增大需要更多像素参与平均噪声小的区域反过来。这个逻辑用Guided Filter实现更优雅把epsilon设成与噪声方差成正比的空间变化量即epsilon(x,y) α × σ_total²(x,y)α是一个可调系数。这样平坦区域epsilon大平滑力度强边缘区域epsilon小保边效果好。代码层面我踩过一个坑用OpenCV的bilateralFilter时sigma_color和sigma_space是全局标量没法做逐像素自适应。后来我改成自己写CUDA核把噪声模型预计算成一张查找表每个像素根据灰度值查表得到sigma_color再在核函数里动态计算权重。这里有个性能陷阱——动态计算exp()函数非常贵我后来用多项式逼近替代了exp精度损失在0.5%以内速度提升了三倍。别用doublefloat就够了GPU上double是灾难。另一个工程细节NLM的积分图优化。标准NLM对每个像素都要算搜索窗口内所有块的相似度复杂度是O(r²×s²)r是块半径s是搜索半径。用积分图可以把相似度计算变成O(1)查表但积分图存的是“块内像素和”和“块内像素平方和”对泊松噪声的偏态分布不友好——平方和会放大高灰度区域的噪声贡献。我的做法是先把图像做方差稳定变换VST比如Anscombe变换把泊松噪声变成近似高斯再做NLM。这个变换在暗光场景下效果显著但很多人不知道。最后说点个人经验。别迷信“保边滤波器”这个标签。Bilateral保的是“强边缘”NLM保的是“重复结构”Guided Filter保的是“引导图里的边缘”。三者没有谁全面优于谁只有谁匹配你的噪声模型。我现在的默认方案是白天场景用Guided Filter引导图用原图epsilon固定暗光场景用“VSTNLM”组合极端低照度0.1lux以下用Bilateral但sigma_color必须按噪声模型动态算。另外永远在降噪前做一次坏点校正——坏点在NLM里会被当成“独特结构”保留下来比噪声还显眼。老张后来按我的流程改了代码把噪声模型估计加进去sigma_color从固定0.1改成逐像素自适应路沿保住了暗部噪声也压下去了。他问我“这招能通用吗”我说不能每个sensor的噪声模型都不一样你得先花两天时间把sensor的噪声特性摸清楚后面调参就是水到渠成的事。这行当没有银弹只有对噪声模型的敬畏。