4. BLC(黑电平校正)

各位做 ISP 的同行,咱们今天聊聊 BLC。黑电平校正,名字听着挺学术,说白了就是——让相机在完全没光的时候,输出值归零。

你可能会想:“这有什么难的?没光不就是 0 吗?” 嗯,现实没这么简单。传感器在暗场下,由于暗电流、读出噪声、ADC 偏移等因素,输出的原始数据并不是 0,而是一个正数。这个正数,就是黑电平(Black Level)。如果不把它减掉,整张画面就像蒙了一层灰雾,暗部细节全被埋没了。

核心一句话: BLC 的目标是找到这个“暗电流偏移量”,然后在每个像素上减去它,让纯黑场景的像素值归零。

4.1 BLC 的原理:暗电流与 ADC 偏移

我刚开始做 ISP 时,对 BLC 的理解就是“减一个固定值”。后来踩了坑才明白,这个“固定值”其实并不固定。它受温度、增益、曝光时间影响很大。

传感器内部通常有两种方式产生黑电平:

  • 光学黑像素(OB, Optical Black): 传感器边缘有一圈被金属遮挡的像素,它们永远接收不到光。这些像素的输出值,就代表了当前条件下的黑电平。
  • ADC 偏移: 模数转换器本身会有一个固定的偏移量,通常由硬件寄存器控制。

实际的黑电平 = OB 像素平均值 + ADC 偏移量。我们做 BLC 校准,就是要精确测量这个值。

我的经验: 有些传感器在低增益下 OB 值很稳定,但高增益下 OB 值会跳动。我遇到过一款 sensor,ISO 3200 时 OB 值比 ISO 100 时高了 12 个 DN(数字数)。如果不做增益相关的 BLC 补偿,暗部就会偏色。

4.2 BLC 校准方法:三步走

校准 BLC 其实不复杂,但步骤必须严谨。我个人习惯按下面三步走:

  1. 采集暗场数据: 盖上镜头盖,确保完全遮光。用目标增益和曝光时间拍一张 raw 图。注意,环境温度要稳定,最好在 25°C 恒温箱里做。
  2. 计算 OB 均值: 从 raw 图中提取 OB 区域(通常是前几行或左右边缘),计算每个通道(R/Gr/Gb/B)的平均值。
  3. 写入寄存器或 LUT: 将计算出的偏移量写入传感器的 BLC 寄存器,或者在 ISP 的 BLC 模块中配置减去的值。

下面是一个简单的校准流程示意图:

BLC 校准流程 1. 采集暗场 Raw 图 2. 提取 OB 像素区域 3. 计算各通道均值 4. 写入寄存器 / LUT

4.3 BLC 对暗部细节的影响

这个影响太大了。我直接说结论:BLC 不准,暗部细节要么被裁切,要么被噪声淹没。

咱们分两种情况看:

BLC 状态 现象 对暗部的影响
BLC 偏大(减多了) 暗部发黑,细节丢失 本来有微弱信号的像素被减到 0 以下,被 clamp 掉,细节永久丢失
BLC 偏小(减少了) 画面发灰,对比度低 暗部残留了本应减掉的偏移量,噪声被放大,信噪比下降
BLC 准确 暗部纯净,细节丰富 微弱信号被保留,噪声水平最低,动态范围最大化
我曾经踩过的坑: 有一款安防相机,在夜间模式下 BLC 没做温度补偿。夏天晚上 sensor 温度 45°C,暗电流比校准时的 25°C 高了 8 个 DN。结果画面暗部偏紫,怎么调 AWB 都调不回来。后来发现是 BLC 没跟上温度变化,暗部残留了 R 通道的偏移。

4.4 BLC 调试实战:从寄存器到图像

好了,理论说完了,咱们上手。假设你拿到一颗新的 sensor,怎么调 BLC?

第一步,先看 datasheet。大部分 sensor 会给出推荐的 BLC 寄存器值。但别全信,我建议你用自己的暗场数据验证一遍。

第二步,写一个简单的校准脚本。下面是一个伪代码示例:

// 伪代码:BLC 校准
uint16_t ob_avg_r = 0, ob_avg_gr = 0, ob_avg_gb = 0, ob_avg_b = 0;
int ob_pixel_count = 0;

// 遍历 OB 区域(假设 raw 图前 4 行是 OB)
for (int row = 0; row < 4; row++) {
    for (int col = 0; col < width; col++) {
        uint16_t pixel = raw_image[row][col];
        // 根据 Bayer 模式累加
        if (is_red(row, col)) ob_avg_r += pixel;
        else if (is_green_red(row, col)) ob_avg_gr += pixel;
        else if (is_green_blue(row, col)) ob_avg_gb += pixel;
        else if (is_blue(row, col)) ob_avg_b += pixel;
        ob_pixel_count++;
    }
}

// 计算均值
ob_avg_r /= (ob_pixel_count / 4);
ob_avg_gr /= (ob_pixel_count / 4);
ob_avg_gb /= (ob_pixel_count / 4);
ob_avg_b /= (ob_pixel_count / 4);

// 写入寄存器(假设 sensor 支持逐通道 BLC)
sensor_write_register(BLC_R, ob_avg_r);
sensor_write_register(BLC_GR, ob_avg_gr);
sensor_write_register(BLC_GB, ob_avg_gb);
sensor_write_register(BLC_B, ob_avg_b);

第三步,验证。拍一张暗场图,用工具(比如 Imatest 或 Python)看直方图。理想情况下,暗场直方图的峰值应该在 0 附近,且没有明显的拖尾。

调试小技巧: 我习惯在调试时把 BLC 值故意设偏,然后观察暗部偏色方向。比如 R 通道 BLC 偏小,暗部会偏红;B 通道偏小,暗部会偏蓝。这能帮你快速定位是哪个通道的 BLC 不准。

4.5 多增益下的 BLC 补偿

前面提到,BLC 会随增益变化。现代 sensor 通常支持多增益下的 BLC 自动补偿,但有些低端 sensor 需要你自己做。

怎么做?很简单,在不同增益下分别采集暗场,建立一张 增益 vs. BLC 偏移 的查找表(LUT)。

模拟增益 (dB) R 通道 BLC (DN) Gr 通道 BLC (DN) Gb 通道 BLC (DN) B 通道 BLC (DN)
0 64 63 63 65
6 68 67 67 69
12 76 74 75 78
18 92 89 90 95

你看,增益每增加 6dB,BLC 偏移大约增加 4-6 个 DN。如果不做补偿,高增益下暗部偏色会非常明显。

实战建议: 在 ISP 的 BLC 模块中,实现一个线性插值函数。根据当前增益,从 LUT 中查表并插值出当前的 BLC 值。这样就能做到增益自适应补偿。

嗯,BLC 这块内容差不多就这些。它虽然基础,但直接影响暗部画质。很多图像问题,比如暗部偏色、噪点异常、动态范围不足,追根溯源往往就是 BLC 没调好。下次遇到这类问题,不妨先从 BLC 查起。