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 输入到校正输出的完整链路:

黑电平校正流程图 黑电平校正完整流程 RAW 原始数据 提取 OB 像素(暗行/暗列) 计算黑电平值(均值/中值) 像素值 - 黑电平 + 保留偏移 ⚠️ 注意 OB 像素位于传感器 边缘,不参与成像 每帧实时更新 💡 技巧 局部校正时,每行 OB 像素单独计算

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 像素越多,统计越稳定,但有效成像区域会变小,需要平衡。