2. 黑电平校正
任务目标
对RAW图像执行黑电平校正(Black Level Correction),消除传感器暗电流导致的固定基底噪声,恢复暗部区域的真实信号,为后续去马赛克、白平衡等ISP模块提供无偏置的RAW数据。
核心原理
CMOS/CCD传感器在无光照时仍会输出非零信号,称为黑电平(Black Level)。该值由传感器暗电流与读出电路偏置共同决定,通常以RAW码值表示(如64/1023或128/4095)。校正公式为:
校正后像素 = max(原始像素 - 黑电平, 0)
黑电平可来自传感器寄存器配置(全局标量),也可来自OB(Optical Black)行/列统计(逐行/逐列矢量)。本案例实现全局标量与逐行矢量两种校正模式。
环境依赖
- Python 3.8+,仅需NumPy库(
pip install numpy) - 输入:10bit RAW数据,以uint16存储(0~1023有效范围)
完整实现
import numpy as np
def black_level_correction(raw_img: np.ndarray,
black_level: int | np.ndarray,
mode: str = 'global') -> np.ndarray:
"""
RAW图像黑电平校正
Args:
raw_img: 输入RAW图,shape=(H, W),uint16类型
black_level: 黑电平值
mode='global'时,为标量int
mode='row'时,为shape=(H,)的逐行黑电平数组
mode: 'global' 或 'row'
Returns:
校正后RAW图,uint16,已截断到[0, 1023]
"""
if mode == 'global':
corrected = raw_img.astype(np.int32) - int(black_level)
elif mode == 'row':
# 逐行减去对应行的黑电平
corrected = raw_img.astype(np.int32) - black_level.reshape(-1, 1)
else:
raise ValueError("mode must be 'global' or 'row'")
# 截断到有效范围并转回uint16
corrected = np.clip(corrected, 0, 1023).astype(np.uint16)
return corrected
def estimate_black_level_from_ob(raw_img: np.ndarray,
ob_rows: int = 4) -> np.ndarray:
"""
从传感器OB(Optical Black)行估计逐行黑电平
假设RAW顶部前ob_rows行为被遮挡的OB行
Args:
raw_img: 输入RAW图,含OB行
ob_rows: OB行数
Returns:
逐行黑电平数组,shape=(H,)
"""
ob_region = raw_img[:ob_rows, :].astype(np.float32)
# 每行OB像素均值作为该行黑电平,再对所有OB行取平均得到全局值
row_means = ob_region.mean(axis=1)
global_bl = row_means.mean()
# 实际应用中逐行校正使用每行均值,此处返回全局值用于global模式
return global_bl
if __name__ == "__main__":
# ---------- 最小验证闭环 ----------
# 构造模拟RAW:10bit,尺寸8x8,真实信号叠加黑电平基底
np.random.seed(42)
H, W = 8, 8
true_signal = np.random.randint(0, 800, size=(H, W), dtype=np.uint16)
bl_value = 64 # 模拟传感器黑电平
# 模拟传感器输出 = 真实信号 + 黑电平
raw_with_bl = np.clip(true_signal.astype(np.int32) + bl_value,
0, 1023).astype(np.uint16)
# 执行全局黑电平校正
corrected = black_level_correction(raw_with_bl, black_level=bl_value,
mode='global')
# 验证:校正后暗部区域(原真实信号为0~50)应接近0
dark_mask = true_signal < 50
dark_corrected = corrected[dark_mask]
dark_original = raw_with_bl[dark_mask]
print("原始暗部像素均值: {:.2f}".format(dark_original.mean()))
print("校正后暗部像素均值: {:.2f}".format(dark_corrected.mean()))
print("校正后暗部最大偏差: {} (应≤1,因截断取整)".format(
np.abs(dark_corrected.astype(np.int32) -
true_signal[dark_mask].astype(np.int32)).max()))
# 逐行模式验证
row_bl = np.full(H, bl_value, dtype=np.uint16)
corrected_row = black_level_correction(raw_with_bl, black_level=row_bl,
mode='row')
assert np.array_equal(corrected, corrected_row), "两种模式结果应一致"
print("逐行模式与全局模式结果一致: True")
运行与验证
- 将上述代码保存为
black_level_correction.py - 终端执行:
python black_level_correction.py - 预期输出(确定性结果):
原始暗部像素均值: 约64~114(取决于随机种子)
校正后暗部像素均值: 约0~50
校正后暗部最大偏差: 0 (因真实信号与黑电平均为整数,相减无误差)
逐行模式与全局模式结果一致: True
通过判定标准:程序无异常退出;校正后暗部均值显著下降(接近真实信号水平);两种模式输出完全一致;暗部最大偏差为0(整数运算无精度损失)。
关键说明
- 真实RAW数据中黑电平值需从传感器驱动或寄存器读取,本案例提供
estimate_black_level_from_ob函数演示从OB行估计的方法,实际使用时需根据传感器手册确认OB行位置。 - 校正后必须做clip截断,防止暗部减出负值导致uint16下溢。
- 若传感器存在行噪声(rolling shutter),应使用逐行黑电平数组而非全局标量。
上图展示了黑电平校正的本质效果:整体直方图向左平移黑电平值,使暗部区域(原本被基底噪声掩盖的低码值信号)归零并保留真实细节。校正后RAW数据的动态范围从[BL, 1023]恢复为[0, 1023-BL],为后续ISP模块提供无偏置输入。