2、黑电平校正算法原理:全局减法的局限性、动态黑电平校正、基于行/列的黑电平校正

黑电平校正,说白了就是去掉传感器在暗场下的固定底噪。你想想看,哪怕把镜头盖死,传感器依然会输出一个非零的数值。这个数值如果不扣掉,整幅画面就会偏灰,暗部细节全被埋没了。

但校正这事儿,远没有「减一个固定值」那么简单。我在项目中踩过不少坑,今天就把这些经验掰开了讲。

2.1 全局减法的局限性

最直观的做法,就是取一帧暗场图像,算出一个全局平均值,然后从每一帧的每个像素里减去这个值。代码写起来很简单:

// 全局减法示例(伪代码)
uint16_t global_black_level = 64;  // 假设暗场均值
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        pixel[y][x] = (pixel[y][x] > global_black_level) ?
                       pixel[y][x] - global_black_level : 0;
    }
}

但实际效果呢?一塌糊涂。

我遇到过这样一个案例:某款手机传感器,左边暗场均值是60,右边却到了72。用全局64去减,左边欠校正(残留4个DN),右边过校正(多减了8个DN)。结果拍出来的夜景照片,左边偏灰,右边暗部细节直接丢失。

全局减法的三大硬伤:

  • 空间不均匀性:传感器不同区域的暗电流不一致,一个值管不了全幅
  • 温度漂移:黑电平随温度变化,开机冷机和运行热机完全是两码事
  • 增益耦合:模拟增益改变时,黑电平也会跟着变,全局减法根本跟不上

说白了,全局减法只适合玩具级应用。真正量产的产品,没人敢这么干。

2.2 动态黑电平校正

既然黑电平会变,那就动态跟踪它。怎么做?

大多数传感器会在像素阵列的边缘预留一些光学黑像素(Optical Black, OB)。这些像素被金属层遮挡,收不到光,但电路和正常像素完全一样。它们的输出,就是当前帧的实时黑电平参考。

我习惯的做法是这样的:

  1. 从每一帧中提取OB区域的像素值
  2. 剔除异常点(比如坏像素或受串扰影响的边缘像素)
  3. 计算OB区域的均值或中位数
  4. 用这个值作为当前帧的校正量
// 动态黑电平校正(基于OB区域)
uint32_t ob_sum = 0;
uint16_t ob_count = 0;
for (int y = ob_start_y; y < ob_end_y; y++) {
    for (int x = ob_start_x; x < ob_end_x; x++) {
        if (!is_defect_pixel(y, x)) {  // 剔除坏点
            ob_sum += pixel[y][x];
            ob_count++;
        }
    }
}
uint16_t dynamic_black = ob_sum / ob_count;

// 逐像素校正
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        pixel[y][x] = (pixel[y][x] > dynamic_black) ?
                       pixel[y][x] - dynamic_black : 0;
    }
}

经验之谈:OB区域通常有4~16行/列。别用全部OB像素,边缘几行容易受光泄露影响。我一般会砍掉最外面2行,只取中间稳定的部分。

动态校正的好处很明显——它能跟上温度和增益的变化。但问题也来了:OB区域只能反映全局趋势,解决不了空间不均匀性。

2.3 基于行/列的黑电平校正

这才是工业级方案的核心。

传感器读出电路是按行扫描的。每一行在读出时,都会经过相同的模拟链(放大器、ADC)。但不同行的读出时间不同,温度分布也不同,导致行与行之间的黑电平存在差异。这种现象叫行噪声(Row Noise)或列方向固定模式噪声(Column FPN)。

我记得有一次调试一款全局快门传感器,暗场图像上出现了明显的横条纹。用全局校正根本去不掉,后来一查,每行的黑电平偏差最大达到5个DN。这就是典型的行噪声。

解决方案?逐行校正。

具体做法是:在每一行的有效像素之前,传感器会输出几个哑像素(Dummy Pixel)或行参考像素。这些像素不感光,只反映该行当前的黑电平状态。

// 基于行的黑电平校正
for (int y = 0; y < height; y++) {
    // 提取当前行的参考像素(前4个哑像素)
    uint32_t row_ob_sum = 0;
    for (int x = 0; x < 4; x++) {
        row_ob_sum += pixel[y][x];  // 假设前4列是OB
    }
    uint16_t row_black = row_ob_sum / 4;

    // 校正当前行的有效像素
    for (int x = 4; x < width; x++) {
        int32_t corrected = (int32_t)pixel[y][x] - (int32_t)row_black;
        pixel[y][x] = (corrected > 0) ? (uint16_t)corrected : 0;
    }
}

同理,有些传感器还会提供列参考像素,用于校正列方向的不均匀性。两者结合,就是2D黑电平校正。

注意:行/列参考像素的数量通常很少(2~8个),容易受随机噪声影响。我建议做一下中值滤波或剔除最大最小值后再取平均,能有效抑制单点噪声的干扰。

2.4 三种方案的对比

方案 校正粒度 抗温度漂移 抗空间不均匀 硬件开销 适用场景
全局减法 整帧 差 差 极低 低端玩具、调试阶段
动态校正 整帧(实时) 好 差 低 消费级摄像头、安防
行/列校正 逐行/逐列 好 好 中 工业相机、医疗、车载

从表中能看出来,行/列校正是目前最均衡的方案。但硬件上需要额外存储每行的校正值,对行缓冲(Line Buffer)有要求。

2.5 知识体系:黑电平校正的核心逻辑

下面这张图,是我自己总结的黑电平校正知识体系。它把三种方案的关系和适用边界画清楚了。

黑电平校正方案对比与演进 黑电平校正方案演进与对比 黑电平校正 全局减法 动态黑电平校正 行/列黑电平校正 固定值减除 空间不均匀 OB区域提取 帧级实时更新 行参考像素 列参考像素 核心结论:行/列校正兼顾空间均匀性与实时性,是工业级首选 全局减法仅用于调试或极低成本方案,动态校正适合消费级产品

2.6 避坑指南

最后,分享几个我实际项目中踩过的坑:

  • OB区域被光污染:有些传感器的OB区域密封不严,强光下会有光泄露。我曾经在户外强光测试时发现OB值异常偏高,排查了两天才找到原因。解决方案是增加OB区域的屏蔽层,或者在算法中做光泄露检测。
  • 行校正引入新噪声:如果行参考像素太少(比如只有2个),随机噪声会导致校正后的图像出现新的行噪声。我建议至少用4个参考像素,并做中值滤波。
  • 增益切换时的跳变:自动增益控制(AGC)切换时,黑电平会突变。如果校正跟不上,画面会出现闪烁。解决办法是在增益切换后,强制更新OB参考值,并做帧间平滑过渡。
  • 温度补偿系数:不同传感器的黑电平温度系数不一样。我习惯在产线标定阶段,记录-20°C到80°C的黑电平曲线,然后做线性插值补偿。

一个小技巧:如果你用的是行/列校正,建议在ISP流水线中把黑电平校正放在最前面。因为后面的模块(如去噪、伽马校正)都会放大黑电平误差,早校正早安心。