3. 坏点校正(DPC):静态坏点与动态坏点检测算法,以及常用的校正策略

坏点校正,简称 DPC,是 ISP 流水线里最基础、但也最容易被忽视的一环。我见过不少团队把精力全花在 3A 上,结果 sensor 送进来的图全是星星点点的白斑或黑点,后面调得再好也白搭。说白了,坏点就是 sensor 上那些「不听话」的像素——该亮的时候不亮,该暗的时候不暗。

我个人习惯把坏点分成两类:静态坏点和动态坏点。处理思路完全不同,咱们一个一个说。

3.1 静态坏点检测

静态坏点,就是出厂就有的、或者随着时间推移固定坏掉的像素。这类坏点的位置是固定的,检测起来相对简单。

常用的检测方法:

  1. 全黑帧检测:盖上镜头盖,拍一张全黑图。正常像素输出值应该接近 0,坏点(亮点)会输出明显高于阈值的值。
  2. 全白帧检测:对着均匀光源拍一张全白图。正常像素输出值应该接近饱和值,坏点(暗点)会输出明显低于阈值的值。
  3. 差值法:连续拍多帧,计算每个像素的均值与方差。方差过大的像素,大概率是坏点。

关键参数:

  • 亮点阈值:全黑帧中,像素值 > 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 的最前面,在去噪、去马赛克之前。为什么?因为坏点如果不先处理掉,后面的算法会把坏点当成真实信号,越处理越糟糕。

ISP 流水线中 DPC 的位置 RAW 输入 DPC 坏点校正 去噪 去马赛克 后续处理 为什么 DPC 要放在最前面? 1. 坏点如果不先处理,去噪算法会把坏点当成噪声保留 2. 去马赛克算法会利用坏点周围的像素做插值,导致错误扩散 3. 后续的 3A 算法(AWB/AE/AF)会受坏点影响,导致白平衡偏移或曝光不准 4. 越早处理,错误传播的范围越小,图像质量损失越低

3.5 工程实现中的注意事项

嗯,这里要注意几个坑,我踩过不少。

坑一:坏点列表的存储与更新

静态坏点列表一般存在 sensor 的 OTP 里,或者存在主板的 flash 中。但 sensor 用久了会老化,新坏点会不断出现。我建议在产线测试时留一个「坏点更新接口」,允许在售后阶段通过固件升级更新坏点列表。

坑二:动态检测的阈值设置

阈值设得太低,误判多;设得太高,漏判多。我的做法是:先根据 sensor 的噪声模型算出一个基础阈值,然后在实际场景中微调。不同亮度下的阈值应该不同——暗区噪声大,阈值要放宽;亮区噪声小,阈值可以收紧。

坑三:性能与精度的平衡

动态检测如果做 5x5 甚至 7x7 的邻域统计,计算量会很大。在嵌入式平台上,我通常的做法是:

  • 先做一次快速的 3x3 检测,筛掉大部分正常像素
  • 对疑似坏点再做 5x5 的精细检测
  • 这样能节省 70% 以上的计算量

避坑指南:我曾经在一个项目中,为了追求检测精度,把邻域开到了 7x7。结果帧率直接掉了一半。后来改成两级检测,精度没降,帧率回来了。记住:工程实现不是算法竞赛,够用就好。

3.6 总结

坏点校正这件事,说难不难,说简单也不简单。静态坏点靠列表,动态坏点靠算法。校正策略上,我建议优先用方向插值,实在不行再退回到均值或中值。最重要的是,一定要把 DPC 放在 ISP 的最前面,别让坏点污染了后面的处理。

最后说一句:没有完美的坏点校正算法,只有最适合你 sensor 和场景的方案。多试、多调、多测,才是正道。