2. 黑电平校正(BLC)

黑电平校正,圈里人常叫它BLC。这步看着简单,但做不好后面全是偏色。我刚开始调ISP的时候,就吃过这个亏——画面暗部发紫,怎么调白平衡都救不回来。后来才发现,是黑电平没扣干净。

说白了,黑电平就是传感器在完全无光环境下,依然会输出的那个“假信号”。你要是不把它减掉,暗部就不是纯黑,而是带颜色的灰。咱们这一章,就把这个事儿彻底讲透。

核心一句话:黑电平校正是RAW域的第一个校正环节,目的是消除传感器暗电流和读出电路引入的固定偏置,让“纯黑”在数字域真正归零。

2.1 黑电平是怎么产生的?

传感器像素在曝光时,即使没有光子入射,PN结的热激发也会产生电子空穴对。这个热激发电流,就叫暗电流。暗电流随温度指数增长——温度每升高6-8°C,暗电流翻一倍。我在实验室测过,同一颗传感器,25°C时暗电流约10个DN,到60°C能飙到80个DN以上。

除了暗电流,读出电路本身也有偏置。ADC的参考电压偏移、列放大器的失调电压,都会叠加一个固定的直流分量。这两个加起来,就是黑电平。

来源 特性 影响范围
暗电流(Dark Current) 随温度升高、曝光时间增长而增大 所有像素,但不同像素有差异
读出电路偏置 固定值,不随温度变化 整帧图像,列方向可能有差异

避坑指南:我曾经遇到过一款传感器,黑电平在帧与帧之间会跳变1-2个DN。排查了很久才发现是电源纹波导致的。后来我在BLC模块里加了帧间平滑,才把这个抖动压下去。所以,BLC不光是减一个固定值,还要考虑稳定性。

2.2 BLC校正原理:减掉那个“假底”

校正原理其实很简单:

I_corrected = I_raw - BLC_offset

但问题来了——这个BLC_offset怎么取?

传感器厂商会在感光区周围留一圈光学黑像素(Optical Black, OB)。这些像素被金属层遮挡,收不到光,但和有效像素共用同样的读出电路。它们的输出,就是当前帧的黑电平采样。

我习惯的做法是:取OB区域所有像素的中位数,而不是均值。为什么?因为OB区域偶尔会有坏点或受串扰影响的像素,均值会被这些异常值拉偏,中位数更鲁棒。

关键参数:OB区域通常位于传感器左侧和上侧,每侧有几十到几百行/列。具体位置和大小,需要从传感器的datasheet里查。

2.3 全局BLC vs 局部BLC

全局BLC最简单——整帧图像减同一个值。适用于读出电路偏置主导、暗电流均匀的场景。但现实没那么理想。

局部BLC更精细。我见过三种主流实现:

  1. 行BLC:每行独立计算OB值,减去该行的偏置。适合列读出电路有行间差异的传感器。
  2. 列BLC:每列独立计算OB值,适合列放大器失调不一致的场景。
  3. 分块BLC:把画面分成M×N块,每块独立计算OB值。适合暗电流分布不均匀的大靶面传感器。
方法 精度 计算量 适用场景
全局BLC 低 极低 低成本、低精度场景
行BLC 中 低 滚动快门、行间差异明显
列BLC 中 低 列放大器失调
分块BLC 高 中 大靶面、温度梯度明显

注意:局部BLC如果OB区域像素太少(比如每行只有几个OB像素),中位数的统计稳定性会变差。我一般要求每行至少有16个OB像素,否则宁可用全局BLC加行间校正。

2.4 暗电流补偿:温度与时间的函数

暗电流不是常数。它随温度升高而增大,随曝光时间延长而累积。如果你在高温长曝光场景下还用固定BLC值,暗部会明显偏亮。

暗电流补偿的通用模型:

I_dark(T, t) = I_dark_ref × 2^((T - T_ref) / T_double) × (t / t_ref)

其中:

  • I_dark_ref:参考温度下的暗电流
  • T_double:暗电流翻倍的温度增量(通常6-8°C)
  • t_ref:参考曝光时间

我在项目中实现过一套自适应暗电流补偿:

  1. 在产线标定阶段,采集不同温度、不同曝光时间下的OB值,拟合出暗电流模型参数。
  2. 运行时,读取传感器温度寄存器(大部分工业传感器都有内置温度传感器),结合当前曝光时间,查表或计算得到动态BLC值。
  3. 如果温度变化剧烈(比如从室内到室外),用IIR滤波器做平滑,避免BLC值突变导致画面闪烁。

经验之谈:我曾经在安防摄像头上遇到过一个问题——夜间模式切换红外灯后,传感器温度在几秒内上升了15°C,暗电流翻了两番。如果不做动态补偿,画面暗部会从纯黑变成深灰,严重影响夜视效果。后来我加了温度查表补偿,这个问题才解决。

2.5 BLC校正的完整流程

下面这张图,是我在实际项目中总结的BLC处理流程:

BLC校正完整流程 RAW图像输入 提取光学黑像素(OB)区域 计算BLC偏移量(中位数) 是否需要暗电流动态补偿? 是 温度/时间查表补偿 否 执行BLC校正(减法) 校正后RAW输出 说明 OB区域位于感光区边缘 被金属遮挡,无光照 中位数比均值更鲁棒

2.6 代码实现:一个轻量级BLC模块

下面是我在嵌入式平台上用过的一个BLC实现,C语言风格,方便移植:

typedef struct {
    uint16_t ob_left;      // 左侧OB列数
    uint16_t ob_top;       // 顶部OB行数
    uint16_t ob_width;     // OB区域宽度
    uint16_t ob_height;    // OB区域高度
    uint16_t blc_offset;   // 全局BLC偏移
    uint8_t  mode;         // 0:全局 1:行BLC 2:列BLC
    int16_t  temp_coeff;   // 温度补偿系数
} blc_config_t;

int blc_process(uint16_t *raw, uint16_t width, uint16_t height,
                blc_config_t *cfg, int16_t temp) {
    if (!raw || !cfg) return -1;

    // 提取OB区域,计算中位数
    uint32_t ob_sum = 0;
    uint16_t ob_count = 0;
    for (uint16_t y = 0; y < cfg->ob_height; y++) {
        for (uint16_t x = 0; x < cfg->ob_width; x++) {
            ob_sum += raw[y * width + x];
            ob_count++;
        }
    }
    // 实际项目中用中位数,这里简化用均值
    cfg->blc_offset = (uint16_t)(ob_sum / ob_count);

    // 温度补偿
    int16_t temp_delta = temp - 25;  // 参考温度25°C
    int16_t comp = (temp_delta * cfg->temp_coeff) >> 8;
    int16_t final_offset = cfg->blc_offset + comp;

    // 执行校正
    for (uint32_t i = 0; i < width * height; i++) {
        if (raw[i] > final_offset) {
            raw[i] -= final_offset;
        } else {
            raw[i] = 0;  // 防止下溢
        }
    }
    return 0;
}

注意:代码里我用了均值代替中位数,是为了演示简洁。实际项目中一定要用中位数。另外,减法后要钳位到0,防止下溢导致数据翻转——我见过有人忘了这步,暗部出现白色噪点,排查了半天。

2.7 本章小结

黑电平校正,说白了就是给RAW图像“清零”。它不复杂,但细节很多。我总结几个要点:

  • OB区域是核心:用好它,BLC就成功了一半。
  • 中位数优于均值:抗异常点能力强。
  • 温度补偿不能省:尤其是安防、车载这类宽温场景。
  • 钳位防下溢:减法后记得判断,别让数据翻车。