059、多帧降噪(MFR)实战:连拍合成、对齐算法与噪声模型优化 059、多帧降噪MFR实战连拍合成、对齐算法与噪声模型优化去年夏天某款旗舰机在暗光场景下翻车了——用户拍夜景人像背景噪点像撒了盐人脸边缘还有鬼影。我拿到log一看多帧降噪模块的合成权重配比有问题运动检测阈值设得太死导致有效帧数被砍掉一半。更坑的是sensor的读出噪声在长曝光帧里被放大对齐算法直接跑偏。这种问题调试过MFR的人应该都懂——不是算法不行是噪声模型没吃透对齐策略没跟硬件特性匹配。连拍合成不是简单平均就完事多帧降噪的核心思路是用时间换信噪比。连拍N帧每帧的噪声是独立同分布的理论上合成后信噪比提升sqrt(N)倍。但实际调试中这个理想值从来达不到——原因在于帧间不是完全独立的读出噪声、固定模式噪声FPN会重复出现。我常用的合成策略是加权平均权重根据每帧的噪声方差动态分配。低噪声帧给高权重高噪声帧比如长曝光帧的热像素给低权重。代码实现时权重计算要避开一个坑别直接用像素值方差得用局部块方差。否则纹理区域会被误判为噪声导致细节被抹掉。// 这里踩过坑直接用全局方差会误伤纹理// 正确做法分块计算局部方差再取中位数作为噪声估计floatcalc_frame_weight(uint16_t*frame,intwidth,intheight){floatblock_var[64];// 分8x8块intblock_idx0;for(inty0;yheight;y8){for(intx0;xwidth;x8){floatmean0,var0;for(intj0;j8;j){for(inti0;i8;i){meanframe[(yj)*width(xi)];}}mean/64;for(intj0;j8;j){for(inti0;i8;i){floatdiffframe[(yj)*width(xi)]-mean;vardiff*diff;}}var/64;block_var[block_idx]var;}}// 别这样写直接取平均方差会被纹理块拉高// 正确取中位数剔除纹理块的影响qsort(block_var,block_idx,sizeof(float),cmp_float);floatnoise_varblock_var[block_idx/2];return1.0f/(noise_var1e-6f);}合成时还有个细节参考帧的选择。别总是用第一帧得用噪声最小的那帧。我习惯先做一轮快速噪声估计挑出信噪比最高的帧作为参考其他帧对齐到它。这样能减少对齐误差的累积。对齐算法运动补偿的生死线多帧降噪最怕运动。人稍微动一下对齐不准合成出来就是鬼影。常用的对齐算法有光流法和块匹配法。手机平台上块匹配更实用——计算量小硬件加速友好。块匹配的核心是搜索策略。全搜索太慢三步搜索TSS是经典方案。但调试时我发现TSS在低纹理区域容易陷入局部最优。改进方法是加一个梯度引导先计算每个块的梯度幅值梯度大的块用更精细的搜索步长梯度小的块直接跳过。// 别这样写固定搜索范围低纹理区域浪费算力// 正确根据梯度动态调整搜索范围intblock_match(uint16_t*ref,uint16_t*cur,intbx,intby,intblock_size){intgradcalc_gradient(ref,bx,by,block_size);intsearch_range(gradTH_HIGH)?16:(gradTH_LOW)?8:4;intbest_mv_x0,best_mv_y0;floatbest_sadFLT_MAX;for(intdy-search_range;dysearch_range;dy){for(intdx-search_range;dxsearch_range;dx){floatsadcalc_sad(ref,cur,bx,by,dx,dy,block_size);if(sadbest_sad){best_sadsad;best_mv_xdx;best_mv_ydy;}}}// 这里踩过坑SAD阈值设得太死运动区域被误判为静态// 正确根据噪声水平动态调整SAD阈值if(best_sadnoise_level*block_size*block_size*2){returnMV_INVALID;// 标记为运动区域不参与合成}return(best_mv_y16)|best_mv_x;}对齐后还有个关键步骤一致性检查。用双向匹配验证运动向量的可靠性。前向匹配和后向匹配的向量差超过阈值说明这块区域不可靠直接丢弃。这个步骤能有效抑制鬼影但会增加计算量。我通常只在人脸区域做双向匹配背景区域用单向就够了。噪声模型优化从sensor特性出发噪声模型是MFR的基石。不同sensor的噪声特性差异很大——索尼的IMX系列读出噪声低但热像素多三星的ISOCELL系列固定模式噪声小但散粒噪声大。调试时不能套用通用模型得针对具体sensor标定。我常用的噪声模型是泊松-高斯混合模型散粒噪声服从泊松分布读出噪声和热噪声服从高斯分布。标定时拍一组暗场和灰阶卡拟合出噪声参数。// 别这样写假设噪声是纯高斯分布忽略散粒噪声// 正确用泊松-高斯模型参数从sensor标定获取typedefstruct{floata;// 散粒噪声系数与信号强度相关floatb;// 读出噪声方差常数floatc;// 热像素阈值与温度相关}noise_model_t;floatestimate_noise_variance(noise_model_t*model,floatsignal,floattemp){floatshot_noisemodel-a*signal;// 散粒噪声与信号成正比floatread_noisemodel-b;floatthermal_noisemodel-c*(temp-25.0f);// 温度补偿returnshot_noiseread_noisethermal_noise;}温度补偿是个容易被忽略的点。手机在暗光下连续拍照sensor温度能升到50度以上热噪声翻倍。我习惯在ISP的metadata里读温度值动态调整噪声模型参数。否则拍第一张和拍第十张的降噪效果会明显不同。实战中的坑与经验连拍帧数的选择不是越多越好。帧数多了运动概率增加对齐失败率上升。我通常用4-6帧再多就分两组做局部合成再二次合成。这样能平衡信噪比提升和运动鲁棒性。曝光策略多帧降噪最好配合短曝光帧。长曝光帧虽然信噪比高但容易过曝而且运动模糊严重。我习惯用1/4参考曝光时间的短帧做降噪再用一帧正常曝光做细节补偿。这样暗部噪点被压制高光细节不丢失。边缘处理对齐后的边缘区域合成权重要降低。否则边缘处的小偏移会被放大产生锯齿。我用的方法是在边缘检测图上做高斯模糊生成权重掩膜边缘区域的权重衰减到0.5。调试工具别只盯着PSNR看。多帧降噪的最终效果人眼感知更重要。我习惯在调试时同时看三张图原始单帧、合成帧、残差图合成帧减参考帧。残差图能直观显示哪些区域被平滑了哪些区域保留了细节。如果残差图在纹理区域有大量能量说明降噪过度了。平台适配不同平台的ISP对多帧合成的支持不同。高通平台有MCTFMotion Compensated Temporal Filter硬件模块联发科平台有MFR专用DSP。调试时尽量用硬件加速别纯软件实现。否则帧率上不去用户体验差。最后说一句多帧降噪不是万能的。极端暗光下单帧信噪比太低对齐算法基本失效。这时候不如用长曝光防抖或者干脆上闪光灯。算法再牛也敌不过物理极限。