4. 坏点校正:坏点检测算法与动态校正

坏点校正,说白了就是给传感器“排雷”。

我刚开始做ISP调试时,最头疼的就是坏点。明明镜头、光源都没问题,拍出来的图像上总有那么几个像素“不听话”——要么死亮,要么死黑。后来我才明白,这是传感器的先天缺陷,每个传感器都有,只是多少的问题。

这一章,我们就来聊聊怎么把这些“坏分子”揪出来,再想办法补上。

4.1 坏点是什么?

坏点,就是传感器阵列中响应异常的像素。它们通常分为三类:

类型 表现 成因
亮点(Hot Pixel) 固定输出高值,像个小太阳 暗电流过大,漏电
暗点(Dead Pixel) 固定输出低值,像个小黑洞 光电二极管损坏
闪烁点(Stuck Pixel) 输出值随机跳变,忽明忽暗 电路噪声或接触不良

你想想看,如果一张照片上有个别像素永远亮着或暗着,那得多别扭。尤其是在做色彩校正之前,坏点会严重干扰后续的统计和计算。所以,坏点校正必须放在ISP流水线的最前面。

核心原则:坏点校正不是“修复”,而是“掩盖”。我们无法让坏掉的像素恢复,只能用周围正常像素的值去“猜”它本该是什么样子。

4.2 坏点检测算法

检测坏点,常用的方法有两种:梯度法和邻域法。我个人习惯把两者结合使用,效果更稳。

4.2.1 梯度法

梯度法的思路很简单:如果一个像素是坏点,它和周围像素的差异会特别大。我们计算该像素在水平和垂直方向上的梯度,如果梯度值超过某个阈值,就判定为坏点。

公式长这样:

G_h = |P(x,y) - P(x-1,y)| + |P(x,y) - P(x+1,y)|
G_v = |P(x,y) - P(x,y-1)| + |P(x,y) - P(x,y+1)|
if G_h > T_h or G_v > T_v: 标记为坏点

我在项目中遇到过一个问题:梯度法对边缘很敏感。如果图像本身就有高对比度的边缘(比如灯管边缘),梯度法容易误判。所以,我通常会加一个“边缘保护”逻辑——先判断当前像素是否在边缘区域,如果是,就放宽阈值。

4.2.2 邻域法

邻域法更直观。我们取当前像素周围一圈(3x3或5x5邻域)的像素值,计算它们的均值或中值。如果当前像素值和这个统计值相差太大,就判定为坏点。

def detect_bad_pixel(img, x, y, threshold=0.15):
    # 取3x3邻域
    neighbors = img[x-1:x+2, y-1:y+2].flatten()
    center = img[x, y]
    median_val = np.median(neighbors)
    
    # 计算相对偏差
    if abs(center - median_val) / (median_val + 1e-6) > threshold:
        return True
    return False

邻域法的好处是稳定,不容易受边缘干扰。但缺点也很明显——计算量大。每个像素都要算一次中值,在嵌入式平台上跑起来有点吃力。

我的经验:在实际项目中,我会先用梯度法做快速筛选,再用邻域法做二次确认。这样既保证了速度,又提高了准确率。

4.3 坏点替换与插值

检测到坏点之后,下一步就是把它替换掉。替换的方法有很多,我挑几个常用的说说。

4.3.1 中值替换

最简单的方法:用邻域像素的中值替换坏点。中值对噪声不敏感,效果比较稳健。

img[x, y] = np.median(neighbors)

4.3.2 均值替换

用邻域像素的均值替换。均值计算快,但容易受其他坏点影响。如果邻域里还有坏点,均值就会被带偏。

4.3.3 双线性插值

对于坏点,我们可以用周围四个方向(上、下、左、右)的像素做双线性插值。这种方法能保留更多的细节,尤其适合纹理丰富的区域。

def bilinear_replace(img, x, y):
    # 取四个方向的正常像素
    top = img[x-1, y] if not is_bad(img[x-1, y]) else 0
    bottom = img[x+1, y] if not is_bad(img[x+1, y]) else 0
    left = img[x, y-1] if not is_bad(img[x, y-1]) else 0
    right = img[x, y+1] if not is_bad(img[x, y+1]) else 0
    
    count = (top > 0) + (bottom > 0) + (left > 0) + (right > 0)
    if count > 0:
        img[x, y] = (top + bottom + left + right) / count

注意:替换时一定要避免“二次污染”。如果邻域里也有坏点,千万别把它们算进去。我建议在替换前先标记所有坏点,然后统一处理。

4.4 动态坏点校正

静态坏点校正只能处理固定位置的坏点。但传感器的坏点会随着温度、曝光时间、增益变化而变化。这就是动态坏点校正要解决的问题。

动态校正的核心思路是:实时检测,实时替换。每一帧图像进来,都重新跑一遍检测算法,找出当前帧的坏点,然后替换。

流程如下:

  1. 读取当前帧:获取原始RAW数据。
  2. 坏点检测:用梯度法+邻域法检测坏点。
  3. 坏点标记:生成坏点掩膜(Bad Pixel Map)。
  4. 坏点替换:根据掩膜,对坏点进行插值替换。
  5. 输出校正帧:得到干净的图像数据。

动态校正的好处是自适应性强。比如在高温环境下,传感器暗电流增大,会出现大量临时亮点。静态校正的坏点表里没有这些点,但动态校正能实时发现并处理。

性能考量:动态校正的计算量比较大。在嵌入式平台上,我建议只对图像中的“可疑区域”做检测,而不是全图扫描。比如,先做一次快速梯度检测,只对梯度异常的区域做邻域法二次确认。

4.5 知识体系图

下面这张图,把坏点校正的整个流程串起来了。你可以把它当作一个快速参考。

坏点校正知识体系 原始RAW图像 坏点检测 梯度法 + 邻域法 是坏点? 是 坏点替换/插值 否 保留原值 校正后图像 动态校正:每帧都跑 一遍检测+替换流程

4.6 避坑指南

做坏点校正这么多年,我踩过不少坑。分享几个典型的:

  • 阈值设置不当:阈值设得太低,会把正常像素误判为坏点;设得太高,又会漏掉真正的坏点。我建议先用一张均匀光照的灰卡图像做标定,统计像素值的分布,再根据标准差设定阈值。
  • 忽略Bayer格式:RAW图像是Bayer格式的,不同通道的响应特性不同。检测坏点时,一定要在同通道内比较,不能跨通道。否则,红色通道的像素和绿色通道的像素比,那肯定有差异。
  • 替换后出现条纹:如果坏点比较密集,替换后可能会在图像上留下条纹或块状痕迹。这时候,我建议用更大范围的邻域(比如5x5)做插值,或者改用更复杂的插值算法(如边缘导向插值)。

曾经有一次,我在一个项目中用了静态坏点表,结果产品在高温环境下出现了大量亮点。排查了半天才发现,是温度升高导致传感器产生了新的坏点。从那以后,我坚持在量产前做高低温老化测试,并启用动态坏点校正。

4.7 小结

坏点校正看似简单,但要做好并不容易。它需要你理解传感器的物理特性,掌握检测算法的原理,还要根据实际场景调整参数。

我个人觉得,动态坏点校正是未来的趋势。随着传感器分辨率越来越高,坏点数量也会增加。只有实时检测、实时替换,才能保证图像质量始终如一。