4. 坏点检测算法:基于邻域统计的DPC原理
坏点检测,说白了就是在一张图像里找出那些「不对劲」的像素。我刚开始做ISP的时候,觉得这玩意儿挺简单——不就是找几个亮点暗点嘛?后来被实际数据教育了一顿,才发现这里面的门道比想象中深得多。
今天咱们聊的,是业界最常用的方法:基于邻域统计的坏点检测。英文叫Defect Pixel Correction,简称DPC。这套思路不复杂,但很实用,我到现在做项目还经常用它打底。
4.1 什么是坏点?先定义清楚
在CMOS传感器上,坏点分三种:
| 类型 | 表现 | 成因 |
|---|---|---|
| 亮点 | 固定输出高值(接近饱和) | 暗电流过大、像素漏电 |
| 暗点 | 固定输出低值(接近0) | 光电二极管损坏、电路断路 |
| 闪烁点 | 输出值随机跳变 | 晶体管噪声异常、接触不良 |
你想想看,一个像素如果永远都是255或者永远都是0,那肯定有问题。但麻烦的是——有些坏点不是那么极端,它只是「偏亮」或「偏暗」,跟正常像素混在一起,很难一眼看出来。
4.2 邻域统计的核心思想
坏点检测的基本假设很简单:正常像素跟它周围的邻居长得差不多,坏点则不然。
举个例子。你拍一张均匀的灰卡,正常情况下,一个像素和它周围8个邻居的值应该很接近。但如果这个像素是坏点,它的值就会明显偏离邻居们的统计分布。
基于这个思路,DPC算法一般分三步走:
- 选邻域——确定用哪些邻居来做比较
- 算统计量——计算邻居的均值、中值、标准差等
- 做判决——判断当前像素是否偏离正常范围
核心公式(简化版):
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 中值
算邻居的「正常值」有两种主流方法:均值和 中值。我直接说结论——中值更靠谱。
原因很简单:均值对异常值敏感。如果你的邻域里恰好有另一个坏点,均值会被拉偏,导致误判。中值不一样,它只取排序后中间那个值,哪怕邻域里有一两个坏点,只要不超过半数,中值基本不受影响。
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
为什么会这样?因为噪声本身就会让像素值波动。噪声越大,正常像素和邻居的差异本来就大,阈值必须跟着放宽,否则会把噪声当成坏点。
4.6 完整检测流程(附SVG流程图)
下面这张图把整个DPC检测流程串起来了。你可以看到,从输入RAW图到输出坏点标记图,中间经过了邻域提取、统计计算、阈值判决三个核心环节。
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
4.8 常见问题与避坑
最后聊几个我踩过的坑,希望能帮你省点时间。
- 误检率太高? 先检查是不是噪声太大。如果是,考虑先做降噪再做DPC,或者把K值调大。
- 漏检率太高? 看看是不是坏点刚好在纹理边缘。纹理边缘的像素差异本来就大,容易被当成正常。我建议对纹理区域单独降低阈值。
- 坏点簇怎么处理? 单个坏点用邻域统计好办,但坏点簇里全是坏点,统计量就失效了。这种情况需要结合全局统计——比如整张图的均值、方差,或者用多帧检测。
- 实时性不够? 中值排序是瓶颈。可以考虑用「快速中值」算法(比如基于直方图的),或者干脆用均值+裁剪代替。我在一个4K@60fps的项目里就是这么干的,效果损失很小。
嗯,关于基于邻域统计的DPC原理,今天就聊到这儿。这套方法虽然基础,但确实是ISP里最常用的坏点检测手段。你把它吃透了,后面再学更高级的算法(比如基于机器学习的、基于多帧的)就会轻松很多。