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 自适应坏点检测与校正
这是目前工业界的主流方案。它结合了检测和校正,而且会根据图像内容动态调整参数。
自适应方案的核心思路是:
- 先检测,再校正——不是所有像素都做校正,只有被判定为坏点的才处理。
- 根据局部纹理选择校正方法——平坦区域用均值,边缘区域用方向插值。
- 阈值自适应——根据噪声水平动态调整检测阈值。
自适应校正伪代码:
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流水线中的位置,以及内部的处理逻辑。
4.7 工程实践中的几个坑
最后,分享几个我在项目中踩过的坑,希望能帮你们少走弯路。
坑一: 坏点检测阈值不能全局统一。不同增益下,噪声水平不一样。我建议根据ISO值或模拟增益动态调整阈值。增益越高,阈值越宽松。
坑二: 校正时不要把坏点周围的像素也当成坏点。我曾经见过一个算法,检测到一个坏点后,把它周围一圈像素也标记为「可疑」,结果越修越糟。记住:坏点是孤立的,不会成片出现。
坑三: 对于彩色传感器(Bayer格式),坏点校正要在去马赛克之前做。为什么?因为在RAW域,每个像素只记录一种颜色,坏点只影响一个通道。如果等到去马赛克之后再做,坏点会扩散到三个通道,修复难度大增。