3. 坏点校正(DPC):静态坏点与动态坏点检测算法,以及常用的校正策略
坏点校正,简称 DPC,是 ISP 流水线里最基础、但也最容易被忽视的一环。我见过不少团队把精力全花在 3A 上,结果 sensor 送进来的图全是星星点点的白斑或黑点,后面调得再好也白搭。说白了,坏点就是 sensor 上那些「不听话」的像素——该亮的时候不亮,该暗的时候不暗。
我个人习惯把坏点分成两类:静态坏点和动态坏点。处理思路完全不同,咱们一个一个说。
3.1 静态坏点检测
静态坏点,就是出厂就有的、或者随着时间推移固定坏掉的像素。这类坏点的位置是固定的,检测起来相对简单。
常用的检测方法:
- 全黑帧检测:盖上镜头盖,拍一张全黑图。正常像素输出值应该接近 0,坏点(亮点)会输出明显高于阈值的值。
- 全白帧检测:对着均匀光源拍一张全白图。正常像素输出值应该接近饱和值,坏点(暗点)会输出明显低于阈值的值。
- 差值法:连续拍多帧,计算每个像素的均值与方差。方差过大的像素,大概率是坏点。
关键参数:
- 亮点阈值:全黑帧中,像素值 > 10(12-bit RAW)即视为坏点
- 暗点阈值:全白帧中,像素值 < 最大值 - 50 即视为坏点
- 坏点列表:检测到的坏点坐标存入 OTP 或 flash,上电时加载
我在项目中遇到过一个问题:某款 sensor 出厂时坏点列表是好的,但用了半年后,高温环境下新冒出来一批坏点。静态检测只能抓到出厂时的,运行中产生的就抓不到了。这就引出了动态坏点检测。
3.2 动态坏点检测
动态坏点,是 sensor 在工作过程中临时出现的异常像素。温度变化、电压波动、强光照射都可能诱发。这类坏点位置不固定,必须实时检测。
实时检测的核心思路:
说白了,就是拿当前像素跟它周围的邻居比。如果差异太大,就判定为坏点。具体做法有很多种,我挑两种最常用的讲。
方法一:基于邻域统计的检测
// 伪代码:5x5 邻域坏点检测
for each pixel P at (x, y):
// 取 5x5 邻域,排除中心点
neighbors = get_5x5_neighbors(x, y)
mean = average(neighbors)
std = standard_deviation(neighbors)
// 如果中心点偏离均值超过 3 倍标准差
if abs(P - mean) > 3 * std:
mark_as_defect(x, y)
方法二:基于方向梯度的检测
这个方法我比较喜欢,因为它能区分「边缘」和「坏点」。你想想看,如果像素在边缘上,它跟邻居的差异本来就大,但那是正常的。方向梯度法会检查四个方向(水平、垂直、两个对角线)的梯度,如果某个方向梯度异常大,而其他方向正常,那大概率是边缘;如果四个方向梯度都异常,那就是坏点。
| 检测方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 邻域统计法 | 实现简单,计算量小 | 容易误判边缘 | 低噪声 sensor,平坦区域多 |
| 方向梯度法 | 能区分边缘与坏点 | 计算量稍大 | 高噪声 sensor,纹理复杂场景 |
| 自适应阈值法 | 鲁棒性好,适应不同亮度 | 需要额外统计全局亮度 | 光照变化大的场景 |
注意:动态检测最怕的就是「误杀」。我曾经在一个项目中,把星空的星星误判成了坏点,结果整张图被抹得干干净净。后来加了一个「亮度梯度保护」——如果当前像素的亮度梯度与周围像素一致,即使值异常也不判为坏点。
3.3 坏点校正策略
检测到坏点之后,怎么修?常用的策略有下面几种。
策略一:邻域均值替换
最简单粗暴的方法。取坏点周围 3x3 或 5x5 邻域内正常像素的均值,直接替换坏点值。
// 3x3 邻域均值校正
P_defect = (P_tl + P_tc + P_tr +
P_ml + P_mr +
P_bl + P_bc + P_br) / 8
优点:实现简单,速度快。缺点:会模糊细节,尤其是坏点位于边缘时。
策略二:中值替换
取邻域内像素的中值,而不是均值。中值对异常值不敏感,效果比均值好一些。
我的经验:中值替换在消除孤立坏点时效果很好,但如果坏点成片出现(比如 2x2 的 cluster),中值也会失效。这时候需要更大的邻域,或者用更复杂的插值算法。
策略三:方向插值
先判断坏点所在区域的纹理方向,然后沿着该方向取两个正常像素做线性插值。比如水平边缘,就用左右两个像素的平均值。
// 水平方向插值
if (is_horizontal_edge(x, y)):
P_defect = (P_left + P_right) / 2
// 垂直方向插值
else if (is_vertical_edge(x, y)):
P_defect = (P_top + P_bottom) / 2
// 对角线方向插值
else:
P_defect = (P_tl + P_tr + P_bl + P_br) / 4
策略四:自适应校正
这是目前工业界用得最多的方法。结合多种策略,根据坏点周围的纹理复杂度、亮度水平、噪声水平,动态选择最优的校正方式。
我参与过的一个项目中,我们用了这样的分级策略:
- 孤立坏点:中值替换(3x3 邻域)
- 边缘坏点:方向插值(沿边缘方向)
- 成片坏点:扩大邻域到 5x5,用加权均值
- 高亮区域坏点:保持原值不校正(防止过曝区域被错误修正)
3.4 DPC 在 ISP 流水线中的位置
坏点校正通常放在 ISP 的最前面,在去噪、去马赛克之前。为什么?因为坏点如果不先处理掉,后面的算法会把坏点当成真实信号,越处理越糟糕。
3.5 工程实现中的注意事项
嗯,这里要注意几个坑,我踩过不少。
坑一:坏点列表的存储与更新
静态坏点列表一般存在 sensor 的 OTP 里,或者存在主板的 flash 中。但 sensor 用久了会老化,新坏点会不断出现。我建议在产线测试时留一个「坏点更新接口」,允许在售后阶段通过固件升级更新坏点列表。
坑二:动态检测的阈值设置
阈值设得太低,误判多;设得太高,漏判多。我的做法是:先根据 sensor 的噪声模型算出一个基础阈值,然后在实际场景中微调。不同亮度下的阈值应该不同——暗区噪声大,阈值要放宽;亮区噪声小,阈值可以收紧。
坑三:性能与精度的平衡
动态检测如果做 5x5 甚至 7x7 的邻域统计,计算量会很大。在嵌入式平台上,我通常的做法是:
- 先做一次快速的 3x3 检测,筛掉大部分正常像素
- 对疑似坏点再做 5x5 的精细检测
- 这样能节省 70% 以上的计算量
避坑指南:我曾经在一个项目中,为了追求检测精度,把邻域开到了 7x7。结果帧率直接掉了一半。后来改成两级检测,精度没降,帧率回来了。记住:工程实现不是算法竞赛,够用就好。
3.6 总结
坏点校正这件事,说难不难,说简单也不简单。静态坏点靠列表,动态坏点靠算法。校正策略上,我建议优先用方向插值,实在不行再退回到均值或中值。最重要的是,一定要把 DPC 放在 ISP 的最前面,别让坏点污染了后面的处理。
最后说一句:没有完美的坏点校正算法,只有最适合你 sensor 和场景的方案。多试、多调、多测,才是正道。