3. 黑电平校正(BLC)与线性化

黑电平校正,圈内人常叫它BLC。这活儿看着不起眼,却是ISP Pipeline里第一个真正意义上的“校正”环节。说白了,传感器在完全无光环境下输出的像素值不应该是0,而是一个偏高的底数——这个底数就是黑电平。如果不把它减掉,后面所有算法都会带着一个固定的直流偏置在算,暗部细节直接废掉。

我早年调过一颗安森美的传感器,黑电平居然有64个码值。当时年轻,没仔细做BLC就直接跑AWB,结果白平衡怎么调都偏蓝。后来才发现,暗电流把R通道抬高了整整一截。嗯,从那以后,我拿到任何新Sensor的第一件事,就是先看它的OB(Optical Black)区域数据。

3.1 黑电平是怎么来的

黑电平的成因主要有三个:

  • 暗电流:半导体热激发产生的电子,温度越高越严重。我实测过,温度每升高10°C,暗电流差不多翻一倍。
  • ADC偏移:模数转换器本身有个固定的输出偏置,不同通道可能还不一样。
  • 像素复位噪声:CMOS传感器复位电平的不确定性,这个在全局快门传感器上尤其明显。

关键认知:黑电平不是噪声,是偏置。噪声可以靠降噪滤掉,偏置必须靠BLC减掉。减不干净,暗部细节就永远埋在底数里。

3.2 全局BLC vs 局部BLC

实现方式上,BLC分两种:全局BLC和局部BLC。我列个表,方便你对比选型。

对比项 全局BLC 局部BLC
原理 整帧减去同一个固定值 按区域/行/列分别计算减除值
计算量 极低,一次减法 中等,需统计OB区域均值
适用场景 暗电流均匀、温度稳定的Sensor 大靶面Sensor、长曝光、温度变化大
缺点 无法处理行间/列间差异 需预留OB像素区域,占用有效像素

全局BLC的实现非常简单,就是一行代码的事:

// 全局BLC:整帧减去固定黑电平值
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        pixel[y][x] = max(0, pixel[y][x] - black_level);
    }
}

但实际项目中,我很少用全局BLC。为什么?因为暗电流在空间上不是均匀的。传感器边缘的暗电流往往比中心高,尤其是大靶面传感器。我曾经调过一个1英寸的Sensor,左上角和右下角的黑电平差了12个码值。用全局BLC,左上角减干净了,右下角还留着底数;右下角减到位了,左上角就变负值被截断了。

局部BLC的做法是:利用传感器周边的光学黑区(Optical Black,OB)像素,实时统计每行或每列的黑电平均值,然后逐行或逐列减去。

// 局部BLC:逐行统计OB区域均值并减去
for (int y = 0; y < height; y++) {
    // 统计该行OB区域(假设前16列为OB像素)的均值
    int sum = 0;
    for (int x = 0; x < OB_WIDTH; x++) {
        sum += pixel[y][x];
    }
    int row_black = sum / OB_WIDTH;
    
    // 用该行的黑电平值校正整行
    for (int x = OB_WIDTH; x < width; x++) {
        pixel[y][x] = max(0, pixel[y][x] - row_black);
    }
}

我的经验:局部BLC的OB区域宽度建议取16~32个像素。太窄了统计噪声大,太宽了占用有效像素太多。另外,OB区域要避开传感器边缘的暗角区域,否则统计值会偏低。

3.3 线性化校正——BLC的“好搭档”

线性化校正,很多人把它和BLC混为一谈。其实它们是两件事,但通常放在一起做。

BLC解决的是“零点偏移”,线性化解决的是“响应曲线弯曲”。

传感器的光电响应在理想情况下是线性的:光强翻倍,输出码值翻倍。但实际传感器在低照度段和高照度段都有非线性区。低照度段尤其严重,因为暗电流和读出噪声会压缩动态范围。

线性化校正的典型做法是分段线性拟合:

// 线性化校正:分段线性查找表
// lut[input] = output,由标定数据生成
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        pixel[y][x] = lut[pixel[y][x]];
    }
}

这个LUT怎么来的?需要在实验室用积分球做标定。我一般取10~15个光强点,记录传感器输出,然后做分段线性插值。注意,线性化一定要在BLC之后做,因为BLC减掉的偏置会改变曲线的截距,先做线性化再减BLC,校正就不准了。

重要提醒:线性化校正必须在Lens Shading Correction之前做。因为LSC的增益是乘性的,如果输入数据本身非线性,乘性增益会把非线性误差放大。我曾经见过一个项目,LSC之后暗角是补平了,但画面出现奇怪的条纹——查了两天才发现是线性化没做好,LSC把非线性失真放大了。

3.4 调试参数与效果评估

BLC和线性化的调试参数不多,但每个都很关键:

参数 说明 典型范围
Black Level Target 校正后的目标黑电平值,通常设为0或16 0 ~ 64
OB Region Start OB像素的起始列/行位置 0 ~ 32
OB Region Width OB像素的宽度 8 ~ 64
Linearization LUT 分段线性校正表,通常256或4096点 由标定生成

效果评估,我一般看三个指标:

  1. 暗场均值:盖上镜头盖拍一张全黑图,统计全图均值。理想值应该接近0(或你设定的Target)。偏差超过2个码值就要检查了。
  2. 暗场行间波动:逐行统计均值,看行与行之间的波动。波动超过1个码值,说明局部BLC没做好,或者OB区域统计噪声太大。
  3. 线性度残差:用积分球拍不同光强下的灰阶图,拟合响应曲线,看残差。残差超过1%就需要重新标定LUT。

一句话总结:BLC做不好,暗部偏色、细节丢失;线性化做不好,LSC和CCM都会跟着出错。这两个环节是ISP Pipeline的基石,值得花时间调透。

3.5 BLC与线性化在Pipeline中的位置

下面这张图展示了BLC和线性化在整个ISP Pipeline中的位置关系。注意,它们必须在去马赛克(Demosaic)之前完成,因为Bayer域的数据一旦被插值成RGB,黑电平信息就混在一起分不开了。

ISP Pipeline 前级:BLC 与线性化位置 Sensor Raw BLC 线性化 LSC ... BLC 核心逻辑 • 减除暗电流/ADC偏移引入的固定偏置 • 全局BLC:整帧减固定值,简单但精度低 • 局部BLC:逐行/列统计OB区域,精度高 线性化核心逻辑 • 补偿传感器响应曲线的非线性 • 使用分段线性LUT,由积分球标定生成 • 必须在BLC之后、LSC之前执行 ⚠️ 关键顺序约束 Sensor Raw → BLC → 线性化 → LSC → 后续Pipeline 顺序不可颠倒!BLC必须在线性化之前,线性化必须在LSC之前

这张图我画了很多遍,每次给团队新人讲Pipeline都会拿出来。你注意看那个顺序约束框——BLC、线性化、LSC这三个模块的顺序是锁死的。我见过不止一个项目,因为赶进度把线性化和LSC的顺序搞反了,结果画面边缘出现奇怪的色偏,怎么调LSC都调不回来。后来发现是线性化没做,LSC的乘性增益把非线性失真放大了。