4. 坏点校正

各位同学,欢迎来到第四讲。今天我们来聊聊一个让所有ISP工程师又爱又恨的话题——坏点校正。

为什么说又爱又恨?因为坏点这东西,你不管它,图像上就会出现星星点点的白斑或黑斑,特别影响观感。但你要是管得太狠,又容易把正常的细节给抹掉。我在刚入行那会儿,就吃过这个亏。

4.1 坏点到底是什么?

说白了,坏点就是传感器上那些「不听话」的像素。它们要么永远亮着,要么永远暗着,要么对光线变化毫无反应。

从成因上分,坏点主要有两类:

类型 成因 表现 能否修复
静态坏点 制造缺陷、物理损伤 固定位置、固定亮度 可通过标定消除
动态坏点 温度、增益、老化 时有时无、亮度变化 需要实时检测

静态坏点好办,出厂前标定一次,做个坏点列表,每次处理时直接替换就行。但动态坏点就麻烦了——你永远不知道它什么时候冒出来。

关键认知: 坏点校正不是「消除」缺陷,而是「掩盖」缺陷。我们用周围正常像素的信息去「猜」坏点本该是什么值。猜得好,图像干净;猜得不好,反而引入伪影。

4.2 静态坏点检测:一劳永逸的标定法

静态坏点检测,我习惯在暗场和亮场两种条件下做。

暗场标定: 盖上镜头盖,拍一张全黑图像。正常像素输出应该接近0,但坏点会明显偏亮。设定一个阈值,超过的就是坏点。

亮场标定: 对着均匀光源,拍一张中灰图像。正常像素输出应该接近平均值,偏离太多的就是坏点。

两种方法结合,基本能把静态坏点一网打尽。

我的经验: 标定不是一次性的。传感器用久了,坏点会增多。我建议每半年重新标定一次,尤其是用在安防或工业场景的相机。

4.3 动态坏点检测:实时战场

动态坏点检测,说白了就是在图像处理流水线里实时判断每个像素是不是坏了。这比静态检测难得多,因为你没有参考图像。

常用的方法有几种:

  • 邻域比较法: 看当前像素和周围8个邻居的差异。如果差异太大,就怀疑是坏点。
  • 方向梯度法: 分别计算水平、垂直、两个对角线方向的梯度。如果某个方向梯度异常大,可能是坏点。
  • 统计法: 计算局部区域的均值和标准差,用3σ原则判断。

我个人比较喜欢方向梯度法。为什么?因为它能区分「边缘」和「坏点」。你想想看,一个正常的边缘像素,它和邻居的差异也很大,但只在垂直边缘的方向上大。而坏点呢?它在所有方向上差异都大。这个区别很关键。

避坑指南: 我曾经在一个项目中,动态坏点检测阈值设得太低,结果把星空照片里的星星全当坏点给抹掉了。后来我加了一个「亮度保护」机制——如果当前像素亮度超过某个上限,就不做坏点检测。因为星星和坏点,在亮度上其实很像,但星星周围通常有渐变的过渡,坏点没有。

4.4 坏点校正方法:三种主流方案

检测到坏点之后,怎么修?这里有三种常用方法。

4.4.1 中值滤波法

这是最经典的方法。把坏点周围的像素值排序,取中间那个值来替换坏点。

// 3x3邻域中值滤波
uint16_t median_filter(uint16_t *neighbors, int count) {
    // 排序
    for (int i = 0; i < count - 1; i++) {
        for (int j = 0; j < count - i - 1; j++) {
            if (neighbors[j] > neighbors[j + 1]) {
                uint16_t temp = neighbors[j];
                neighbors[j] = neighbors[j + 1];
                neighbors[j + 1] = temp;
            }
        }
    }
    // 取中值
    return neighbors[count / 2];
}

中值滤波的好处是抗噪能力强,不会受单个异常值影响。但缺点也很明显——它会把细线条也抹掉。如果坏点正好在一条细线上,中值滤波会把这条线打断。

4.4.2 均值替代法

这个方法更简单:取周围像素的平均值来替换坏点。

// 均值替代
uint16_t mean_replace(uint16_t *neighbors, int count) {
    uint32_t sum = 0;
    for (int i = 0; i < count; i++) {
        sum += neighbors[i];
    }
    return (uint16_t)(sum / count);
}

均值替代计算快,但容易受噪声影响。如果周围像素里恰好也有坏点,那结果就偏了。

4.4.3 自适应坏点检测与校正

这是目前工业界的主流方案。它结合了检测和校正,而且会根据图像内容动态调整参数。

自适应方案的核心思路是:

  1. 先检测,再校正——不是所有像素都做校正,只有被判定为坏点的才处理。
  2. 根据局部纹理选择校正方法——平坦区域用均值,边缘区域用方向插值。
  3. 阈值自适应——根据噪声水平动态调整检测阈值。

自适应校正伪代码:

for each pixel (x, y):
    // 1. 计算局部梯度
    grad_h = |I(x,y) - I(x-1,y)| + |I(x,y) - I(x+1,y)|
    grad_v = |I(x,y) - I(x,y-1)| + |I(x,y) - I(x,y+1)|
    grad_d1 = |I(x,y) - I(x-1,y-1)| + |I(x,y) - I(x+1,y+1)|
    grad_d2 = |I(x,y) - I(x-1,y+1)| + |I(x,y) - I(x+1,y-1)|
    
    // 2. 判断是否为坏点
    min_grad = min(grad_h, grad_v, grad_d1, grad_d2)
    if min_grad > threshold:
        // 3. 选择校正方向
        if grad_h is smallest:
            pixel = (I(x-1,y) + I(x+1,y)) / 2
        else if grad_v is smallest:
            pixel = (I(x,y-1) + I(x,y+1)) / 2
        else:
            pixel = median_filter(neighbors)

你看,自适应方案的核心就是「看情况办事」。平坦区域用均值,边缘区域沿着边缘方向插值,复杂纹理区域用中值保边。这样既去掉了坏点,又保住了细节。

4.5 三种方法对比

方法 计算量 保边能力 抗噪性 适用场景
中值滤波法 中 中 强 通用场景,噪声较大时
均值替代法 低 弱 弱 平坦区域,实时性要求高
自适应法 高 强 强 高质量要求,边缘保留重要

4.6 坏点校正的完整流程

下面这张图展示了坏点校正在整个ISP流水线中的位置,以及内部的处理逻辑。

坏点校正完整流程 RAW 图像输入 是否有坏点标定表? 是 静态坏点替换(查表) 否 动态坏点实时检测 选择校正方法 中值滤波法 均值替代法 自适应校正法 校正后 RAW 输出 静态坏点:出厂标定,查表替换 动态坏点:实时检测,自适应校正

4.7 工程实践中的几个坑

最后,分享几个我在项目中踩过的坑,希望能帮你们少走弯路。

坑一: 坏点检测阈值不能全局统一。不同增益下,噪声水平不一样。我建议根据ISO值或模拟增益动态调整阈值。增益越高,阈值越宽松。

坑二: 校正时不要把坏点周围的像素也当成坏点。我曾经见过一个算法,检测到一个坏点后,把它周围一圈像素也标记为「可疑」,结果越修越糟。记住:坏点是孤立的,不会成片出现。

坑三: 对于彩色传感器(Bayer格式),坏点校正要在去马赛克之前做。为什么?因为在RAW域,每个像素只记录一种颜色,坏点只影响一个通道。如果等到去马赛克之后再做,坏点会扩散到三个通道,修复难度大增。