进阶:静态坏点标定与动态坏点检测算法实现)
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(inty1;yheight-1;y){for(intx1;xwidth-1;x){uint16_tcenterraw[y*stridex];uint16_tneighbors[8];intidx0;for(intdy-1;dy1;dy){for(intdx-1;dx1;dx){if(dy0dx0)continue;neighbors[idx]raw[(ydy)*stride(xdx)];}}// 排序取中位数这里用简单的冒泡排序实际工程用快速选择sort(neighbors,8);uint16_tmedianneighbors[4];// 8个数的中位数是第4和第5的平均这里简化intdiffabs(center-median);// 阈值根据sensor的固定噪声水平FPN动态调整// 别写死成50不同gain下FPN差异很大if(diffthreshold_fpn[gain_index]){// 标记为候选坏点candidate_map[y*widthx]1;}}}第二步多帧一致性验证单帧检测不可靠因为随机噪声可能让正常像素看起来像坏点。我要求至少连续3帧在同一位置被标记才确认是坏点。// 多帧投票机制// 这里踩过坑帧数太少误报多帧数太多漏报多#defineVOTE_FRAMES5#defineVOTE_THRESHOLD3for(inti0;iVOTE_FRAMES;i){// 采集第i帧做局部对比度检测detect_bad_pixels(frame[i],temp_map);// 累加投票for(intp0;ptotal_pixels;p){vote_map[p]temp_map[p];}}// 最终坏点投票数 VOTE_THRESHOLDfor(intp0;ptotal_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_habs(neighbors[3]-neighbors[4]);// 水平intgrad_vabs(neighbors[1]-neighbors[6]);// 垂直intgrad_d1abs(neighbors[0]-neighbors[7]);// 主对角线intgrad_d2abs(neighbors[2]-neighbors[5]);// 副对角线// 取最小梯度方向认为该方向纹理变化最小intmin_gradmin(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_habs(center-pred_h);intdiff_vabs(center-pred_v);intdiff_d1abs(center-pred_d1);intdiff_d2abs(center-pred_d2);// 如果所有方向上的差异都很大说明是坏点// 阈值需要根据gain动态调整gain越高阈值越大intthresholdbase_thresholdgain*gain_factor;if(diff_hthresholddiff_vthresholddiff_d1thresholddiff_d2threshold){return1;// 坏点}return0;}3.3 坏点校正别直接替换发现坏点后怎么修最粗暴的做法直接用周围像素均值替换。但这样会模糊细节。我推荐的做法边缘导向插值。// 边缘导向的坏点校正// 这里踩过坑直接用均值会破坏边缘画面出现“糊斑”uint16_tdpc_correct(uint16_tcenter,uint16_t*neighbors,intis_bad){if(!is_bad)returncenter;// 计算四个方向的梯度intgrad_habs(neighbors[3]-neighbors[4]);intgrad_vabs(neighbors[1]-neighbors[6]);intgrad_d1abs(neighbors[0]-neighbors[7]);intgrad_d2abs(neighbors[2]-neighbors[5]);// 找到梯度最小的方向沿该方向插值intmin_gradgrad_h;uint16_tresult(neighbors[3]neighbors[4])/2;if(grad_vmin_grad){min_gradgrad_v;result(neighbors[1]neighbors[6])/2;}if(grad_d1min_grad){min_gradgrad_d1;result(neighbors[0]neighbors[7])/2;}if(grad_d2min_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吃透了的。