3、黑电平校正:把传感器的“底噪”先清掉
黑电平校正,英文叫 Black Level Correction,简称 BLC。说白了,就是要把传感器在“全黑”状态下还输出的那部分信号给减掉。你想想看,一个像素在完全没有光照时,理想输出应该是 0,对吧?但现实不是这样的。
我刚开始做 ISP 时,拿到一张暗部发灰的图,还以为是镜头脏了。后来才发现,是黑电平没校好。嗯,这节课我们就把它彻底讲透。
3.1 黑电平的来源:传感器暗电流
为什么全黑时传感器还有输出?主要有两个原因:
- 暗电流 (Dark Current):硅基半导体在热激发下会产生电子-空穴对,即使没有光子入射,电荷也在积累。温度每升高 6-8°C,暗电流翻一倍。
- 像素电路偏移:读出电路、放大器、ADC 都有固定的直流偏置,这部分也会叠加到输出上。
我在项目中遇到过一款工业相机,夏天户外工作时暗电流飙到正常值的 3 倍。如果不做黑电平校正,整张图就像蒙了一层灰雾,暗部细节全没了。
关键指标:黑电平值通常用 ADC 码值表示。12-bit RAW 中,典型黑电平在 64-256 之间;14-bit 则在 256-1024 之间。不同传感器、不同增益、不同温度下,这个值都会变。
3.2 黑电平校正原理
校正原理其实很简单:
校正后像素值 = 原始像素值 - 黑电平值
但这里有个坑——减完以后可能出现负值。所以实际工程中会做截断:
校正后像素值 = max(0, 原始像素值 - 黑电平值)
嗯,这里要注意:有些传感器还会在 RAW 数据中预留一个“黑电平偏移”,让你减完后还能保留一点暗部层次。我习惯把这个偏移量叫做“黑电平保留值”,一般设 8-16 个码值就够了。
避坑指南:我曾经在某个项目中直接把黑电平减到 0,结果暗部出现大量死黑像素,细节全丢了。后来加了 8 个码值的保留量,暗部过渡才自然起来。
3.3 全局 vs 局部黑电平校正
黑电平校正有两种主流方法:全局校正和局部校正。我整理了一个对比表,方便你理解:
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全局校正 | 整张图减去同一个黑电平值 | 实现简单,计算量小 | 无法处理行间/列间差异 | 低端传感器、固定环境 |
| 局部校正 | 每行/每列/每块使用不同黑电平 | 精度高,能消除行噪声 | 需要额外存储 OB 像素数据 | 高动态、高帧率、工业相机 |
全局校正适合快速验证。但如果你做的是高端相机,局部校正几乎是必须的。我记得有一次调试一款全局快门传感器,每行读出时间不同,暗电流积累也不一样,用全局校正怎么调都有竖条纹。换成逐行校正后,问题立刻解决。
3.4 黑电平校正的完整流程
下面这张 SVG 流程图,展示了从 RAW 输入到校正输出的完整链路:
3.5 代码实现:从全局到局部
下面我给出两段代码。第一段是全局校正,适合快速上手;第二段是逐行局部校正,适合工程落地。
3.5.1 全局黑电平校正
import numpy as np
def global_black_level_correction(raw_img, black_level, offset=8):
"""
全局黑电平校正
:param raw_img: 原始 RAW 图像 (H, W),uint16
:param black_level: 全局黑电平值,int
:param offset: 保留的暗部偏移量,int
:return: 校正后图像
"""
corrected = raw_img.astype(np.int32)
corrected -= black_level
corrected += offset
corrected = np.clip(corrected, 0, 65535)
return corrected.astype(np.uint16)
3.5.2 逐行局部黑电平校正
def row_wise_black_level_correction(raw_img, ob_cols=16, offset=8):
"""
逐行黑电平校正(利用每行左侧 OB 像素)
:param raw_img: 原始 RAW 图像 (H, W),uint16
:param ob_cols: 每行左侧 OB 像素列数
:param offset: 保留的暗部偏移量
:return: 校正后图像
"""
h, w = raw_img.shape
corrected = np.zeros_like(raw_img, dtype=np.uint16)
for row in range(h):
# 提取该行左侧 OB 像素
ob_pixels = raw_img[row, :ob_cols]
# 计算该行黑电平(用中值更鲁棒)
row_black = np.median(ob_pixels)
# 校正该行
row_corrected = raw_img[row, ob_cols:].astype(np.int32)
row_corrected -= row_black
row_corrected += offset
row_corrected = np.clip(row_corrected, 0, 65535)
corrected[row, ob_cols:] = row_corrected.astype(np.uint16)
return corrected
我的经验:用中值代替均值计算黑电平,能有效避免 OB 像素中个别坏点的影响。我曾经在一条产线上发现某行 OB 区有个死像素,用均值算出来的黑电平偏了 20 个码值,整行都偏色。换成中值后,问题消失。
3.6 工程中的几个关键参数
实际项目中,黑电平校正还涉及几个可调参数。我列了一个表,方便你调试时参考:
| 参数 | 典型值 | 调大影响 | 调小影响 |
|---|---|---|---|
| 黑电平值 | 64-256 (12-bit) | 暗部变暗,细节丢失 | 暗部发灰,对比度下降 |
| 保留偏移 | 8-16 | 暗部噪点增多 | 暗部出现死黑 |
| OB 像素列数 | 8-32 | 黑电平估计更稳 | 易受单点噪声影响 |
避坑指南:我曾经在调试一款 4K 传感器时,OB 像素只用了 4 列,结果每行黑电平波动很大,校正后出现横条纹。后来把 OB 列数增加到 16 列,横条纹立刻消失。记住,OB 像素越多,统计越稳定,但有效成像区域会变小,需要平衡。