4. 坏点检测算法:基于邻域统计的DPC原理

坏点检测,说白了就是在一张图像里找出那些「不对劲」的像素。我刚开始做ISP的时候,觉得这玩意儿挺简单——不就是找几个亮点暗点嘛?后来被实际数据教育了一顿,才发现这里面的门道比想象中深得多。

今天咱们聊的,是业界最常用的方法:基于邻域统计的坏点检测。英文叫Defect Pixel Correction,简称DPC。这套思路不复杂,但很实用,我到现在做项目还经常用它打底。

4.1 什么是坏点?先定义清楚

在CMOS传感器上,坏点分三种:

类型 表现 成因
亮点 固定输出高值(接近饱和) 暗电流过大、像素漏电
暗点 固定输出低值(接近0) 光电二极管损坏、电路断路
闪烁点 输出值随机跳变 晶体管噪声异常、接触不良

你想想看,一个像素如果永远都是255或者永远都是0,那肯定有问题。但麻烦的是——有些坏点不是那么极端,它只是「偏亮」或「偏暗」,跟正常像素混在一起,很难一眼看出来。

我的经验: 实际项目中,最头疼的不是全亮全暗的坏点,而是那些「半死不活」的——输出值比正常像素高20%但没饱和,这种用简单阈值根本抓不住。

4.2 邻域统计的核心思想

坏点检测的基本假设很简单:正常像素跟它周围的邻居长得差不多,坏点则不然。

举个例子。你拍一张均匀的灰卡,正常情况下,一个像素和它周围8个邻居的值应该很接近。但如果这个像素是坏点,它的值就会明显偏离邻居们的统计分布。

基于这个思路,DPC算法一般分三步走:

  1. 选邻域——确定用哪些邻居来做比较
  2. 算统计量——计算邻居的均值、中值、标准差等
  3. 做判决——判断当前像素是否偏离正常范围

核心公式(简化版):

if |P_center - median(neighbors)| > K * sigma(neighbors):
    标记为坏点
else:
    正常像素

其中K是阈值系数,sigma是邻域标准差。K越大,检测越宽松;K越小,越容易误检。

4.3 邻域怎么选?三种常见模式

邻域的选择直接影响检测效果。我见过不少新手一上来就用3x3窗口,结果边缘纹理全被当成坏点。这里我总结三种常用模式:

模式 窗口大小 适用场景 优缺点
十字邻域 3x3十字(上下左右) Bayer RAW图 避免跨颜色通道比较,但信息量少
方形邻域 3x3或5x5全窗口 RGB/YUV图 信息丰富,但容易受纹理干扰
自适应邻域 动态调整 高噪声场景 效果好,但计算量大

我个人习惯在Bayer域用十字邻域。为什么?因为Bayer图里每个像素只对应一个颜色通道,你用方形窗口会把不同颜色的值混在一起算,那统计量就乱了。十字邻域只取同色通道的邻居,物理意义更清晰。

4.4 统计量怎么选?均值 vs 中值

算邻居的「正常值」有两种主流方法:均值和 中值。我直接说结论——中值更靠谱。

原因很简单:均值对异常值敏感。如果你的邻域里恰好有另一个坏点,均值会被拉偏,导致误判。中值不一样,它只取排序后中间那个值,哪怕邻域里有一两个坏点,只要不超过半数,中值基本不受影响。

注意: 中值排序在硬件实现上比较费资源。如果你在做FPGA或ASIC,可以考虑用「近似中值」——比如只取4个方向上的值排序,或者用均值+裁剪的折中方案。我曾经在一个低功耗项目里用过均值+标准差裁剪,效果接近中值,但资源省了一半。

4.5 判决阈值怎么定?

阈值K是DPC算法里最敏感的参数。K设大了,坏点漏检;K设小了,好点被误杀。我见过一个项目,就因为K设得太小,把星空照片里的星星全当坏点给抹掉了——那叫一个惨。

这里给几个经验值:

  • 低噪声场景(ISO 100-400): K = 2.0 ~ 3.0
  • 中等噪声场景(ISO 800-1600): K = 3.0 ~ 4.5
  • 高噪声场景(ISO 3200+): K = 4.5 ~ 6.0

为什么会这样?因为噪声本身就会让像素值波动。噪声越大,正常像素和邻居的差异本来就大,阈值必须跟着放宽,否则会把噪声当成坏点。

避坑指南: 我曾经在一个手机项目里发现,同样的K值在实验室暗室环境下跑得好好的,一到户外强光下就疯狂误检。后来查了半天,发现是镜头阴影校正(LSC)没做好,边缘亮度下降导致统计量偏差。所以DPC最好放在LSC之后做,或者至少把LSC的增益考虑进阈值计算里。

4.6 完整检测流程(附SVG流程图)

下面这张图把整个DPC检测流程串起来了。你可以看到,从输入RAW图到输出坏点标记图,中间经过了邻域提取、统计计算、阈值判决三个核心环节。

DPC坏点检测流程图 输入RAW图 步骤1:提取邻域窗口 步骤2:计算中值+标准差 步骤3:阈值判决 输出坏点标记图 关键参数 • 邻域大小:3x3 / 5x5 • 统计量:中值 / 均值 • 阈值K:2.0 ~ 6.0 输出格式 • 二值图:0=正常,1=坏点 • 或置信度图:0~255 • 坐标列表:用于后续修复

4.7 代码实现:一个最小可用的DPC检测器

光说不练假把式。下面给一个Python实现,用3x3十字邻域 + 中值 + 标准差判决。代码不长,但核心逻辑都在里面了。

import numpy as np

def dpc_detect(raw_img, k=3.0):
    """
    基于邻域统计的坏点检测
    raw_img: 单通道RAW图 (H, W)
    k: 阈值系数
    返回: 坏点标记图 (H, W), 0=正常, 1=坏点
    """
    h, w = raw_img.shape
    defect_map = np.zeros_like(raw_img, dtype=np.uint8)
    
    # 边界处理:复制边缘像素
    padded = np.pad(raw_img, pad_width=1, mode='edge')
    
    for i in range(1, h+1):
        for j in range(1, w+1):
            center = padded[i, j]
            
            # 提取十字邻域(上下左右)
            neighbors = [
                padded[i-1, j],   # 上
                padded[i+1, j],   # 下
                padded[i, j-1],   # 左
                padded[i, j+1]    # 右
            ]
            
            # 计算中值和标准差
            median_val = np.median(neighbors)
            std_val = np.std(neighbors)
            
            # 判决
            if std_val > 0 and abs(center - median_val) > k * std_val:
                defect_map[i-1, j-1] = 1
    
    return defect_map
使用建议: 这段代码适合做原型验证。实际工程中,我会加上「坏点簇检测」——如果连续3x3区域里坏点超过3个,说明可能是坏点簇,需要特殊处理。另外,边界处理用edge padding比zero padding好,因为边缘像素本身值就低,用0填充会导致误检。

4.8 常见问题与避坑

最后聊几个我踩过的坑,希望能帮你省点时间。

  • 误检率太高? 先检查是不是噪声太大。如果是,考虑先做降噪再做DPC,或者把K值调大。
  • 漏检率太高? 看看是不是坏点刚好在纹理边缘。纹理边缘的像素差异本来就大,容易被当成正常。我建议对纹理区域单独降低阈值。
  • 坏点簇怎么处理? 单个坏点用邻域统计好办,但坏点簇里全是坏点,统计量就失效了。这种情况需要结合全局统计——比如整张图的均值、方差,或者用多帧检测。
  • 实时性不够? 中值排序是瓶颈。可以考虑用「快速中值」算法(比如基于直方图的),或者干脆用均值+裁剪代替。我在一个4K@60fps的项目里就是这么干的,效果损失很小。
重要提醒: DPC检测只是第一步,检测出来的坏点怎么修,是另一个大话题。下一章我们会详细讲坏点修复的各种方法——插值、滤波、多帧融合等等。但记住一个原则:检测宁可漏检,不要误检。误检会把好像素修坏,造成不可逆的画质损失。漏检至少还能保留原始信息。

嗯,关于基于邻域统计的DPC原理,今天就聊到这儿。这套方法虽然基础,但确实是ISP里最常用的坏点检测手段。你把它吃透了,后面再学更高级的算法(比如基于机器学习的、基于多帧的)就会轻松很多。