3. 坏点校正(DPC):从像素瑕疵到完美图像
坏点校正,英文叫 Defect Pixel Correction,简称 DPC。说白了,就是处理传感器上那些「不听话」的像素点。我做了这么多年 ISP,可以负责任地告诉你——坏点校正做不好,后面的所有处理都是在放大错误。
你想想看,一个 CMOS 传感器动辄几千万像素,生产过程中难免会有几个像素点表现异常。这些坏点如果不处理,拍出来的照片就会出现固定的亮点、暗点或者闪烁点,非常影响观感。
3.1 坏点类型:静态 vs 动态
坏点分两大类,我习惯把它们比作「永久性故障」和「间歇性抽风」。
| 类型 | 表现 | 成因 | 处理难度 |
|---|---|---|---|
| 静态坏点 | 固定位置,始终异常 | 制造缺陷、物理损伤 | 低(可离线标定) |
| 动态坏点 | 特定条件才出现 | 温度、增益、老化 | 高(需实时检测) |
静态坏点又分三种:
- 亮点(Hot Pixel):输出值始终偏高,甚至饱和。我在项目中遇到过一颗传感器,出厂时亮点只有几十个,用了半年后涨到上千个——温度老化导致的。
- 暗点(Dead Pixel):输出值始终偏低,甚至为零。这种通常是因为光电二极管损坏。
- Stuck Pixel:固定输出某个值,不随光照变化。嗯,这种最烦人,因为它看起来像正常像素,但就是不动。
动态坏点就狡猾多了。它们平时表现正常,但在高增益、高温或长曝光时突然「发病」。我记得有一次调试夜景模式,发现画面某些位置出现随机闪烁的白点,查了半天才发现是动态坏点在作祟。
3.2 坏点检测算法
检测坏点,核心思路就一句话:看这个像素和周围邻居是不是「格格不入」。下面介绍两种最常用的方法。
3.2.1 梯度法
梯度法基于一个假设:自然图像中,相邻像素的亮度变化是平滑的。如果某个像素与周围四个方向(水平、垂直、两个对角线)的梯度都异常大,那它很可能是个坏点。
具体做法是这样的:
- 计算中心像素与上下左右四个邻域的差值
- 计算中心像素与两个对角线方向的差值
- 如果所有方向的差值都超过阈值,判定为坏点
// 梯度法检测伪代码
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 数据。
3.6 小结
坏点校正看似简单,但要做好其实不容易。我总结几个关键点:
- 静态坏点用查表,动态坏点用算法——两者结合才能覆盖所有场景
- 阈值要动态调整——固定阈值在低增益下可能漏检,在高增益下可能误检
- 硬件实现要权衡面积和延迟——没有完美的方案,只有最适合的方案