078、坏点校正(DPC)进阶:静态坏点标定与动态坏点检测算法实现
一、一个让我熬夜三天的坏点问题
2019年某款旗舰机项目,量产前两周,产线突然反馈:暗光下预览画面出现大量“星星点”——不是噪点,是那种固定位置、亮度异常的白点。产线良率从92%直接掉到67%。
我连夜调取log,发现ISP的DPC模块根本没生效。查了半天,问题出在静态坏点标定环节:产线标定环境用的是DNP光源,色温5500K,但标定算法里有一个阈值写死了——对不同通道的响应差异没做归一化处理。结果就是,R通道的坏点被漏标了30%,而B通道的正常像素被误标成了坏点。
这个教训让我意识到:坏点校正不是简单的“发现一个点就替换掉”,而是一套从标定到检测、从静态到动态的完整工程体系。
二、静态坏点标定:别让产线坑了你
静态坏点标定,说白了就是出厂前把sensor上“天生残疾”的像素找出来,存到OTP里。但这里有个坑:sensor的坏点分布会随温度、增益变化而“漂移”。
2.1 标定环境搭建
别信sensor厂商给的坏点表——那是晶圆级测试结果,封装后应力变化会导致新的坏点出现。我习惯的做法是:
暗场标定:盖上镜头盖,拍100帧暗场,取均值。这一步是为了找“死像素”(输出始终为0或饱和)。阈值设定:像素值低于暗电流均值+3σ的,标记为暗坏点;高于饱和值-3σ的,标记为亮坏点。
亮场标定:用均匀光源(积分球最佳,没有就用DNP灯箱),拍灰阶卡。这一步找的是“响应异常像素”——比如某个像素的响应比周围邻居高了20%以上。
这里踩过坑:亮场标定不能只用一张灰阶图。不同亮度下,坏点的表现不一样。有些像素在低亮度下正常,高亮度下就“发疯”。所以我一般拍5个灰阶:10%、25%、50%、75%、90%反射率。
2.2 标定算法实现
别这样写:遍历所有像素,算平均值,设阈值。这样会把边缘像素和正常像素一起误判。
我的做法是分两步:
第一步:局部对比度检测
// 以3x3窗口为例,计算中心像素与周围8邻域的差异// 注意:这里用中位数而不是均值,避免被坏点本身污染for(inty=1;y<height-1;y++){for(intx=1;x<width-1;x++){uint16_tcenter=raw[y*stride+x];uint16_tneighbors[8];intidx=0;for(intdy=-1;dy<=1;dy++){for(intdx=-1;dx<=1;dx++){if(dy==0&&dx==0)continue;neighbors[idx++]=raw[(y+dy)*stride+(x+dx)];}}// 排序取中位数,这里用简单的冒泡排序,实际工程用快速选择sort(neighbors,8);uint16_tmedian=neighbors[4];// 8个数的中位数是第4和第5的平均,这里简化intdiff=abs(center-median);// 阈值:根据sensor的固定噪声水平(FPN)动态调整// 别写死成50,不同gain下FPN差异很大if(diff>threshold_fpn[gain_index]){// 标记为候选坏点candidate_map[y*width+x]=1;}}}第二步:多帧一致性验证
单帧检测不可靠,因为随机噪声可能让正常像素看起来像坏点。我要求至少连续3帧在同一位置被标记,才确认是坏点。
// 多帧投票机制// 这里踩过坑:帧数太少误报多,帧数太多漏报多#defineVOTE_FRAMES5#defineVOTE_THRESHOLD3for(inti=0;i<VOTE_FRAMES;i++){// 采集第i帧,做局部对比度检测detect_bad_pixels(frame[i],temp_map);// 累加投票for(intp=0;p<total_pixels;p++){vote_map[p]+=temp_map[p];}}// 最终坏点:投票数 >= VOTE_THRESHOLDfor(intp=0;p<total_pixels;p++){if(vote_map[p]>=VOTE_THRESHOLD){bad_pixel_list[bad_count++]=p;}}三、动态坏点检测:别让ISP睡大觉
静态坏点标定只能处理出厂时就存在的缺陷。但sensor用久了,或者遇到高温、高增益场景,新的坏点会冒出来。这就是动态坏点检测(Dynamic DPC)的用武之地。
3.1 实时检测的难点
动态检测必须在ISP pipeline里实时跑,不能拖慢帧率。所以算法必须轻量。
我见过最蠢的做法:在每一帧上跑一遍完整的静态标定流程。帧率直接掉到10fps,产品经理差点没把我吃了。
3.2 轻量级动态检测算法
核心思路:利用像素间的空间相关性,结合梯度信息做快速判断。
// 实时DPC检测,在Bayer域处理// 注意:这里只处理当前像素,不依赖整帧统计uint16_tdpc_detect(uint16_tcenter,uint16_t*neighbors,intgain){// 计算四个方向的梯度intgrad_h=abs(neighbors[3]-neighbors[4]);// 水平intgrad_v=abs(neighbors[1]-neighbors[6]);// 垂直intgrad_d1=abs(neighbors[0]-neighbors[7]);// 主对角线intgrad_d2=abs(neighbors[2]-neighbors[5]);// 副对角线// 取最小梯度方向,认为该方向纹理变化最小intmin_grad=min(min(grad_h,grad_v),min(grad_d1,grad_d2));// 如果中心像素与最小梯度方向上的两个邻居差异都很大// 别这样写:if (abs(center - neighbor1) > threshold && abs(center - neighbor2) > threshold)// 这样会把边缘纹理误判为坏点// 正确做法:比较中心像素与四个方向上的预测值uint16_tpred_h=(neighbors[3]+neighbors[4])/2;uint16_tpred_v=(neighbors[1]+neighbors[6])/2;uint16_tpred_d1=(neighbors[0]+neighbors[7])/2;uint16_tpred_d2=(neighbors[2]+neighbors[5])/2;intdiff_h=abs(center-pred_h);intdiff_v=abs(center-pred_v);intdiff_d1=abs(center-pred_d1);intdiff_d2=abs(center-pred_d2);// 如果所有方向上的差异都很大,说明是坏点// 阈值需要根据gain动态调整:gain越高,阈值越大intthreshold=base_threshold+gain*gain_factor;if(diff_h>threshold&&diff_v>threshold&&diff_d1>threshold&&diff_d2>threshold){return1;// 坏点}return0;}3.3 坏点校正:别直接替换
发现坏点后怎么修?最粗暴的做法:直接用周围像素均值替换。但这样会模糊细节。
我推荐的做法:边缘导向插值。
// 边缘导向的坏点校正// 这里踩过坑:直接用均值会破坏边缘,画面出现“糊斑”uint16_tdpc_correct(uint16_tcenter,uint16_t*neighbors,intis_bad){if(!is_bad)returncenter;// 计算四个方向的梯度intgrad_h=abs(neighbors[3]-neighbors[4]);intgrad_v=abs(neighbors[1]-neighbors[6]);intgrad_d1=abs(neighbors[0]-neighbors[7]);intgrad_d2=abs(neighbors[2]-neighbors[5]);// 找到梯度最小的方向,沿该方向插值intmin_grad=grad_h;uint16_tresult=(neighbors[3]+neighbors[4])/2;if(grad_v<min_grad){min_grad=grad_v;result=(neighbors[1]+neighbors[6])/2;}if(grad_d1<min_grad){min_grad=grad_d1;result=(neighbors[0]+neighbors[7])/2;}if(grad_d2<min_grad){result=(neighbors[2]+neighbors[5])/2;}returnresult;}四、工程落地的几个血泪教训
4.1 坏点表管理
静态坏点表不能只存坐标,还要存“坏点类型”:是暗坏点还是亮坏点?是固定坏点还是温度敏感型?我见过一个项目,把温度敏感型坏点当固定坏点处理,结果低温下正常像素被误校正,画面出现“网格纹”。
我的做法:坏点表结构体包含以下字段:
- 坐标 (x, y)
- 坏点类型 (0:暗坏点, 1:亮坏点, 2:温度敏感型)
- 触发条件 (温度阈值、增益阈值)
- 校正权重 (有些坏点只需要部分校正,不需要完全替换)
4.2 与3A系统的交互
动态DPC检测到坏点后,要不要通知AE/AWB?答案是:要。
如果坏点恰好落在AE统计窗口内,会导致AE误判场景亮度。我遇到过:一个亮坏点正好在画面中心,AE以为场景很亮,疯狂降曝光,结果画面整体偏暗。
解决方案:在AE/AWB统计时,排除坏点列表中的像素。或者更优雅的做法:在统计前先做DPC校正,让3A看到的是“干净”的数据。
4.3 性能优化
动态DPC是ISP pipeline里最耗资源的模块之一。我做过一个优化:只在亮度通道做DPC检测,色度通道直接复用检测结果。因为人眼对亮度变化更敏感,色度通道的坏点不太容易被察觉。
另一个优化:分区域检测。画面边缘的坏点对视觉影响小,可以降低检测频率;画面中心区域重点检测。
五、个人经验总结
做了十几年Camera调试,坏点校正是我觉得“看起来简单,做起来坑最多”的模块之一。
第一,别迷信sensor厂商的标定数据。他们给的坏点表是在理想环境下测的,量产时封装应力、焊接温度都会引入新坏点。我习惯在模组组装完成后,再做一次整机级别的坏点标定。
第二,动态检测的阈值不是调出来的,是算出来的。很多工程师靠肉眼调阈值,今天觉得这个场景OK,明天换个场景就炸了。正确的做法:采集不同温度、不同增益下的噪声数据,建立阈值模型。我一般用3σ原则,但σ要实时计算——用sensor的暗电流统计值。
第三,坏点校正不是越强越好。过度校正会损失细节,画面看起来“假”。我见过一个产品,DPC开得太猛,星空照片里的星星都被当成坏点修掉了。用户投诉说“你们手机拍不出星星”。
第四,留一个“后悔药”。在ISP寄存器里留一个DPC bypass位,调试阶段可以随时开关对比效果。量产固件里也要保留这个功能,万一线上出问题,可以远程关闭DPC应急。
最后说一句:坏点校正做得好不好,不是看实验室的测试数据,而是看用户在实际场景中的体验。那些在暗光下、高温下、长时间使用后依然能保持画面干净的产品,才是真正把DPC吃透了的。