3、坏点校正(DPC):坏点类型、静态坏点校正、动态坏点校正

坏点校正,简称 DPC。说白了,就是处理传感器上那些「不听话」的像素点。

我刚入行那会儿,拿到第一块 raw 图,满屏的亮点和暗点,差点以为 sensor 坏了。后来才知道,这是每个 CMOS 都有的先天毛病。你想想看,几千万个像素点,怎么可能个个都完美?

3.1 坏点的类型

坏点分三种,我习惯按它们的「性格」来记:

类型 表现 成因
亮点(Hot Pixel) 固定输出高值,像一直亮着的小灯泡 暗电流过大,漏电严重
暗点(Dead Pixel) 固定输出低值,像永远关着的灯 光电二极管损坏,完全不感光
闪烁点(Stuck Pixel) 输出值卡在某个中间值,不随光照变化 读出电路故障,信号被「卡住」了
经验之谈: 我遇到过最头疼的是闪烁点。亮点暗点一眼就能看出来,但闪烁点跟正常像素混在一起,不仔细看根本发现不了。尤其是卡在中间灰阶的那种,拍出来的照片像蒙了一层雾。

3.2 静态坏点校正

静态校正,就是提前把坏点的位置记下来,然后直接替换掉。

怎么做?分两步:

  1. 坏点标定: 在产线上,用均匀光源拍一张全黑图和一张全白图。全黑图里亮的就是亮点,全白图里暗的就是暗点。把坐标存进 OTP 或 flash 里。
  2. 坏点替换: 读 raw 图时,遇到这些坐标,就用周围像素插值替换掉。

替换算法有很多种,我常用的是中值滤波和均值滤波:

// 简单的 3x3 中值替换
uint16_t dpc_static_correct(uint16_t *raw, int x, int y, int width) {
    int idx = y * width + x;
    uint16_t neighbors[8];
    int n = 0;

    // 收集周围 8 个像素
    for (int dy = -1; dy <= 1; dy++) {
        for (int dx = -1; dx <= 1; dx++) {
            if (dx == 0 && dy == 0) continue;
            neighbors[n++] = raw[(y + dy) * width + (x + dx)];
        }
    }

    // 排序取中值
    for (int i = 0; i < 7; i++) {
        for (int j = i + 1; j < 8; j++) {
            if (neighbors[i] > neighbors[j]) {
                uint16_t tmp = neighbors[i];
                neighbors[i] = neighbors[j];
                neighbors[j] = tmp;
            }
        }
    }

    return neighbors[4]; // 返回中值
}
注意: 静态校正有个坑——坏点表是出厂时标定的。传感器用久了,新的坏点会冒出来。我曾经遇到一台设备,用了半年后画面出现大量噪点,查了半天才发现是新增的坏点没被覆盖到。

3.3 动态坏点校正

动态校正,就是不依赖预存坏点表,实时检测并修复坏点。

为什么需要动态校正?原因有三:

  • 坏点会随着温度和老化增加
  • 产线标定不可能覆盖所有场景
  • 有些坏点只在特定增益下才显现

动态校正的核心思路:判断当前像素是否「异常」。

我常用的判断方法是:

  1. 取当前像素周围 3x3 或 5x5 邻域
  2. 计算邻域内像素的均值或中值
  3. 计算当前像素与均值的差值
  4. 如果差值超过阈值,就判定为坏点
// 动态坏点检测与校正
uint16_t dpc_dynamic_correct(uint16_t *raw, int x, int y, int width, int threshold) {
    int idx = y * width + x;
    uint16_t center = raw[idx];

    // 计算 3x3 邻域均值(排除中心点)
    uint32_t sum = 0;
    int count = 0;
    for (int dy = -1; dy <= 1; dy++) {
        for (int dx = -1; dx <= 1; dx++) {
            if (dx == 0 && dy == 0) continue;
            sum += raw[(y + dy) * width + (x + dx)];
            count++;
        }
    }
    uint16_t mean = sum / count;

    // 判断差值是否超过阈值
    int diff = abs((int)center - (int)mean);
    if (diff > threshold) {
        return mean;  // 坏点,用均值替换
    }

    return center;  // 正常点,保持不变
}
关键参数: 阈值怎么设?设小了容易误判,把纹理细节当成坏点;设大了又漏检。我一般会根据 sensor 的噪声水平动态调整阈值,暗光下阈值放大,亮光下阈值收窄。

3.4 静态 vs 动态:怎么选?

两种方法各有优劣,我整理了一张对比表:

对比项 静态校正 动态校正
计算量 低(只需查表替换) 高(需要实时检测)
坏点覆盖 只覆盖标定过的 能覆盖新增坏点
误判风险 无(坐标是确定的) 有(可能误判纹理)
适用场景 产线标定、固定坏点 温度变化、老化场景

实际项目中,我通常两种一起用。先用静态校正处理已知坏点,再用动态校正兜底。这样既保证了效率,又覆盖了新增坏点。

3.5 坏点校正的整体流程

嗯,这里我把整个 DPC 的流程画了张图,方便你理解:

坏点校正(DPC)整体流程 输入 Raw 图 是否有预存坏点表? 是 静态坏点校正 否 动态坏点校正 输出校正后 Raw 图 实际项目中,静态校正和动态校正通常串联使用,先修已知坏点,再查新增坏点
我的建议: 如果你刚开始做 ISP,先从静态校正入手。把坏点表标定好,能解决 80% 的问题。等后面遇到温度漂移或老化问题了,再上动态校正。别一上来就想搞大而全,容易把自己绕进去。