044、坏点校正的漏检与误杀——静态表+动态检测的互补策略——从单像素坏点到cluster坏点的检测算法设计与极限case处理
044、坏点校正的漏检与误杀——静态表动态检测的互补策略——从单像素坏点到cluster坏点的检测算法设计与极限case处理昨晚在实验室盯着一块Sensor的RAW图越看越觉得不对劲。暗场下明明做了坏点校正画面里却还是星星点点地闪着白点不是那种均匀的噪点是那种固定位置、固定亮度的“钉子户”。更头疼的是同一颗Sensor在另一台样机上表现正常换到这台就冒出来十几个新坏点。客户那边的反馈更直接你们这个坏点校正是不是摆设这种问题我遇到过太多次了。坏点校正DPCDefect Pixel Correction在ISP pipeline里是个不起眼的模块但恰恰是它最容易在量产阶段翻车。翻车的模式就两种漏检——该修的没修画面里残留固定亮点误杀——不该修的修了把正常的像素点给抹掉了画面出现“死灰”或者细节丢失。而这两者往往是矛盾的你为了减少漏检把检测阈值调低误杀率立刻飙升反过来阈值调高漏检又压不住。先说漏检。静态坏点表Static Defect Table是出厂前烧录的基于Sensor厂商提供的测试数据。但问题在于Sensor的坏点不是一成不变的。温度漂移、电压波动、长时间使用后的老化都会产生新的坏点。尤其是暗电流随温度升高而增大某些像素在常温下正常到了高温环境就变成“热像素”亮度值明显高于周围。这种动态坏点静态表根本管不到。再说误杀。动态检测算法通常基于邻域比较比如当前像素与周围8邻域的差值超过阈值就判为坏点。但遇到真实图像中的高对比度边缘、细纹理、或者高光区域这种检测很容易误判。比如一根白色细线穿过画面线上的像素点与两侧暗区的差值极大算法一看哎哟全是坏点全给抹了。结果就是画面里的细线变成了虚线细节全丢。我见过最极端的case是某款车载摄像头在夜间行车时路灯的高光区域被动态检测误杀导致画面出现一圈一圈的“黑晕”客户直接投诉说你们这个摄像头是不是坏了。后来排查发现问题出在检测算法只用了单帧的邻域差值没有考虑时间域的一致性。真正的坏点是固定的每帧都在同一位置异常而高光边缘是随场景移动的不会固定在某个像素上。所以正确的思路是静态表动态检测互补。静态表负责处理出厂已知的坏点动态检测负责捕捉运行过程中新出现的坏点。但动态检测不能只看单帧必须结合多帧时间域信息。这里有个关键设计动态坏点的判定需要连续N帧在同一位置都出现异常才确认为坏点。N的取值很讲究太小了误杀率高太大了漏检率高。我一般建议N取3到5帧具体要看帧率和场景运动速度。车载场景帧率30fpsN取3比较合适安防场景帧率可能只有15fpsN取2到3。但多帧检测也有坑。如果场景本身是静止的比如安防监控对着一个固定区域那么画面中的噪点也会在固定位置出现多帧容易被误判为坏点。这时候需要引入一个“噪声容忍度”的概念——不是看绝对差值而是看相对信噪比。具体做法是先计算当前像素与邻域均值的差值再除以邻域标准差得到一个归一化的偏离度。只有偏离度超过某个阈值并且连续多帧都超过才判定为坏点。这个阈值我通常设在5到6个标准差太低会误杀太高会漏检。再说cluster坏点。单像素坏点好处理但Sensor制造过程中可能出现一片区域的多个像素同时损坏形成2x2、3x3甚至更大的坏点簇。这种cluster坏点如果按单像素处理会出现一个严重问题坏点簇内部的像素互相作为邻域参考导致检测不到。比如一个3x3的坏点簇中心像素的8邻域全是坏点邻域均值本身就被污染了差值计算出来反而不大漏检了。处理cluster坏点我常用的方法是分级检测。第一级用大窗口比如5x5或7x7计算邻域统计值找出疑似坏点区域第二级在疑似区域内做细粒度检测区分单点坏和cluster坏。大窗口的好处是即使坏点簇内部有污染窗口边缘的正常像素仍然能提供参考。但大窗口也有问题——如果坏点簇太大比如超过5x5大窗口也被污染了。这时候需要引入“方向性检测”分别计算水平、垂直、两个对角线方向的梯度取最小值作为参考。因为坏点簇通常是各向同性的而真实图像中的边缘是各向异性的方向性检测可以区分这两者。这里踩过一个大坑。某次在工业视觉项目里客户的产品表面有规则的网格纹理网格交叉点的像素亮度极高。我们的动态检测算法把这些交叉点全判成了坏点导致校正后网格纹理变得模糊。后来分析发现网格交叉点的亮度虽然高但在时间域上是稳定的而且方向性梯度在水平和垂直方向都很大但在对角线方向很小。真正的坏点应该是所有方向梯度都异常大。于是我们加了一个条件只有四个方向梯度都超过阈值才判定为坏点。这样网格交叉点就被排除了。极限case处理我总结了几类。第一类是Sensor输出饱和——当场景过曝时所有像素都达到最大值这时候任何检测算法都会失效。处理办法是检测到饱和区域后跳过该区域的坏点检测只做静态表校正。第二类是Sensor输出全黑——暗场下所有像素值都很低动态检测的阈值需要动态调整不能用一个固定值。我通常的做法是根据当前帧的全局统计值均值、方差自适应调整检测阈值。第三类是坏点恰好位于图像边缘——邻域像素不足无法计算统计值。处理办法是边缘像素只做静态表校正不做动态检测或者用镜像填充的方式扩展邻域。还有一个容易被忽略的点坏点校正的顺序。在ISP pipeline里坏点校正通常放在黑电平校正之后、去噪之前。如果放在去噪之后去噪算法会把坏点的能量扩散到周围像素导致坏点特征被掩盖检测难度大增。如果放在黑电平之前暗电流的影响还没被去除检测阈值不好设。所以顺序很重要别乱调。代码实现上动态检测的核心逻辑大概是这样的// 这里踩过坑别用int存像素差值会溢出// 用int32_t特别是处理12bit RAW时int32_tdiff[4];// 四个方向的梯度int32_tmin_gradINT32_MAX;// 计算四个方向的梯度取最小值// 方向1水平diff[0]abs(pixel-left)abs(pixel-right);// 方向2垂直diff[1]abs(pixel-up)abs(pixel-down);// 方向3主对角线diff[2]abs(pixel-up_left)abs(pixel-down_right);// 方向4副对角线diff[3]abs(pixel-up_right)abs(pixel-down_left);for(inti0;i4;i){if(diff[i]min_grad){min_graddiff[i];}}// 归一化除以邻域标准差// 别用全局标准差要用局部窗口的floatnorm_grad(float)min_grad/local_std;// 多帧一致性判断// 这里用环形缓冲区存历史结果别用数组拷贝浪费带宽if(norm_gradthresholdhistory[frame_idx%N]1){// 连续N帧都异常确认为坏点pixelmedian_filter(pixel,window);}最后说点个人经验。坏点校正这个模块看起来简单但真正做好很难。我见过太多团队在这个模块上栽跟头原因都是只盯着单帧的检测算法忽略了时间域和空间域的联合分析。我的建议是第一静态表一定要做而且要定期更新——量产后的老化测试数据要回流到静态表里第二动态检测一定要结合多帧单帧检测就是耍流氓第三阈值参数不要拍脑袋定要基于大量实拍数据做统计分析找到合理的分布区间第四一定要留调试接口把检测到的坏点位置和置信度输出到日志里方便现场排查。坏点校正不是一锤子买卖它是Sensor生命周期里持续演进的过程。你今天调好的参数明天换个温度环境可能就失效了。所以设计算法的时候就要想着怎么自适应、怎么自学习而不是靠一堆硬编码的阈值撑场面。