:双边滤波、非局部均值与BM3D在ISP中的落地)
057、空间降噪Spatial Denoise双边滤波、非局部均值与BM3D在ISP中的落地一个让我失眠三天的噪点问题2018年我在调试一款IMX586的48M模组时遇到了一个至今记忆犹新的场景。客户反馈在室内灯光下约50lux人像皮肤区域出现“油画感”——不是噪点而是那种平滑到失去纹理的塑料质感。我盯着示波器看了三天最后发现是双边滤波的sigma_range参数设得太小导致边缘保留过度反而把皮肤毛孔的微纹理当成了“边缘”来保留而平坦区域被过度平滑。这个案例让我明白空间降噪从来不是简单的“去噪”而是在噪声、纹理、边缘三者之间走钢丝。双边滤波ISP中最容易翻车的滤波器双边滤波的核心思想很简单不仅考虑像素的空间距离还考虑像素值的相似度。公式长这样BF[I]_p (1/W_p) * Σ G_s(||p-q||) * G_r(|I_p - I_q|) * I_q但落地时坑特别多。第一个坑sigma_spatial和sigma_range的耦合关系很多刚入行的工程师喜欢把sigma_spatial设成固定值比如5。但实际调试中sigma_spatial必须根据图像分辨率动态调整。4K分辨率下5个像素的“空间邻域”和1080P下完全不是一个概念。我一般用这个经验公式// 这里踩过坑直接写死sigma_spatial5换分辨率就崩floatsigma_spatial0.02f*min(width,height);// 按图像短边的2%计算第二个坑边界处理双边滤波在图像边界处如果不做特殊处理会出现明显的暗边。别用简单的“复制边界像素”策略——那会让边界处的噪声特性突变。我习惯的做法是// 别这样写直接复制边界像素// pixel src[clamp(x, 0, width-1)][clamp(y, 0, height-1)];// 正确做法缩小核大小或者用镜像反射intactual_radiusmin(radius,min(x,width-1-x));actual_radiusmin(actual_radius,min(y,height-1-y));第三个坑计算量双边滤波是O(radius²)的复杂度在ISP的实时pipeline里根本跑不动。我见过有人硬上5x5的双边滤波结果帧率直接腰斩。实际落地必须用分离式双边滤波或者查表法加速。非局部均值NLM用“找相似”的思路去噪NLM的思路很巧妙图像中每个像素都能在图像的其他位置找到相似的结构。比如一张白墙照片左上角和右下角的纹理应该是相似的。NLM[I]_p Σ w(p,q) * I_q w(p,q) exp(-||N_p - N_q||² / h²)这里的N_p和N_q是像素周围的patch通常7x7或5x5。调试NLM时最关键的参数是h滤波强度h设小了去噪效果不明显h设大了会出现“鬼影”——不同位置的相似patch被强行平均导致纹理错位。我踩过最深的坑是在夜景模式下h设到308bit图像结果路灯的灯丝纹理被平均成了模糊一团。经验值对于8bit图像h在10-20之间对于10bit raw域h要放大到40-80。NLM在ISP中的落地难点搜索窗口大小理论上搜索整个图像效果最好但计算量爆炸。实际只能限制在21x21或31x31的搜索窗口内。patch大小patch太小区分度不够patch太大计算量飙升。我一般用7x7这是性价比最高的选择。权重计算指数运算在硬件里很贵。可以用查表法或者用分段线性近似。// 这里踩过坑直接调用expf()在ARM上慢到怀疑人生// float weight expf(-dist / (h * h));// 改用查表法staticfloatexp_lut[256];// 预计算intidx(int)(dist*255.0f/(h*h));idxclamp(idx,0,255);floatweightexp_lut[idx];BM3D效果最好但也是最难落地的BM3D被称为“去噪界的瑞士军刀”它把空间域和变换域结合了起来。流程分两步第一步基础估计对每个参考块在图像中找相似块块匹配将这些相似块堆叠成3D数组对3D数组做3D变换2D DCT 1D Haar硬阈值滤波逆变换加权聚合第二步最终估计用第一步的结果作为引导重新做块匹配用维纳滤波代替硬阈值再次聚合BM3D在ISP中的血泪史我曾经试图在手机ISP里跑BM3D结果发现一个参考块需要搜索20-30个相似块每个块还要做3D变换。单帧处理时间超过500ms完全不可行。后来妥协的方案是只在Y通道做BM3DUV通道用简单的NLM块匹配的搜索范围限制在9x9相似块数量从16个降到8个用固定阈值代替自适应阈值即使这样也只能在高端SoC的NPU上跑而且只能用于夜景模式这种低帧率场景。一个实用的BM3D调试技巧BM3D的阈值参数sigma噪声标准差非常敏感。我见过有人直接用固定值结果在不同ISO下效果天差地别。正确的做法是// 别这样写sigma写死// float sigma 25.0f;// 正确做法根据ISO动态调整floatsigmabase_sigma*(current_iso/100.0f);// 还要考虑增益的影响sigma*(analog_gaindigital_gain)/2.0f;三种算法在ISP pipeline中的位置选择这是很多架构师容易搞错的地方。双边滤波适合放在demosaic之后、色彩校正之前。因为此时图像还是raw域或线性RGB噪声分布相对均匀。放在gamma之后做双边滤波噪声特性已经被非线性拉伸破坏效果大打折扣。NLM适合放在gamma校正之后、锐化之前。因为NLM对纹理的保护能力较强在gamma域人眼感知均匀做视觉效果更好。但要注意NLM在暗部区域容易产生色斑需要在UV通道额外做一次色度降噪。BM3D如果要用只能放在raw域或者线性RGB域。BM3D的块匹配依赖图像的自相似性gamma校正会破坏这种自相似性。我见过有人把BM3D放在gamma之后结果块匹配全部失败去噪效果还不如简单的均值滤波。实际调试中的参数联动空间降噪从来不是独立工作的它和时域降噪、锐化、HDR都有耦合。和时域降噪的配合空间降噪强度应该和运动检测结果联动。运动区域空间降噪加强静止区域时域降噪为主。我习惯用这个策略// 根据运动mask调整空间降噪强度floatspatial_strengthbase_strength*(1.0f-motion_mask);// 运动mask范围0-10表示静止1表示剧烈运动和锐化的配合降噪后的图像必须做锐化补偿否则画面会“软”。但锐化强度不能太大否则会把残留噪声放大。我一般把锐化强度控制在降噪前噪声水平的30%左右。和HDR的配合多帧HDR合成后的噪声分布非常不均匀——暗帧的噪声被放大亮帧的噪声被压缩。这时候不能用全局统一的降噪参数必须做逐像素的噪声估计。个人经验总结不要追求理论最优BM3D效果确实好但90%的手机ISP根本跑不动。NLM是性价比最高的选择双边滤波适合做边缘保护。噪声估计是一切的基础没有准确的噪声模型再好的降噪算法也是瞎蒙。我习惯在ISP的暗电流区域光学黑电平实时采样噪声而不是用固定值。人眼才是最终裁判PSNR和SSIM只能参考最终效果要以人眼主观评价为准。我见过PSNR提高了3dB但人眼看过去全是“油画感”的案例。降噪是减法不是加法宁可保留少量噪声也不要过度平滑。因为噪声可以后续用时域降噪处理但丢失的纹理永远回不来了。参数要可调但不能全可调给调试人员留3-5个核心参数就够了太多参数只会让调试变成玄学。我一般只暴露降噪强度、边缘保护强度、纹理保护强度。最后说一句空间降噪没有银弹。每个场景、每个sensor、每个ISP都有自己的脾气调试的过程就是和这些脾气打交道的过程。别指望一套参数打天下也别迷信某个算法的“理论优势”——在工程落地面前稳定性和实时性永远比理论峰值更重要。