3. 黑电平校正(BLC):从暗电流到数字校正

黑电平校正,英文叫 Black Level Correction,简称 BLC。这名字听起来挺学术,但说白了,就是解决一个很实际的问题:当传感器完全看不到光的时候,它输出的像素值应该是多少?

理想情况下,应该是 0。但现实世界没这么完美。传感器即使在全黑环境里,也会读出一些非零的数值。这个数值,就是黑电平。如果不把它校正掉,整张画面就会蒙上一层灰雾,暗部细节全被吞掉。

我刚开始做 ISP 的时候,就吃过这个亏。当时调一个 sensor,白天效果看着还行,一到夜景,暗部全是偏紫的噪点,怎么调都调不干净。后来才发现,是黑电平没校正好,导致暗部偏色被后面的白平衡放大了。嗯,从那以后,我再也不敢小看 BLC 了。

3.1 黑电平是怎么产生的?

黑电平的根源,主要来自三个方面。我习惯把它们分成“物理的”和“电路的”两类。

来源 原因 影响
暗电流 硅基半导体在热激发下产生电子-空穴对,即使无光照也有电荷积累 随温度升高呈指数增长,长曝光时尤其明显
像素复位噪声 像素复位后,浮置扩散节点电压无法完全归零 引入固定模式噪声(FPN)
ADC 偏移 模数转换器的参考电压或比较器存在固有偏移 整体加一个固定偏置

你想想看,这三个因素叠加在一起,全黑像素读出来可能是 64、128,甚至更高。如果不处理,暗部细节就全被这个“底噪”淹没了。

关键指标:黑电平通常用 DN(Digital Number)表示。常见 CMOS sensor 的黑电平在 64~256 DN(12-bit RAW)之间。温度每升高 10°C,暗电流大约翻一倍。

3.2 校正原理:减掉那个“底”

校正原理其实很简单:从每个像素值里减去一个参考黑电平值。

但问题来了——这个“参考值”怎么取?

我见过两种主流做法:

  1. OB(Optical Black)像素法:传感器边缘有一圈被金属遮挡的像素,它们永远接收不到光。用这些像素的平均值作为黑电平参考。
  2. 固定值法:直接使用 sensor 厂商提供的固定黑电平值,或者通过标定得到。

我个人更推荐 OB 像素法。为什么?因为它能实时跟踪温度变化。我在一个安防项目中遇到过,白天和晚上的黑电平能差 20 多个 DN,用固定值根本扛不住。

校正公式也很直接:

// 黑电平校正核心公式
P_corrected = P_raw - BLC_offset

// 如果使用 OB 像素
BLC_offset = mean(OB_pixels)

// 带增益补偿的版本(用于高增益场景)
BLC_offset = mean(OB_pixels) * gain_compensation

注意:校正后的值可能为负数。硬件实现时一定要做 clamp(钳位)处理,把负值截断到 0。否则后面的 ISP 模块会出各种奇怪的问题。

3.3 硬件实现:流水线里的减法器

在 ISP 硬件流水线里,BLC 通常是第一个处理模块。它紧跟在 RAW 数据采集之后,比去噪、去马赛克都早。

硬件实现的核心是一个减法器 + 累加器结构:

  • OB 像素累加:在行消隐期(H-Blank)或帧消隐期(V-Blank),把 OB 像素的值累加起来,计算平均值。
  • 逐像素减法:每个有效像素进来,减去刚才算好的 BLC_offset。
  • Clamp 电路:结果小于 0 时强制输出 0。

这里有个细节——行级 BLC vs 帧级 BLC。我建议在 CMOS sensor 上用行级 BLC,因为每行的 OB 像素能更准确地反映该行的暗电流状态。帧级 BLC 虽然省资源,但遇到卷帘快门(Rolling Shutter)时,行与行之间的黑电平差异会被忽略。

下面这张图展示了 BLC 在 ISP 流水线中的位置和内部结构:

BLC 在 ISP 流水线中的位置与内部结构 ISP 流水线(前级) RAW 输入 BLC 校正 去噪 去马赛克 后续模块 BLC 模块内部结构 OB 像素提取 累加 / 平均 逐像素减法 Clamp 钳位 关键设计点 行级 BLC:每行使用该行 OB 像素 → 精度高,适合卷帘快门 帧级 BLC:整帧使用同一偏移 → 资源少,适合全局快门

3.4 软件模拟:用 Python 验证算法

在硬件实现之前,我习惯先用软件跑一遍算法。这样能快速验证思路,也能提前发现边界情况。

下面是一个简单的 BLC 模拟代码:

import numpy as np
import cv2

def blc_correction(raw_image, ob_pixels=None, fixed_offset=None):
    """
    黑电平校正软件模拟
    
    参数:
        raw_image: 原始 RAW 图像 (numpy array)
        ob_pixels: OB 像素区域 (numpy array),用于动态计算偏移
        fixed_offset: 固定黑电平偏移值
    
    返回:
        校正后的图像
    """
    # 确保输入是 uint16 或 float
    img = raw_image.astype(np.float32)
    
    if ob_pixels is not None:
        # 使用 OB 像素计算动态偏移
        blc_offset = np.mean(ob_pixels)
        print(f"[BLC] 动态偏移: {blc_offset:.2f} DN")
    elif fixed_offset is not None:
        # 使用固定偏移
        blc_offset = fixed_offset
        print(f"[BLC] 固定偏移: {blc_offset:.2f} DN")
    else:
        raise ValueError("必须提供 ob_pixels 或 fixed_offset")
    
    # 执行校正
    corrected = img - blc_offset
    
    # Clamp 到有效范围
    corrected = np.clip(corrected, 0, None)
    
    return corrected.astype(np.uint16)

# 模拟使用
if __name__ == "__main__":
    # 模拟一个 12-bit RAW 图像(全黑场景)
    h, w = 1080, 1920
    dark_current = 32  # 模拟暗电流贡献
    adc_offset = 48    # 模拟 ADC 偏移
    noise = np.random.normal(0, 5, (h, w))  # 随机噪声
    
    raw_black = dark_current + adc_offset + noise
    raw_black = np.clip(raw_black, 0, 4095).astype(np.uint16)
    
    # 模拟 OB 像素(传感器边缘被遮挡的区域)
    ob_region = raw_black[0:10, 0:10]  # 假设左上角 10x10 是 OB 像素
    
    # 执行校正
    corrected = blc_correction(raw_black, ob_pixels=ob_region)
    
    print(f"校正前均值: {np.mean(raw_black):.2f} DN")
    print(f"校正后均值: {np.mean(corrected):.2f} DN")

我的经验:软件模拟时,一定要加入噪声模型。纯理想情况下的 BLC 很好做,但一旦加上 sensor 的随机噪声和固定模式噪声,你就会发现简单的减法不够用。这时候可以考虑加一个 IIR 滤波器对 OB 像素做平滑,防止单帧噪声导致偏移抖动。

3.5 避坑指南:我踩过的几个坑

做了这么多年 ISP,BLC 相关的坑我踩过不少。挑几个典型的说说:

  • 坑一:OB 像素被污染。 有一次 sensor 的 OB 区域封装漏光,导致黑电平算出来偏高,整张图偏暗。排查了三天才发现。后来我要求所有 OB 像素必须做 坏点检测,剔除异常值再求平均。
  • 坑二:增益后的黑电平漂移。 模拟增益(Analog Gain)放大信号的同时,也会放大黑电平。如果不做增益补偿,高 ISO 下暗部会偏色。我一般会在 BLC 后面加一个 增益归一化 步骤。
  • 坑三:BLC 和去噪的顺序。 有些 ISP 把去噪放在 BLC 前面。我个人不建议这么做——黑电平没校掉,噪声的均值不是零,去噪算法会算错统计量。

重要提醒:BLC 校正后的数据,一定要检查是否有 负值被截断 导致的直方图缺口。这个缺口会在后面的色调映射(Tone Mapping)中被放大,形成肉眼可见的条带。我曾经在一个医疗影像项目里吃过这个亏,图像在暗部出现了明显的等高线效应。

3.6 小结:BLC 的核心要点

黑电平校正看起来简单,但它是整个 ISP 流水线的基石。基础没打好,后面的模块再怎么调也救不回来。

记住三个关键点:

  1. 来源要清楚:暗电流、复位噪声、ADC 偏移,三者叠加决定了黑电平的大小。
  2. 方法要选对:OB 像素法适合温度变化大的场景,固定值法适合资源受限的嵌入式设备。
  3. 实现要严谨:Clamp 处理、增益补偿、OB 像素坏点剔除,一个都不能少。