012、RAW域预处理黑电平校正、坏点校正与镜头阴影校正的算法与标定实战一个让我半夜被电话叫醒的bug2018年某款旗舰机项目夜拍模式刚调通产线突然报批量黑屏——不是全黑是那种暗部发紫、边缘发绿、中间还有几个死像素点的“五彩斑斓黑”。产线经理在电话里吼“你们Camera团队到底行不行”我盯着log看了半小时最后发现是RAW域预处理三个模块的流水线顺序搞反了坏点校正跑在黑电平校正之前导致暗电流被当成坏点给抹了镜头阴影校正又拿错误的数据去算增益整个链条崩了。从那以后我养成了一个习惯RAW域预处理顺序就是命。黑电平校正BLC→ 坏点校正DPC→ 镜头阴影校正LSC这个顺序谁动谁出事。黑电平校正你以为减个值就完事了很多新手拿到RAW图第一反应是“黑电平不就是减去一个固定值吗”然后对着全黑帧算个均值代码里写一句pixel - black_level就交差了。这种写法在实验室里能跑一到量产就翻车。真实场景下的黑电平有多野温度漂移Sensor温度从-20℃升到60℃暗电流能翻3-5倍。你冬天标定的黑电平夏天用就是错的。增益耦合模拟增益每增加一倍黑电平会跟着往上跳。我见过某颗Sensor在16x增益下黑电平比1x时高了12个DN12-bit RAW。列/行噪声CMOS的读出电路天生有列方向噪声每列的黑电平可能差2-3个DN。你取全图均值去减每列都欠校正。实战中怎么干我们团队的做法是产线标定时对每个增益档位、每个温度点采集全黑帧然后按列计算黑电平。代码里维护一张black_level_table[gain_idx][column]的表运行时根据当前增益和温度插值。// 别这样写pixel - global_black_level;// 这里踩过坑全局减会导致列噪声放大暗部出现竖条纹// 正确的做法按列校正uint16_tblack_level_colblack_level_table[gain_idx][col];pixel(pixelblack_level_col)?(pixel-black_level_col):0;// 注意减完要钳位到0否则负值会卷成65535画面出现白色噪点还有一个容易被忽略的点OBOptical Black区域。很多Sensor在像素阵列边缘有被遮挡的暗像素可以用来实时估算黑电平。但OB区域本身也有噪声直接取均值会引入随机抖动。我们一般用中值滤波后再取均值或者用IIR低通滤波做时间域平滑。坏点校正别把星星当成坏点抹了坏点校正的坑比黑电平还多。最经典的翻车案例夜拍星空模式算法把星星当成坏点给抹了用户投诉“你们手机拍不出银河”。坏点分两类静态坏点出厂就有位置固定产线标定一次就行。动态坏点随温度、增益、曝光时间变化。高温下Sensor的暗电流噪声会超过阈值被误判为坏点。标定环节的实操经验产线标定静态坏点时我们拍三张不同亮度的灰阶卡暗场0.1 lux、中灰18%反射率、亮场90%反射率。对每个像素如果它在三张图里都偏离邻域均值超过阈值才标记为坏点。这样能过滤掉随机噪声导致的误判。// 坏点检测基于邻域统计// 这里踩过坑用3x3窗口太小容易把纹理边缘误判为坏点// 建议用5x5窗口排除中心像素后计算均值和标准差uint16_tneighbors[24];// 5x5窗口去掉中心intcount0;for(intdy-2;dy2;dy){for(intdx-2;dx2;dx){if(dx0dy0)continue;neighbors[count]image[ydy][xdx];}}floatmeancalculate_mean(neighbors,24);floatstdcalculate_std(neighbors,24,mean);// 动态阈值根据增益和ISO调整floatthresholdbase_threshold*(1.0gain*0.1);if(fabs(pixel-mean)threshold*std){// 标记为坏点用中值替换pixelmedian_filter(neighbors,24);}动态坏点怎么防我们加了一个“温度感知”模块当Sensor温度超过50℃时自动放宽坏点检测的阈值避免把热噪声当成坏点。同时对检测到的坏点做时间域一致性检查——如果同一个位置连续5帧都被标记为坏点才真正执行替换。这样能避免单帧随机噪声导致的误判。镜头阴影校正LSC不是简单的乘个系数镜头阴影校正Lens Shading Correction是RAW域预处理里最吃算力的模块也是最容易出“阴阳脸”的环节。LSC的物理本质镜头边缘的光照强度比中心低同时CMOS的像素响应在边缘也有角度依赖性。所以LSC需要同时补偿光照衰减cos^4 law和像素响应角度microlens shading。标定环节的坑产线标定LSC时通常拍一张均匀光源下的灰卡。但均匀光源本身就不均匀——积分球的光均匀性通常只有±2%这2%的误差会直接映射到LSC系数里。我们试过用多张不同角度的灰卡做平均但效果有限。后来换了个思路用多项式拟合代替逐点查表。对每个颜色通道R/Gr/Gb/B用二阶或三阶多项式拟合径向衰减曲线。这样既能平滑掉标定噪声又能减少存储空间。// 别这样写直接查表256x256的表格占用256KB内存// 这里踩过坑查表法在边缘区域有量化误差导致出现同心圆条纹// 推荐做法多项式拟合// 假设中心在 (cx, cy)计算归一化半径 rfloatdx(x-cx)/cx;floatdy(y-cy)/cy;floatr2dx*dxdy*dy;floatr4r2*r2;// 三阶多项式gain 1 a*r2 b*r4floatgain1.0coeff_a*r2coeff_b*r4;// 注意增益不能无限大边缘增益通常不超过4xif(gain4.0f)gain4.0f;pixel(uint16_t)(pixel*gain0.5f);// 这里踩过坑浮点运算后要四舍五入否则会有偏色多通道对齐的玄学LSC最头疼的是R/Gr/Gb/B四个通道的增益曲线不一致。如果Gr和Gb的增益差超过1%画面就会出现“棋盘格”纹理。我们曾经因为LSC系数没对齐被客户投诉“你们手机拍白色墙壁有彩色格子”。解决方案标定时对每个通道独立拟合然后强制Gr和Gb的增益差小于0.5%。具体做法是在拟合完成后对Gr和Gb的系数做加权平均让两者趋近。三个模块的流水线协同回到开头那个bug。正确的流水线顺序是黑电平校正先减掉暗电流让数据归零。坏点校正在干净的数据上检测坏点避免暗电流被当成坏点。镜头阴影校正最后做增益补偿因为坏点校正已经修复了异常像素不会把坏点增益放大。一个容易被忽略的细节黑电平校正后的数据可能有负值如果OB区域噪声导致减过头坏点校正模块必须处理负值。我们会在BLC之后加一个钳位但钳位会丢失信息。更好的做法是BLC输出带符号的16位数据DPC和LSC都支持有符号运算最后再钳位到无符号16位。个人经验性建议标定数据要带温度标签产线标定时同时记录Sensor温度。后期如果发现黑电平漂移可以回溯是温度问题还是增益问题。预留调试接口在固件里留一个寄存器可以动态调整坏点检测阈值。量产后的客诉很多时候靠这个接口救场。别迷信“自适应算法”有些论文鼓吹全自动坏点检测不用标定。我在三个项目上试过全翻车了。产线标定自适应微调才是工程上靠谱的方案。LSC的增益上限要设死边缘增益超过4x时信噪比已经崩了不如直接裁掉边缘像素。有些手机厂商的“超广角”模式其实就是把LSC增益过大的区域裁掉。调试时用伪彩色图把LSC增益映射成伪彩色一眼就能看出哪个通道没对齐。比盯着灰度图数像素高效得多。最后说一句RAW域预处理是ISP pipeline的基石这里出问题后面所有模块去马赛克、降噪、锐化都会跟着错。花80%的时间把这20%的环节做扎实比后面花200%的时间去修bug划算得多。