3. 坏点校正(DPC):静态坏点与动态坏点、坏点检测算法、校正策略与调试经验

坏点校正,简称 DPC,是 ISP Pipeline 里非常基础但又极其关键的一环。说白了,就是 sensor 本身有瑕疵,拍出来的图像上会有一些不该出现的亮点或暗点。如果不处理,后面做降噪、锐化,这些坏点会被放大,非常难看。

我刚开始接触 ISP 时,觉得坏点校正不就是把坏点找出来替换掉吗?后来在实际项目中才发现,这里面的坑远比想象的多。尤其是动态坏点,稍不注意就会把图像细节误判成坏点,或者漏掉真正的坏点。

3.1 静态坏点与动态坏点

先搞清楚两种坏点的区别。这是做 DPC 的前提。

类型 定义 表现 成因
静态坏点 位置固定,亮度值始终异常 全黑下为亮点(hot pixel)或全白下为暗点(dead pixel) sensor 制造缺陷、物理损伤
动态坏点 位置不固定,随温度、增益、曝光时间变化 高增益或长曝光时出现,低增益时消失 sensor 暗电流不均匀、温度噪声

静态坏点好处理。出厂时做一次标定,把坏点坐标存到 OTP 里,后面直接查表替换就行。我遇到过最头疼的是动态坏点。有一次在暗光场景下,用户发现画面里时不时冒出一个白点,但位置不固定。查了半天,原来是 sensor 温度升高后,某些像素的暗电流突然增大,变成了临时坏点。

注意:动态坏点无法通过出厂标定解决。必须靠实时检测算法来捕捉。这也是 DPC 调试中最容易出问题的地方。

3.2 坏点检测算法

检测坏点,核心思路就是看当前像素和周围邻居的差异是否过大。常用的方法有两种:梯度法和中值法。

3.2.1 梯度法

梯度法比较直观。计算当前像素在四个方向(水平、垂直、两个对角线)上的梯度值。如果某个方向的梯度远大于其他方向,且当前像素值明显偏离邻域均值,就判定为坏点。

// 伪代码:梯度法检测坏点
// 以 3x3 窗口为例,中心像素为 P(i,j)
grad_h = |P(i,j-1) - P(i,j+1)|
grad_v = |P(i-1,j) - P(i+1,j)|
grad_d1 = |P(i-1,j-1) - P(i+1,j+1)|
grad_d2 = |P(i-1,j+1) - P(i+1,j-1)|

min_grad = min(grad_h, grad_v, grad_d1, grad_d2)
max_grad = max(grad_h, grad_v, grad_d1, grad_d2)

if (max_grad / min_grad > threshold) && (|P(i,j) - median| > diff_threshold):
    mark as defect pixel

梯度法的优点是计算量小,适合硬件实现。但缺点也很明显:对纹理边缘敏感。你想想看,如果当前像素正好在一条细线上,梯度法很容易把它误判成坏点。我在一个项目中就吃过这个亏,拍出来的文字边缘全是斑点,后来不得不降低梯度阈值,改用中值法。

3.2.2 中值法

中值法更鲁棒一些。取当前像素周围邻域的中值,如果当前像素与中值的差异超过某个阈值,就认为是坏点。

// 伪代码:中值法检测坏点
// 5x5 窗口,取周围 24 个像素的中值
median_val = median(neighbors_24)
diff = |P(i,j) - median_val|

if diff > threshold:
    mark as defect pixel

中值法对纹理的误判率更低,因为中值本身不受单个异常值影响。但计算中值需要排序,硬件开销比梯度法大。实际工程中,我一般会结合两种方法:先用梯度法做粗筛,再用中值法做确认。这样既能保证速度,又能降低误判。

我的经验:动态坏点的检测阈值不能固定。我习惯根据 sensor 的增益和温度做自适应调整。增益越高,阈值越低,因为动态坏点在高增益下更明显。温度每升高 10 度,阈值可以降低 10%-15%。

3.3 坏点校正策略

检测到坏点后,怎么替换?常用的策略有两种:插值替换和自适应滤波。

3.3.1 插值替换

最简单的做法。用坏点周围正常像素的平均值或中值替换坏点。

// 伪代码:插值替换
// 以 3x3 窗口为例,只取非坏点像素做平均
valid_pixels = []
for each neighbor in 3x3 window:
    if not is_defect(neighbor):
        valid_pixels.append(neighbor)

if len(valid_pixels) > 0:
    P(i,j) = mean(valid_pixels)
else:
    // 周围全是坏点,扩大窗口
    P(i,j) = interpolate_from_larger_window()

插值替换的优点是简单、速度快。但缺点也很直接:如果坏点周围也有坏点,或者坏点位于纹理区域,替换后的像素会显得不自然。我记得有一次调试,坏点正好在一条边缘上,插值后边缘出现了明显的锯齿。

3.3.2 自适应滤波

自适应滤波更聪明一些。它会根据坏点周围的内容特征,选择不同的滤波方式。

  • 平坦区域:用均值滤波,平滑效果好。
  • 边缘区域:用方向性滤波,只沿边缘方向做插值,保护边缘。
  • 纹理区域:用中值滤波,保留纹理细节。

自适应滤波的效果更好,但计算量也更大。我一般在高端产品上用自适应滤波,在低端产品上用插值替换。说白了,还是成本和性能的权衡。

小技巧:自适应滤波中判断区域类型,可以用前面梯度法算出来的梯度值。梯度小的区域是平坦区,梯度大的区域是边缘或纹理。这样可以把检测和校正两个模块复用一部分计算资源。

3.4 调试经验与常见问题

做 DPC 调试这么多年,踩过的坑不少。挑几个典型的说说。

3.4.1 误判问题

最常见的坑。把图像细节当成坏点处理了。比如星空照片里的星星、远处灯光、甚至人脸的高光。我曾经在一个安防项目中,把监控画面里路灯的光晕全当坏点给抹掉了,结果晚上画面一片模糊。

解决办法:增加保护机制。检测到坏点后,先判断它是否属于高频纹理。如果是,就降低校正强度,甚至不处理。

3.4.2 漏判问题

动态坏点漏判,通常是因为阈值设得太高。但阈值设低了,又容易误判。这是个两难问题。

解决办法:我个人的习惯是,把阈值分成两级。第一级用较严格的阈值,确保不误判;第二级用较宽松的阈值,对第一级未覆盖的区域做补充检测。两级检测的结果取并集。

3.4.3 校正后图像模糊

坏点校正后,图像局部变模糊。这通常是因为校正算法把坏点周围的正常像素也一起平滑了。

解决办法:只替换坏点本身,不要动邻居像素。另外,校正后可以加一个微弱的锐化,补偿模糊。

避坑指南:我曾经在调试时发现,某些 sensor 在低增益下坏点不明显,但高增益下坏点数量暴增。后来查资料才知道,这是 sensor 的「暗电流非均匀性」导致的。解决办法是在高增益下启用更激进的 DPC 参数,低增益下用保守参数。千万别一套参数打天下。

3.5 DPC 知识体系总览

下面这张图总结了 DPC 的核心知识结构。你可以把它当作调试时的参考地图。

坏点校正(DPC)知识体系 坏点校正 DPC 静态坏点 动态坏点 混合坏点 梯度法检测 中值法检测 组合检测 插值替换 自适应滤波 方向性插值 阈值自适应 误判/漏判平衡 增益/温度补偿 核心原则:检测准确 > 校正强度。宁可漏掉一个坏点,也不要误伤一片正常像素。 调试顺序:先静态后动态 → 先检测后校正 → 先全局后局部

这张图把 DPC 的四个核心环节串起来了:坏点类型 → 检测算法 → 校正策略 → 调试要点。你在实际调试时,可以按这个顺序一步步排查问题。