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")

运行与验证

  1. 将上述代码保存为 black_level_correction.py
  2. 终端执行:python black_level_correction.py
  3. 预期输出(确定性结果):
原始暗部像素均值: 约64~114(取决于随机种子)
校正后暗部像素均值: 约0~50
校正后暗部最大偏差: 0 (因真实信号与黑电平均为整数,相减无误差)
逐行模式与全局模式结果一致: True

通过判定标准:程序无异常退出;校正后暗部均值显著下降(接近真实信号水平);两种模式输出完全一致;暗部最大偏差为0(整数运算无精度损失)。

关键说明

  • 真实RAW数据中黑电平值需从传感器驱动或寄存器读取,本案例提供estimate_black_level_from_ob函数演示从OB行估计的方法,实际使用时需根据传感器手册确认OB行位置。
  • 校正后必须做clip截断,防止暗部减出负值导致uint16下溢。
  • 若传感器存在行噪声(rolling shutter),应使用逐行黑电平数组而非全局标量。
黑电平校正前后RAW直方图对比(10bit模拟) 校正前(含黑电平基底) BL=64 0 1023 RAW码值 减去BL 校正后(暗部归零) 0 0 959 RAW码值 暗部区域 细节保留

上图展示了黑电平校正的本质效果:整体直方图向左平移黑电平值,使暗部区域(原本被基底噪声掩盖的低码值信号)归零并保留真实细节。校正后RAW数据的动态范围从[BL, 1023]恢复为[0, 1023-BL],为后续ISP模块提供无偏置输入。