4. 坏点校正(DPC):静态与动态坏点检测算法、校正策略、调试参数优化

坏点校正,圈内人常叫 DPC(Defect Pixel Correction)。说白了,就是处理 sensor 上那些「不听话」的像素点。有的永远亮着,有的永远暗着,还有的时好时坏——这些统称坏点。

我刚开始做 ISP 时,觉得 DPC 不就是个中值滤波嘛。后来被一个项目狠狠教育了——sensor 温度一高,坏点像雨后春笋一样冒出来,静态表根本不够用。从那以后,我对 DPC 的认知才算真正到位。

核心认知: 坏点校正不是简单的滤波,而是「检测 + 校正」的组合拳。检测不准,校正就是瞎改;校正策略不对,边缘细节全糊。

4.1 坏点分类与成因

先搞清楚敌人长什么样。坏点按行为分三类:

类型 表现 成因 出现时机
静态亮点 固定像素值偏高(接近饱和) 制造缺陷、光刻污染 出厂即存在
静态暗点 固定像素值偏低(接近黑电平) 暗电流异常、电路断路 出厂即存在
动态坏点 随机出现,时好时坏 温度漂移、高增益噪声、辐射 使用过程中出现

静态坏点好办,出厂标定一次就行。动态坏点才是真正的麻烦——你永远不知道它什么时候冒出来。

经验之谈: 我见过一个项目,sensor 在 25°C 下只有 3 个静态坏点,升温到 60°C 后动态坏点飙到 200+。所以 DPC 调试一定要做温度老化测试,别偷懒。

4.2 静态坏点检测算法

静态坏点检测,说白了就是「找茬」。在暗场和亮场下分别抓图,对比理论值和实际值。

4.2.1 暗场标定法

盖上镜头盖,拍一张全黑图。正常像素应该接近黑电平值。如果某个像素明显偏亮,那就是亮点。

// 暗场标定伪代码
for each pixel (x, y):
    if pixel_value > (black_level + dark_threshold):
        mark as static_white_defect[x][y] = true

4.2.2 亮场标定法

对着均匀光源(比如积分球),拍一张全白图。正常像素应该接近饱和值。如果某个像素明显偏暗,那就是暗点。

// 亮场标定伪代码
for each pixel (x, y):
    if pixel_value < (saturation_level - bright_threshold):
        mark as static_black_defect[x][y] = true

我个人习惯把两张标定结果合并成一张坏点地图(Defect Map),存到 sensor 的 OTP 或者系统 flash 里。每次上电直接加载,不用重新检测。

注意: 标定时的环境温度要和实际使用温度尽量一致。我曾经吃过亏——在 25°C 下标定的坏点地图,到 50°C 时部分静态坏点位置偏移了 1-2 个像素。后来我改用多温度点标定,取并集。

4.3 动态坏点检测算法

动态坏点检测,核心思路是「看邻居」。如果一个像素和它周围的像素差异太大,那它大概率有问题。

4.3.1 基于梯度的检测

计算当前像素在四个方向(水平、垂直、45°、135°)上的梯度。如果所有方向的梯度都超过阈值,判定为坏点。

// 梯度检测核心逻辑
grad_h = abs(pixel - left) + abs(pixel - right)
grad_v = abs(pixel - up) + abs(pixel - down)
grad_d1 = abs(pixel - up_left) + abs(pixel - down_right)
grad_d2 = abs(pixel - up_right) + abs(pixel - down_left)

if min(grad_h, grad_v, grad_d1, grad_d2) > gradient_threshold:
    mark as dynamic_defect

为什么会这样?因为正常图像中,像素值在局部是连续的。坏点就像一个「突兀的尖峰」,四个方向都会产生大梯度。

4.3.2 基于统计的检测

另一种思路:计算当前像素和周围 8 邻域的均值、中值。如果偏差超过 3σ 或某个倍数,判定为坏点。

// 统计检测核心逻辑
neighbors = [p1, p2, ..., p8]
median_val = median(neighbors)
diff = abs(pixel - median_val)

if diff > median_val * ratio_threshold:
    mark as dynamic_defect

嗯,这里要注意:统计法对纹理区域容易误检。你想想看,一条细线边缘的像素,和邻域差异也很大,但它不是坏点。所以实际工程中,我通常把梯度法和统计法结合使用。

推荐方案: 先用梯度法粗筛,再用统计法确认。梯度法过滤掉大部分正常纹理,统计法对剩余候选做最终判决。误检率能降低 60% 以上。

4.4 校正策略

检测到坏点后,怎么修?校正策略直接决定图像质量。

4.4.1 中值替换法

最简单粗暴。用周围 3x3 或 5x5 窗口的中值替换坏点。

// 中值替换
corrected_pixel = median(neighbors_3x3)

优点:实现简单,对孤立坏点效果好。
缺点:在边缘区域会模糊细节。

4.4.2 方向插值法

先判断坏点所在区域的纹理方向,然后沿该方向插值。

// 方向插值
// 1. 计算四个方向的梯度
// 2. 选择梯度最小的方向(纹理方向)
// 3. 沿该方向取两个像素做线性插值

if grad_h < grad_v:
    corrected = (left + right) / 2
else:
    corrected = (up + down) / 2

我建议在边缘丰富的场景(比如文字、建筑)使用方向插值法。效果比中值替换好一个档次。

4.4.3 自适应校正

结合以上两种方法,根据局部纹理复杂度动态选择校正策略。

局部纹理复杂度 推荐策略 说明
平坦区域 中值替换 计算快,效果足够
边缘区域 方向插值 保护边缘细节
纹理密集区 加权混合 中值和方向插值按权重融合
我的习惯: 自适应校正虽然效果好,但计算量大。在低端平台上,我通常只用方向插值法,配合一个简单的纹理检测开关——纹理强时用方向插值,纹理弱时回退到中值。

4.5 调试参数优化

DPC 调试,说白了就是调一堆阈值。调得好,坏点全消;调不好,要么漏检,要么误检。

4.5.1 关键参数清单

参数 含义 典型范围 调优方向
gradient_threshold 梯度检测阈值 20-80(8bit) 越大越漏检,越小越误检
ratio_threshold 统计偏差比例 0.3-0.6 越大越漏检,越小越误检
window_size 检测窗口大小 3x3, 5x5 越大越鲁棒,但计算量增加
max_defect_count 单帧最大校正数 10-100 防止过度校正导致图像模糊

4.5.2 调优流程

  1. 准备测试图: 暗场、亮场、灰阶卡、ISO 12233 分辨率卡。
  2. 粗调梯度阈值: 在灰阶卡上调整,确保无明显误检。
  3. 细调比例阈值: 在暗场上调整,确保动态亮点被检出。
  4. 验证边缘保护: 在分辨率卡上检查边缘是否模糊。
  5. 温度循环测试: 在 -20°C 到 60°C 范围内验证稳定性。
避坑指南: 我曾经把梯度阈值设得太低,结果在星空夜景下把星星全当坏点给修了——整张图变成一片死黑。后来我加了一个「亮度自适应」逻辑:高亮区域自动提高阈值,避免误检真实细节。

4.6 DPC 整体流程框架

下面这张图是我自己总结的 DPC 处理流程,从检测到校正,再到参数反馈,形成一个闭环。

DPC 整体流程框架 DPC 处理流程框架 Raw 输入 静态坏点检测 动态坏点检测 坏点地图 坏点合并与去重 校正策略选择(自适应) 校正执行 校正后 Raw 输出 参数反馈调优 说明 静态检测只在标定阶段执行 动态检测每帧都执行

这个流程里,我最想强调的就是那个「参数反馈调优」回路。很多工程师做完检测和校正就收工了,但实际场景中,坏点行为会随着 sensor 老化、温度变化而漂移。我建议在系统中加入一个统计模块,定期分析校正后的图像,如果发现某个区域频繁被校正,就动态调整该区域的检测阈值。

总结: DPC 不是一锤子买卖。静态标定打基础,动态检测抓变化,自适应校正保质量,参数反馈做优化。四步走完,才能说 DPC 调好了。