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 动态坏点校正
静态坏点校正只能处理固定位置的坏点。但传感器的坏点会随着温度、曝光时间、增益变化而变化。这就是动态坏点校正要解决的问题。
动态校正的核心思路是:实时检测,实时替换。每一帧图像进来,都重新跑一遍检测算法,找出当前帧的坏点,然后替换。
流程如下:
- 读取当前帧:获取原始RAW数据。
- 坏点检测:用梯度法+邻域法检测坏点。
- 坏点标记:生成坏点掩膜(Bad Pixel Map)。
- 坏点替换:根据掩膜,对坏点进行插值替换。
- 输出校正帧:得到干净的图像数据。
动态校正的好处是自适应性强。比如在高温环境下,传感器暗电流增大,会出现大量临时亮点。静态校正的坏点表里没有这些点,但动态校正能实时发现并处理。
性能考量:动态校正的计算量比较大。在嵌入式平台上,我建议只对图像中的“可疑区域”做检测,而不是全图扫描。比如,先做一次快速梯度检测,只对梯度异常的区域做邻域法二次确认。
4.5 知识体系图
下面这张图,把坏点校正的整个流程串起来了。你可以把它当作一个快速参考。
4.6 避坑指南
做坏点校正这么多年,我踩过不少坑。分享几个典型的:
- 阈值设置不当:阈值设得太低,会把正常像素误判为坏点;设得太高,又会漏掉真正的坏点。我建议先用一张均匀光照的灰卡图像做标定,统计像素值的分布,再根据标准差设定阈值。
- 忽略Bayer格式:RAW图像是Bayer格式的,不同通道的响应特性不同。检测坏点时,一定要在同通道内比较,不能跨通道。否则,红色通道的像素和绿色通道的像素比,那肯定有差异。
- 替换后出现条纹:如果坏点比较密集,替换后可能会在图像上留下条纹或块状痕迹。这时候,我建议用更大范围的邻域(比如5x5)做插值,或者改用更复杂的插值算法(如边缘导向插值)。
曾经有一次,我在一个项目中用了静态坏点表,结果产品在高温环境下出现了大量亮点。排查了半天才发现,是温度升高导致传感器产生了新的坏点。从那以后,我坚持在量产前做高低温老化测试,并启用动态坏点校正。
4.7 小结
坏点校正看似简单,但要做好并不容易。它需要你理解传感器的物理特性,掌握检测算法的原理,还要根据实际场景调整参数。
我个人觉得,动态坏点校正是未来的趋势。随着传感器分辨率越来越高,坏点数量也会增加。只有实时检测、实时替换,才能保证图像质量始终如一。