3. 黑电平校正(BLC)

黑电平校正,圈内人常叫 BLC。这名字听着挺唬人,其实说白了就一件事:把传感器在「全黑」环境下不该有的那点底噪信号给抠掉。

我刚开始调 ISP 时,觉得这步最简单——不就是减个固定值嘛。后来被一块 sensor 的暗部偏色问题折腾了三天,才发现黑电平远没我想的那么单纯。

3.1 黑电平是怎么冒出来的?

先搞清楚一个问题:为什么全黑环境下,sensor 输出的像素值不是 0?

理想情况下,没有光子打到像素上,ADC 应该输出 0。但现实是,sensor 内部有暗电流、有电路噪声、有 ADC 的偏移量。这些因素叠加起来,让全黑像素也输出一个非零值。这个值,就是黑电平。

我遇到过一块 sensor,黑电平高达 64(12bit 数据)。如果不做校正,暗部全是灰蒙蒙一片,根本没法看。

黑电平的主要来源,我整理了一下:

来源 说明 典型影响
暗电流 热激发产生的电子,温度越高越严重 随温度漂移,长曝光时明显
ADC 偏移 模数转换器的固定偏置 固定值,每颗 sensor 略有差异
像素复位噪声 复位操作残留的电荷 随机性较大,帧间不稳定
电路串扰 电源/时钟耦合到像素输出 与 sensor 布局相关
我的经验:暗电流是黑电平里最头疼的变量。夏天和冬天,同一块 sensor 的黑电平能差 10-15 个 DN。所以做 BLC 时,温度补偿一定要考虑进去。

3.2 校正原理:到底减多少?

校正原理其实就一句话:从每个像素值里减去黑电平值。

但问题来了——黑电平值是多少?怎么测?

大多数 sensor 会预留一些「光学黑像素」(Optical Black, OB)。这些像素被金属遮挡,永远接收不到光。它们的输出,就是当前帧的黑电平参考值。

校正公式很简单:

// 12bit 数据,黑电平目标值 64
// 实际黑电平从 OB 区域统计得到
uint16_t blc_value = get_ob_mean();  // 比如 68
uint16_t target_blc = 64;            // 目标黑电平

for (int i = 0; i < frame_size; i++) {
    int16_t corrected = raw_data[i] - (blc_value - target_blc);
    if (corrected < 0) corrected = 0;
    raw_data[i] = (uint16_t)corrected;
}

这里有个细节:为什么要减到目标值而不是直接减到 0?

嗯,因为后续的 ISP 模块(比如去噪、Gamma)需要一点底数来正常工作。直接砍到 0,暗部细节就全丢了。一般 12bit 数据,目标黑电平设在 64 左右比较常见。

核心要点:BLC 不是简单减到 0,而是减到一个稳定的目标值。这个目标值要保证暗部有足够的动态范围,又不能让黑电平残留太多。

3.3 参数调试方法

调试 BLC 参数,我一般分三步走。每一步都有坑,我一个个说。

第一步:确定 OB 区域

先看 sensor datasheet,找到 OB 像素的位置和数量。有的 sensor 在左边,有的在上下两边。我见过最离谱的,OB 区域分布在四个角落,统计起来特别麻烦。

确定 OB 区域后,要算它的均值和中位数。我个人习惯用中位数——均值容易被异常像素带偏。

// 从 OB 区域统计黑电平
uint32_t sum = 0;
uint16_t min_val = 4095, max_val = 0;

for (int i = 0; i < ob_pixel_count; i++) {
    uint16_t val = ob_buffer[i];
    sum += val;
    if (val < min_val) min_val = val;
    if (val > max_val) max_val = val;
}

uint16_t mean = sum / ob_pixel_count;
// 去掉最大最小各 5% 后算均值,更鲁棒
uint16_t robust_mean = get_robust_mean(ob_buffer, ob_pixel_count, 0.05);
避坑:OB 区域可能有坏像素。我曾经遇到一块 sensor,OB 区域里有几个像素一直输出 0,把均值拉低了一大截。后来加了坏像素检测,才把问题揪出来。

第二步:逐行/逐列校正

有些 sensor 的黑电平不是全图一致的。行与行之间、列与列之间,可能有差异。这时候就要做逐行或逐列的 BLC。

我调过一块卷帘快门的 sensor,每行的黑电平都不一样,从第 1 行到第 1080 行,黑电平慢慢往上漂。如果不做逐行校正,图像从上到下会有一条渐变亮带。

逐行校正的做法:

// 逐行统计 OB 区域
for (int row = 0; row < height; row++) {
    uint16_t row_blc = get_row_ob_mean(row);
    for (int col = 0; col < width; col++) {
        int idx = row * width + col;
        int16_t corrected = raw_data[idx] - (row_blc - target_blc);
        if (corrected < 0) corrected = 0;
        raw_data[idx] = (uint16_t)corrected;
    }
}

第三步:温度补偿

这是最容易被忽略的一步。sensor 温度升高,暗电流增大,黑电平跟着涨。如果不做补偿,白天调好的参数,晚上就偏了。

我一般会在 sensor 附近放一个温度传感器,每隔几秒读一次温度,然后查表补偿黑电平目标值。

温度范围 黑电平偏移量(12bit) 补偿策略
-20°C ~ 0°C -8 ~ -4 降低目标值
0°C ~ 40°C 0 不补偿
40°C ~ 60°C +4 ~ +12 提高目标值
60°C ~ 85°C +12 ~ +30 大幅提高,同时检查暗部噪声
我的习惯:温度补偿曲线最好在实验室里实测出来。拿热风枪吹 sensor,从常温吹到 80°C,记录每个温度点的黑电平值。拟合出一条曲线,写到 sensor 的校准参数里。

3.4 BLC 调优的常见问题

调了这么多年 BLC,踩过的坑不少。挑几个典型的说说。

  • 暗部偏色:BLC 没做对,暗部会偏绿或偏紫。检查一下 R、G、B 三个通道的黑电平是不是分别校正的。有些 sensor 每个通道的黑电平不一样。
  • 暗部闪烁:帧与帧之间黑电平波动太大。可以加一个 IIR 滤波器,让黑电平值平滑变化。
  • 过校正:减太多,暗部变成纯黑,细节全没了。检查一下目标值是不是设得太低。
  • OB 区域污染:OB 像素被光漏进来了。我遇到过镜头安装偏位,光线从侧面漏进 OB 区域,导致黑电平统计偏高。
我曾经踩过的坑:有一款工业相机,客户反馈暗部有竖条纹。查了两天,发现是 BLC 的逐列校正参数写反了——把列偏移量加到了像素值上,而不是减掉。校正变成了加噪,画面反而更差了。从那以后,我每次改 BLC 参数都会先拍一张全黑图验证。

3.5 BLC 在 ISP 流程中的位置

BLC 是 ISP 流水线的第一道工序。它处理的是 raw 域的数据,在去噪、去马赛克、白平衡之前就要做完。

为什么?因为后面的模块都假设黑电平已经被校正到标准值了。如果 BLC 没做好,后面的白平衡会算错,Gamma 会拉偏,整个图像质量都受影响。

下面这张图,是我自己画的 BLC 在 ISP 流程中的位置和内部逻辑:

BLC 在 ISP 流程中的位置与内部逻辑 Raw 输入 BLC 去噪 去马赛克 后续模块 BLC 内部逻辑展开 OB 区域统计 均值/中位数计算 逐行/逐列校正 温度补偿 说明 BLC 必须在 raw 域最先执行 后续模块依赖稳定的黑电平基准 温度补偿是容易被忽略的关键环节 常见问题 暗部偏色 → 检查各通道独立校正 暗部闪烁 → 加 IIR 平滑

你看,BLC 虽然只是 ISP 流水线里的一小步,但它直接影响后面所有模块的输入质量。我见过太多项目,图像调不好,最后追根溯源,都是 BLC 没做扎实。

所以我的建议是:别急着调后面的参数,先把 BLC 搞稳了。拍一张全黑图,看看暗部是不是均匀的、稳定的。这一步做好了,后面的调优才能站得住脚。