060、VST降噪技术方差稳定变换在低照度场景下的噪声抑制策略一个让我头疼了三个月的低照度问题去年做某款旗舰机型的夜景模式遇到了一个典型的“算法救不了硬件”的坑。Sensor端明明用了大像素1.4μmISP端也堆了多帧合成但暗光下拍出来的天空总有一种“水波纹”般的纹理不是传统的高频噪点而是那种低频、有规律的明暗条纹。团队里有人怀疑是sensor读出噪声有人说是ISP的demosaic算法在低信噪比下失效还有人觉得是LSC校正后的残留噪声。我花了整整一周把raw图dump出来逐帧分析。发现了一个关键现象在低照度区域比如ISO 3200以上噪声的方差不再服从泊松分布而是出现了明显的“方差随信号强度非线性变化”的特征。简单说暗区的噪声方差比亮区大得多但又不是简单的乘性噪声——它是信号依赖的而且这种依赖关系在ISP的线性化处理后被扭曲了。这个问题的本质就是噪声的“非平稳性”。传统的高斯滤波、双边滤波甚至BM3D在这种场景下都会失效因为它们假设噪声是独立同分布的。而VSTVariance Stabilizing Transform降噪技术恰恰就是用来解决这个问题的——先把噪声“掰直”再降噪最后“掰回去”。噪声模型为什么你的降噪算法在暗光下“翻车”先理清一个基本概念。在理想情况下sensor的噪声模型可以近似为观测值 真实信号 泊松噪声光子散粒噪声 高斯噪声读出噪声泊松噪声的方差等于信号均值高斯噪声的方差是常数。所以总方差是Var(观测值) α * 真实信号 β其中α是泊松噪声的增益跟sensor的量子效率、转换增益有关β是读出噪声的方差。问题在于这个方差是信号依赖的。当你对图像做线性变换比如白平衡增益、颜色校正矩阵时噪声的统计特性会被进一步扭曲。举个例子R通道的增益如果是G通道的2倍那么R通道的噪声方差就会被放大4倍因为方差是平方关系。这就是为什么很多手机在低照度下拍红色物体时噪点特别明显——不是sensor不好是ISP的增益放大了噪声的非平稳性。VST的核心思想很简单找一个数学变换T使得变换后的数据T(观测值)的方差近似为常数不再依赖信号强度。这样你就可以用标准的、假设噪声为高斯白噪声的降噪算法比如小波阈值、维纳滤波来处理最后再逆变换回去。实战用Anscombe变换处理raw图噪声最经典的VST是Anscombe变换专门针对泊松噪声。公式是T(x) 2 * sqrt(x 3/8)这个变换能把泊松噪声近似转化为方差为1的高斯噪声。但实际sensor噪声是泊松高斯的混合所以需要更通用的“广义Anscombe变换”。我在调试时踩过一个坑直接对sensor输出的raw数据做Anscombe变换结果发现暗区的噪声反而被放大了。后来才意识到sensor的raw数据已经经过了模拟增益ISO增益噪声模型不再是纯泊松。正确的做法是先把raw数据除以模拟增益恢复到“电子数”域单位是e-在这个域做Anscombe变换降噪处理逆变换后再乘以模拟增益代码实现时我习惯这样写C风格伪码实际项目里用的是Halide// 注意这里假设你已经从sensor驱动读到了模拟增益值// 别像我一样傻乎乎地直接用raw值做变换踩过坑的都知道floatanalog_gainget_analog_gain_from_sensor();// 比如ISO 800对应gain8// 第一步反增益回到电子数域// 这里有个细节sensor的black level要提前减掉否则变换会出错float*raw_e(float*)malloc(width*height*sizeof(float));for(inti0;iwidth*height;i){raw_e[i](raw[i]-black_level)/analog_gain;// 防止负值clamp到0以上if(raw_e[i]0)raw_e[i]0;}// 第二步Anscombe变换// 注意这里用2*sqrt(x3/8)而不是2*sqrt(x)3/8是为了修正小信号时的偏差float*transformed(float*)malloc(width*height*sizeof(float));for(inti0;iwidth*height;i){transformed[i]2.0f*sqrtf(raw_e[i]3.0f/8.0f);}// 第三步降噪处理这里用简单的小波软阈值做演示// 实际项目中我用的是BM3D的变体但要注意BM3D本身也假设噪声是高斯白噪声// 所以VST之后用BM3D效果很好但别直接对raw图用BM3D那是自欺欺人wavelet_denoise(transformed,width,height,sigma);// sigma1.0// 第四步逆变换// 逆变换公式x (y/2)^2 - 3/8// 但要注意逆变换后会有偏差需要做无偏校正for(inti0;iwidth*height;i){floatytransformed[i];floatx(y/2.0f)*(y/2.0f)-3.0f/8.0f;// 无偏校正对于泊松噪声逆变换的期望值不等于原始信号// 这里用查表法或者近似公式我一般用查表因为实时性要求高raw_e[i]anscombe_inverse_unbiased(x);}// 第五步重新乘以增益加上black levelfor(inti0;iwidth*height;i){raw[i](uint16_t)(raw_e[i]*analog_gainblack_level0.5f);}一个容易被忽视的细节多通道的VST在ISP pipeline里raw数据是Bayer格式的每个像素只对应一个颜色通道。但VST是针对单通道设计的。如果你对Bayer数据直接做VST会引入“颜色通道间的噪声耦合”。比如R通道的噪声方差被变换后跟G通道的噪声方差不再独立后续的demosaic算法会放大这种相关性产生彩色条纹。我的做法是先做Bayer到RGB的分离对每个通道独立做VST降噪再逆变换最后合并回Bayer。但这样会引入额外的内存和延迟。在手机平台上我通常只在暗光场景ISO 1600才启用这个流程而且只对R和B通道做因为G通道的量子效率高噪声相对小。与ISP pipeline的集成一个真实案例去年那个项目最终方案是这样的在ISP的“线性化”阶段之后、“demosaic”之前插入一个VST模块。具体位置是sensor raw - 黑电平校正 - 线性化去gamma - VST降噪 - demosaic - 白平衡 - 颜色校正 - gamma - ...为什么放在线性化之后因为sensor的raw数据通常是非线性的比如有sensor自带的gamma校正如果先做VST噪声模型会被非线性扭曲。所以必须先做线性化把数据恢复到“光子数”线性域。这个位置还有一个好处VST降噪后的数据噪声是近似高斯白噪声的后续的demosaic算法比如基于梯度的插值对高斯噪声的鲁棒性更好。实测下来PSNR提升了2-3dB主观上天空的“水波纹”完全消失了。性能优化别让VST成为瓶颈VST的计算量其实不大主要是sqrt运算。但在手机平台上每帧处理时间不能超过10ms1080p分辨率。我做了几个优化查表法对于12bit raw数据信号范围0-4095可以预先计算好变换和逆变换的查找表。这样每次只需要一次查表操作比sqrt快一个数量级。定点化把浮点运算转成定点用Q8.8格式精度够用速度翻倍。SIMD优化用NEON指令集做向量化一次处理4个像素的变换和逆变换。查表法的代码大概长这样// 预计算变换表只算一次// 注意这里假设模拟增益是固定的如果增益变化需要重新计算// 别问我为什么强调这个因为我在变增益场景下忘记更新表debug了两天staticuint16_tvst_lut[4096];// 12bit rawvoidinit_vst_lut(floatanalog_gain,floatblack_level){for(inti0;i4096;i){floate(i-black_level)/analog_gain;if(e0)e0;floatt2.0f*sqrtf(e3.0f/8.0f);// 量化到Q8.8格式方便后续定点运算vst_lut[i](uint16_t)(t*256.0f0.5f);}}// 变换时直接查表uint16_tvst_transform(uint16_traw_pixel){returnvst_lut[raw_pixel];// 一行代码搞定比sqrt快10倍}经验性建议VST不是万能的。它最适合的场景是“噪声方差随信号强度变化明显”的情况比如暗光下的天空、墙壁等平坦区域。对于纹理丰富的区域比如草地、头发VST反而可能因为过度平滑而丢失细节。我的做法是用边缘检测做mask只在平坦区域启用VST降噪纹理区域用传统的双边滤波。另外VST对sensor的噪声模型假设比较敏感。不同sensor的泊松噪声系数α和高斯噪声系数β不同需要标定。我一般用“均值-方差曲线”来标定拍一系列均匀灰卡从暗到亮计算每个灰阶的均值和方差然后拟合出α和β。这个标定过程建议在sensor的整个增益范围内都做一遍因为α和β会随增益变化尤其是高增益下读出噪声β会增大。最后别迷信VST。它只是一个“预处理”步骤真正的降噪效果取决于后续的算法。我见过有人把VST当成“银弹”结果降噪后图像一片模糊。记住VST只是把噪声“掰直”了你还需要一个靠谱的降噪器来“削平”它。