4. 坏点校正(DPC):静态与动态坏点检测、校正算法、HAL接口封装

坏点校正,英文叫Defect Pixel Correction,简称DPC。这玩意儿在ISP里看着不起眼,但你要是忽略了它,后面整条pipeline都会跟着遭殃。我刚开始做ISP的时候,就吃过这个亏——有一批模组拍出来的照片,暗场下全是星星点点的白点,客户直接退货。后来一查,就是DPC没做好。

说白了,坏点就是传感器上那些“不听话”的像素。它们要么一直亮着(白点),要么一直黑着(黑点),要么响应不正常。咱们做DPC,就是要揪出这些坏点,然后用周围正常像素的值把它“填”上。

核心要点:坏点校正分为两步——先检测,再校正。检测不准,校正就是瞎搞。

4.1 静态坏点检测

静态坏点,就是出厂时就有的坏点。这些坏点的位置是固定的,咱们可以在产线上提前标定好。我个人习惯的做法是:在暗场和亮场下各拍一张图,然后对比像素的响应。

具体怎么标定?我列个流程:

  1. 暗场标定:盖上镜头盖,拍一张全黑图。正常像素输出应该接近0,坏点(白点)会输出明显的高值。
  2. 亮场标定:对着均匀光源,拍一张全白图。正常像素输出应该接近饱和值,坏点(黑点)会输出明显的低值。
  3. 生成坏点表:把检测到的坏点坐标存成一个表,烧到设备的OTP里。

这里有个坑,我曾经踩过:标定时的温度会影响坏点数量。有些像素在常温下是好的,一升温就变成坏点了。所以我建议,标定时至少做常温和高低温两组,取并集。

小技巧:坏点表不要只存坐标,最好存一个“坏点等级”。比如等级1是永久坏点,等级2是温度敏感型坏点。这样在运行时可以灵活处理。

4.2 动态坏点检测

动态坏点,就是使用过程中新产生的坏点。这类坏点没法提前标定,得靠算法实时检测。你想想看,手机用了一年半载,传感器老化,难免会冒出几个新坏点。

动态检测的核心思路是:比较当前像素和它邻居的差异。如果差异太大,就怀疑它是坏点。我常用的方法有两种:

4.2.1 基于邻域统计的方法

取当前像素周围3x3或5x5的窗口,计算窗口内像素的均值和中值。如果当前像素偏离中值超过某个阈值,就标记为坏点。

// 伪代码:基于中值的动态坏点检测
for each pixel (x, y) {
    window = get_neighbors(x, y, 5);  // 取5x5窗口
    median = compute_median(window);  // 计算中值
    diff = abs(pixel(x,y) - median);
    
    if (diff > threshold) {
        mark_as_defect(x, y);
    }
}

嗯,这里要注意:阈值不能是固定的。为什么?因为不同亮度的区域,像素的方差不一样。暗区方差小,亮区方差大。固定阈值会导致暗区误检多、亮区漏检多。

我个人习惯的做法是:阈值根据局部亮度自适应。比如 threshold = base_threshold + k * local_variance。

4.2.2 基于梯度的方法

这个方法更巧妙。它利用图像边缘的梯度一致性来判断。如果某个像素的梯度方向和周围不一致,那它很可能是个坏点。

我在项目中遇到过一种情况:细纹理区域容易被误检为坏点。比如草地、布料上的纹理,像素变化本来就大。这时候用梯度法就比统计法更靠谱,因为纹理是有规律的,而坏点是随机的。

警告:动态检测的阈值设置要留有余量。宁可漏检几个坏点,也不要误检太多好像素。误检会导致图像细节丢失,看起来像磨皮过度。

4.3 校正算法

检测到坏点之后,就要把它校正过来。校正的本质就是插值——用周围好像素的值来推算坏点本该有的值。

常用的校正方法有:

方法 原理 优缺点
均值插值 取周围像素的平均值 简单快速,但会模糊边缘
中值插值 取周围像素的中值 抗噪性好,适合孤立坏点
方向插值 沿边缘方向插值 保留边缘细节,计算量大
自适应插值 根据局部纹理选择插值方式 效果最好,但实现复杂

我个人最推荐的是方向插值。它先判断坏点所在区域的边缘方向(水平、垂直、对角),然后只沿着边缘方向取像素做插值。这样校正后的坏点不会破坏边缘的锐度。

// 方向插值示例
int interpolate_defect(int x, int y) {
    // 计算四个方向的梯度
    float grad_h = abs(pixel(x-1,y) - pixel(x+1,y));
    float grad_v = abs(pixel(x,y-1) - pixel(x,y+1));
    float grad_d1 = abs(pixel(x-1,y-1) - pixel(x+1,y+1));
    float grad_d2 = abs(pixel(x-1,y+1) - pixel(x+1,y-1));
    
    // 选择梯度最小的方向(即边缘方向)
    if (grad_h < grad_v && grad_h < grad_d1 && grad_h < grad_d2)
        return (pixel(x-1,y) + pixel(x+1,y)) / 2;
    else if (grad_v < grad_d1 && grad_v < grad_d2)
        return (pixel(x,y-1) + pixel(x,y+1)) / 2;
    else if (grad_d1 < grad_d2)
        return (pixel(x-1,y-1) + pixel(x+1,y+1)) / 2;
    else
        return (pixel(x-1,y+1) + pixel(x+1,y-1)) / 2;
}

4.4 HAL接口封装

好了,算法讲完了,咱们来看看怎么把它封装成HAL接口。做嵌入式系统架构的时候,我最看重的是接口的通用性和可配置性。你不能把算法写死,得让上层应用能灵活调整参数。

我设计的DPC HAL接口通常包含三个部分:

  • 初始化接口:加载静态坏点表,配置算法参数
  • 处理接口:传入一帧图像,输出校正后的图像
  • 控制接口:动态调整阈值、开关等参数
// DPC HAL接口定义
typedef struct {
    uint32_t *defect_table;      // 静态坏点表
    uint32_t  defect_count;      // 坏点数量
    uint8_t   enable_dynamic;    // 是否启用动态检测
    uint16_t  dynamic_threshold; // 动态检测阈值
    uint8_t   interpolation_mode; // 插值模式:0-均值,1-中值,2-方向
} dpc_config_t;

// 初始化DPC模块
int dpc_init(dpc_config_t *config);

// 处理一帧图像
int dpc_process(uint16_t *input, uint16_t *output, 
                int width, int height, int bpp);

// 动态调整参数
int dpc_set_param(uint32_t param_id, void *value);

这里有个设计细节我想强调一下:坏点表的内存管理。静态坏点表可能很大(几万个坐标),如果每次处理都从OTP读,效率太低了。我建议在初始化时把坏点表加载到DDR里,然后用一个bitmap来标记坏点位置。这样检测时只需要查bitmap,O(1)复杂度。

避坑指南:我曾经把坏点表设计成链表结构,结果每次查表都要遍历,帧率直接掉了一半。后来改成bitmap,速度提升了10倍。记住:ISP是实时处理,性能是第一位的。

4.5 知识体系总览

为了让你对整个DPC模块有个全局认识,我画了一张流程图。它展示了静态检测、动态检测和校正之间的协作关系。

DPC坏点校正模块架构 原始RAW图像 静态坏点检测 加载OTP坏点表 动态坏点检测 邻域统计/梯度分析 坏点位置标记 坏点校正 方向插值/中值插值 校正后RAW图像

从这张图你能看到,静态检测和动态检测是并行的。静态检测负责处理已知的坏点,动态检测负责捕捉新出现的坏点。两者标记的坏点位置合并后,统一送到校正模块处理。这样设计的好处是:静态检测不占用运行时资源,动态检测只处理可疑像素,整体效率很高。

好了,DPC这一章就讲到这里。记住我说的:坏点校正不是越强越好,关键是在去除坏点和保留细节之间找到平衡。调参的时候多看看暗场和边缘区域,这两个地方最容易出问题。

可可精品资料库