023、RAW域处理:BLC、DPC与LSC的算法原理与调优技巧
一、从一次产线“翻车”说起
去年夏天,某款旗舰机型的产线突然爆出问题:同一批次模组,在暗场下拍出的照片,左上角明显偏绿,右下角偏紫。产线工程师查了三天,怀疑是LSC校准文件出了问题,重新烧录了十几次,问题依旧。我赶到现场,第一件事不是看LSC,而是先抓了一帧RAW图,用工具扫了一遍BLC值——好家伙,四个通道的BLC偏差超过8个DN,暗电流补偿根本没对齐。这就是典型的“BLC没调好,LSC背锅”的案例。
影像系统里,RAW域处理是“地基”。BLC、DPC、LSC这三个模块,看似基础,但任何一个调不好,后面ISP的AWB、CCM、Gamma全都会跟着跑偏。今天这篇笔记,就聊聊这三个“老伙计”的原理和那些年踩过的坑。
二、BLC(Black Level Correction):暗电流的“清零”艺术
原理一句话
传感器在无光条件下,像素依然会产生微弱电流,这个“暗电流”导致输出值不为零。BLC就是把这个底数减掉,让纯黑场景的像素值归零。
算法实现
最简单的BLC是全局减一个固定值:
// 别这样写!全局减固定值会出大问题uint16_tblc_value=64;for(inti=0;i<frame_size;i++){raw_data[i]=(raw_data[i]>blc_value)?(raw_data[i]-blc_value):0;}这里踩过坑:不同通道的暗电流不一样,R、Gr、Gb、B四个通道的BLC值可能差3-5个DN。更坑的是,同一通道在不同行、不同列也有差异——这叫“行噪声”和“列噪声”。所以工业级的BLC必须做“通道独立+行列补偿”。
实战中我常用的方案是“OB(Optical Black)区域动态计算”。传感器通常有被遮挡的像素行/列,这些像素的均值就是当前帧的暗电流水平。但要注意:OB区域本身也有噪声,直接取均值会引入随机抖动。我的做法是取中位数,或者做3sigma剔除后再取均值。
// 推荐写法:动态计算OB区域,通道独立uint16_tblc_r=compute_ob_median(ob_region_r);// 这里用中位数,别用均值uint16_tblc_gr=compute_ob_median(ob_region_gr);uint16_tblc_gb=compute_ob_median(ob_region_gb);uint16_tblc_b=compute_ob_median(ob_region_b);// 逐像素处理,注意溢出保护for(inty=0;y<height;y++){for(intx=0;x<width;x++){uint16_tval=raw_data[y*stride+x];uint16_tblc=get_blc_for_channel(x,y);// 根据Bayer pattern选择对应通道的BLCraw_data[y*stride+x]=(val>blc)?(val-blc):0;}}调优技巧
温度补偿:暗电流随温度指数级增长。25°C和60°C下,BLC值可能差20个DN。我习惯在模组出厂时做-10°C到60°C的标定,存一个温度-BLC查找表,运行时根据sensor温度实时插值。
增益联动:模拟增益和数字增益放大暗电流,BLC值要跟着增益线性缩放。这个很多人会忘——增益变大后BLC没跟着调,暗场直接偏色。
产线校准:产线上用暗箱拍100帧,取每帧的OB均值,再做时域平均。别用单帧,单帧的随机噪声会让你校准出来的BLC值忽高忽低。
三、DPC(Defect Pixel Correction):坏点的“美容”手术
坏点分类
- 静态坏点:出厂就有,位置固定。比如死点(永远黑)、亮点(永远白)。
- 动态坏点:随温度、增益变化出现的坏点。温度高了冒出来,温度低了又消失。
- 闪烁坏点:时域上随机闪现,像星星一样一闪一闪。
算法思路
DPC的核心是“检测+替换”。检测方法有很多,我实战中验证过最稳的是“梯度检测法”:
// 这里踩过坑:用固定阈值检测坏点,不同场景下误检率极高// 正确做法:自适应阈值intdetect_defect(uint16_tcenter,uint16_tneighbors[8],floatgain){floatmedian=compute_median(neighbors,8);// 用中位数,别用均值floatdiff=abs((float)center-median);floatthreshold=base_threshold*gain*noise_model(median);// 阈值随增益和亮度变化return(diff>threshold)?1:0;}替换策略也有讲究。简单替换成邻域均值会模糊细节,我习惯用“中值替换+边缘保护”:
// 推荐写法:边缘保护的中值替换uint16_tcorrect_pixel(uint16_tcenter,uint16_tneighbors[8]){// 先判断中心像素是否在边缘上floath_grad=abs(neighbors[3]-neighbors[4]);// 水平梯度floatv_grad=abs(neighbors[1]-neighbors[6]);// 垂直梯度if(h_grad>v_grad){// 垂直边缘,用垂直方向的两个邻居取中值returnmedian_of_two(neighbors[1],neighbors[6]);}else{// 水平边缘,用水平方向的两个邻居取中值returnmedian_of_two(neighbors[3],neighbors[4]);}}调优技巧
坏点表管理:产线标定出来的静态坏点表,要存到模组的OTP里。但注意:坏点表会随着时间增长,建议在ISP驱动里留一个“在线学习”接口,用户使用过程中发现的新坏点可以追加进去。
动态坏点检测的“死区”:在低亮度下,噪声本身就很大,容易把正常像素误判为坏点。我习惯在亮度低于某个阈值时,把DPC的检测阈值拉高,或者直接关闭动态检测,只做静态坏点替换。
闪烁坏点的处理:时域上闪现的坏点,单帧检测很难搞定。我的做法是“3帧确认”:连续三帧都在同一位置检测到坏点,才执行替换。这样能过滤掉大部分随机噪声。
四、LSC(Lens Shading Correction):镜头阴影的“匀光”工程
现象与原理
镜头的光学特性导致边缘亮度低于中心,同时不同波长的光衰减程度不同,造成边缘偏色。LSC就是给边缘像素“补光”,同时做颜色补偿。
校准与插值
产线校准阶段,用均匀光源拍一张灰卡,计算每个像素位置的增益系数。但全像素存储增益表太占空间,通常用“网格+插值”的方式:
// 网格增益表,16x16网格,每个网格存R/Gr/Gb/B四个通道的增益typedefstruct{uint16_tgrid_width;// 网格宽度,单位像素uint16_tgrid_height;// 网格高度,单位像素uint8_tgrid_cols;// 网格列数uint8_tgrid_rows;// 网格行数uint16_t*gain_r;// R通道增益表uint16_t*gain_gr;// Gr通道增益表uint16_t*gain_gb;// Gb通道增益表uint16_t*gain_b;// B通道增益表}lsc_table_t;// 运行时双线性插值floatinterpolate_gain(lsc_table_t*table,floatx,floaty,intchannel){// 计算网格索引intcol=(int)(x/table->grid_width);introw=(int)(y/table->grid_height);floatfrac_x=(x-col*table->grid_width)/table->grid_width;floatfrac_y=(y-row*table->grid_height)/table->grid_height;// 四个网格点的增益floatg00=get_gain(table,col,row,channel);floatg10=get_gain(table,col+1,row,channel);floatg01=get_gain(table,col,row+1,channel);floatg11=get_gain(table,col+1,row+1,channel);// 双线性插值floatg0=g00*(1-frac_x)+g10*frac_x;floatg1=g01*(1-frac_x)+g11*frac_x;returng0*(1-frac_y)+g1*frac_y;}调优技巧
网格密度选择:网格太密,存储开销大;网格太疏,插值误差大。我的经验是:对于手机镜头,16x16网格足够;对于车载广角镜头(畸变大),建议32x32甚至64x64。
增益上限保护:边缘增益可能达到2.0甚至更高,但增益太大会放大噪声。我习惯设置一个最大增益值(比如2.5),超过的部分截断。如果边缘亮度实在补不回来,说明镜头本身的光学设计有问题,LSC不是万能的。
温度与增益的影响:LSC校准通常在25°C、增益1x下进行。但实际使用中,温度和增益变化会导致镜头阴影特性偏移。高端方案会做“多温度点+多增益点”的LSC表,运行时根据当前状态选择最近的表做插值。
产线校准的“白场”要求:产线拍白场时,光源的均匀性要保证在1%以内。曾经有产线用普通灯箱,边缘亮度比中心低了5%,校准出来的LSC表把边缘增益拉得特别高,结果用户拍出来的照片边缘全是噪点。
五、三个模块的联动调优
BLC、DPC、LSC不是独立工作的,它们的处理顺序和参数耦合直接影响最终效果。
处理顺序
正确的流水线顺序是:BLC → DPC → LSC。为什么?BLC先做,把暗电流底数清掉,DPC检测坏点时用的阈值才准确;DPC做完,坏点被替换,LSC计算增益时不会被坏点干扰。
参数耦合
- BLC不准 → DPC误检率升高(暗电流没清干净,暗场下正常像素被误判为坏点)
- BLC不准 → LSC边缘偏色(暗电流在不同通道的差异,被LSC的增益放大)
- DPC没做好 → LSC校准表里混入坏点数据,导致校准出来的增益表有异常值
实战案例
回到开头的产线问题。最终排查发现:产线更换了sensor批次,新批次的暗电流特性变了,但BLC校准文件没更新。BLC偏差导致DPC在暗场下疯狂误检,把正常像素替换成邻域值,这些被替换过的像素又被LSC的增益放大,最终表现为边缘偏色。
解决方案:重新标定新批次sensor的BLC参数,同时把DPC的检测阈值调松一点(因为新批次sensor的暗电流一致性比旧批次差),再重新跑一遍LSC校准。问题解决。
六、个人经验总结
别迷信“通用算法”:BLC、DPC、LSC的参数高度依赖sensor型号、镜头模组、甚至同一型号不同批次的工艺偏差。每换一次sensor或镜头,必须重新标定。
产线校准是“良心活”:很多公司为了省成本,产线校准只做单帧、单温度点。结果就是:用户拿到手机,冬天拍暗场偏色,夏天拍暗场也偏色。多花点时间做多温度点标定,后期能省大量客诉处理成本。
调试工具要趁手:我习惯在调试阶段,把BLC、DPC、LSC的中间结果都dump出来,用Python脚本可视化。比如BLC补偿后的暗场图像、DPC检测到的坏点位置图、LSC的增益曲面图。肉眼一看就知道问题在哪。
留有余量:BLC的减法、LSC的乘法,都会改变数据的动态范围。设计RAW域处理时,要保证数据位宽足够(比如12bit sensor,内部处理用16bit),避免溢出或截断。
最后一条,也是最重要的一条:永远不要相信“默认参数”。每个模组出厂前,必须用暗场和白场做一次全流程验证。BLC、DPC、LSC这三个模块,任何一个参数不对,后面ISP的AWB、CCM、Gamma全都会跟着跑偏,而且你很难定位到根因——因为症状可能出现在色彩、亮度、噪声等各个方面,但病根往往就在RAW域。