3. 坏点校正(DPC):从像素瑕疵到完美图像

坏点校正,英文叫 Defect Pixel Correction,简称 DPC。说白了,就是处理传感器上那些「不听话」的像素点。我做了这么多年 ISP,可以负责任地告诉你——坏点校正做不好,后面的所有处理都是在放大错误。

你想想看,一个 CMOS 传感器动辄几千万像素,生产过程中难免会有几个像素点表现异常。这些坏点如果不处理,拍出来的照片就会出现固定的亮点、暗点或者闪烁点,非常影响观感。

核心认知:坏点校正不是可选项,而是 ISP 流水线的第一道防线。它决定了后续去噪、锐化等模块的输入质量。

3.1 坏点类型:静态 vs 动态

坏点分两大类,我习惯把它们比作「永久性故障」和「间歇性抽风」。

类型 表现 成因 处理难度
静态坏点 固定位置,始终异常 制造缺陷、物理损伤 低(可离线标定)
动态坏点 特定条件才出现 温度、增益、老化 高(需实时检测)

静态坏点又分三种:

  • 亮点(Hot Pixel):输出值始终偏高,甚至饱和。我在项目中遇到过一颗传感器,出厂时亮点只有几十个,用了半年后涨到上千个——温度老化导致的。
  • 暗点(Dead Pixel):输出值始终偏低,甚至为零。这种通常是因为光电二极管损坏。
  • Stuck Pixel:固定输出某个值,不随光照变化。嗯,这种最烦人,因为它看起来像正常像素,但就是不动。

动态坏点就狡猾多了。它们平时表现正常,但在高增益、高温或长曝光时突然「发病」。我记得有一次调试夜景模式,发现画面某些位置出现随机闪烁的白点,查了半天才发现是动态坏点在作祟。

实战经验:量产前一定要做温度循环测试(-20°C 到 85°C),很多动态坏点只有在极端温度下才会暴露。我曾经吃过这个亏,后来再也不敢省这一步。

3.2 坏点检测算法

检测坏点,核心思路就一句话:看这个像素和周围邻居是不是「格格不入」。下面介绍两种最常用的方法。

3.2.1 梯度法

梯度法基于一个假设:自然图像中,相邻像素的亮度变化是平滑的。如果某个像素与周围四个方向(水平、垂直、两个对角线)的梯度都异常大,那它很可能是个坏点。

具体做法是这样的:

  1. 计算中心像素与上下左右四个邻域的差值
  2. 计算中心像素与两个对角线方向的差值
  3. 如果所有方向的差值都超过阈值,判定为坏点
// 梯度法检测伪代码
int gradients[4];
gradients[0] = abs(pixel - pixel_up);
gradients[1] = abs(pixel - pixel_down);
gradients[2] = abs(pixel - pixel_left);
gradients[3] = abs(pixel - pixel_right);

int min_grad = min(gradients, 4);
int max_grad = max(gradients, 4);

if (min_grad > threshold_high && max_grad > threshold_extreme) {
    // 判定为坏点
}

梯度法的优点是计算量小,硬件实现简单。但缺点也很明显——对纹理边缘敏感,容易把正常的边缘点误判为坏点。我一般会加一个边缘保护机制,比如先判断当前像素是否在纹理区域,如果是就放宽阈值。

3.2.2 中值法

中值法的思路更直接:用邻域像素的中值作为参考,如果当前像素偏离中值太远,就认为是坏点。

// 中值法检测伪代码
int neighbors[8] = {p1, p2, p3, p4, p5, p6, p7, p8};
int median = get_median(neighbors, 8);
int diff = abs(pixel - median);

if (diff > threshold) {
    // 判定为坏点
}

中值法对孤立坏点的检测效果很好,而且不容易受边缘影响。但它的计算量比梯度法大——排序找中值在硬件上比较费资源。我个人习惯在高端芯片上用中值法,在低功耗芯片上用梯度法。

避坑指南:我曾经在项目中把阈值设得太低,结果把星空照片里的星星全当坏点给抹掉了。阈值一定要根据传感器噪声水平动态调整,不能一刀切。

3.3 坏点校正策略

检测到坏点之后,怎么修?主要有两种思路。

3.3.1 插值法

用周围好像素的值来「猜」坏点应该是什么值。最常用的是双线性插值和双三次插值。

// 双线性插值校正
// 假设坏点在 (x, y),用四个邻域像素插值
float value = (pixel_left + pixel_right + pixel_up + pixel_down) / 4.0f;

插值法的优点是校正后图像平滑,看不出修复痕迹。但缺点是会损失一点点细节——毕竟是在「猜」数据。

3.3.2 替换法

直接用邻域像素的中值或均值替换坏点。简单粗暴,效果也不错。

// 中值替换
uint8_t neighbors[8] = {p1, p2, p3, p4, p5, p6, p7, p8};
pixel = get_median(neighbors, 8);

替换法的优点是实现简单,延迟低。缺点是在纹理丰富的区域可能会产生轻微的块状感。

策略 优点 缺点 适用场景
插值法 平滑自然,细节保留好 计算量大,有延迟 高端相机、专业摄影
替换法 实现简单,延迟低 纹理区域可能不自然 手机摄像头、实时预览

3.4 硬件实现考量

把 DPC 算法搬到硬件上,有几个坑必须注意。

第一,行缓冲(Line Buffer)。坏点检测需要访问邻域像素,这意味着至少需要缓存几行数据。以 5x5 窗口为例,需要 4 行缓冲。每行缓冲的大小就是图像宽度。对于 4K 分辨率,这可不是个小数目。

第二,流水线延迟。检测和校正需要时间,会引入几个时钟周期的延迟。在实时视频处理中,必须确保延迟在可接受范围内。我一般控制在 2-3 行扫描时间内完成。

第三,阈值管理。阈值不能是固定的。我习惯的做法是:根据当前帧的增益和温度,查表动态调整阈值。这个查找表可以在芯片初始化时加载,也可以在运行中更新。

硬件优化技巧:对于静态坏点,可以在出厂标定时生成一张坏点坐标表,硬件直接查表跳过检测步骤。这样能省下不少功耗和面积。

3.5 DPC 处理流程图

下面这张图展示了 DPC 模块的完整处理流程,从像素输入到校正输出:

坏点校正(DPC)处理流程图 原始 Bayer 数据 静态坏点查表(坐标匹配) 是坏点? 是 动态坏点检测 否 直接输出原始值 坏点校正(插值 / 替换) 校正后 Bayer 数据 静态坏点查表优先 动态检测作为补充

从流程图可以看出,DPC 的处理路径是分叉的:静态坏点通过查表直接定位,动态坏点则需要实时检测。两种路径最终汇聚到校正模块,输出干净的 Bayer 数据。

3.6 小结

坏点校正看似简单,但要做好其实不容易。我总结几个关键点:

  • 静态坏点用查表,动态坏点用算法——两者结合才能覆盖所有场景
  • 阈值要动态调整——固定阈值在低增益下可能漏检,在高增益下可能误检
  • 硬件实现要权衡面积和延迟——没有完美的方案,只有最适合的方案