4. 坏点校正(DPC):静态与动态坏点检测算法、校正策略、调试参数优化
坏点校正,圈内人常叫 DPC(Defect Pixel Correction)。说白了,就是处理 sensor 上那些「不听话」的像素点。有的永远亮着,有的永远暗着,还有的时好时坏——这些统称坏点。
我刚开始做 ISP 时,觉得 DPC 不就是个中值滤波嘛。后来被一个项目狠狠教育了——sensor 温度一高,坏点像雨后春笋一样冒出来,静态表根本不够用。从那以后,我对 DPC 的认知才算真正到位。
4.1 坏点分类与成因
先搞清楚敌人长什么样。坏点按行为分三类:
| 类型 | 表现 | 成因 | 出现时机 |
|---|---|---|---|
| 静态亮点 | 固定像素值偏高(接近饱和) | 制造缺陷、光刻污染 | 出厂即存在 |
| 静态暗点 | 固定像素值偏低(接近黑电平) | 暗电流异常、电路断路 | 出厂即存在 |
| 动态坏点 | 随机出现,时好时坏 | 温度漂移、高增益噪声、辐射 | 使用过程中出现 |
静态坏点好办,出厂标定一次就行。动态坏点才是真正的麻烦——你永远不知道它什么时候冒出来。
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 里。每次上电直接加载,不用重新检测。
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
嗯,这里要注意:统计法对纹理区域容易误检。你想想看,一条细线边缘的像素,和邻域差异也很大,但它不是坏点。所以实际工程中,我通常把梯度法和统计法结合使用。
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 调优流程
- 准备测试图: 暗场、亮场、灰阶卡、ISO 12233 分辨率卡。
- 粗调梯度阈值: 在灰阶卡上调整,确保无明显误检。
- 细调比例阈值: 在暗场上调整,确保动态亮点被检出。
- 验证边缘保护: 在分辨率卡上检查边缘是否模糊。
- 温度循环测试: 在 -20°C 到 60°C 范围内验证稳定性。
4.6 DPC 整体流程框架
下面这张图是我自己总结的 DPC 处理流程,从检测到校正,再到参数反馈,形成一个闭环。
这个流程里,我最想强调的就是那个「参数反馈调优」回路。很多工程师做完检测和校正就收工了,但实际场景中,坏点行为会随着 sensor 老化、温度变化而漂移。我建议在系统中加入一个统计模块,定期分析校正后的图像,如果发现某个区域频繁被校正,就动态调整该区域的检测阈值。