3、黑电平校正:从概念到实战

黑电平校正,听起来挺专业,说白了就是告诉相机:「喂,真正的黑色长什么样?」

我刚开始接触RAW处理时,觉得这步可有可无。直到有一次,我处理一张夜景长曝光照片,暗部区域怎么调都偏紫,后来才发现是黑电平没校正好。嗯,从那以后,我再也不敢跳过这个环节了。

3.1 黑电平到底是什么?

先问一个问题:传感器在完全无光的情况下,输出的数字值应该是多少?

理想情况是0。但现实是,传感器本身有暗电流——即使盖上镜头盖,像素也会因为热噪声、读出噪声产生一个非零的底数。这个底数,就是黑电平。

核心定义:黑电平(Black Level)是传感器在零光照条件下输出的数字信号值,通常以DN(Digital Number)或ADC码值表示。

不同传感器、不同增益、不同温度下,黑电平都不一样。我见过最夸张的案例——某款工业传感器在60°C环境下,黑电平漂了将近40个DN。你想想看,这要是没校正,暗部细节全被淹没了。

3.2 为什么要做黑电平校正?

不做黑电平校正,会出什么问题?我列几个实际踩过的坑:

  • 暗部偏色:黑电平没减干净,暗部残留非零值,导致偏紫或偏绿。我在手机ISP调试时遇到过,暗部总是偏品红,查了三天才发现是黑电平没对齐。
  • 动态范围损失:黑电平抬高了,有效信号范围被压缩。本来12bit传感器能到4095,结果黑电平占了64,实际可用范围就少了1.5%。
  • 白平衡不准:黑电平对RGB通道的影响不一致,会导致白平衡计算偏差。尤其是低光照下,这个误差会被放大。
  • 降噪算法失效:很多降噪算法假设黑电平已经归零。如果没校正,噪声模型就错了,降噪效果大打折扣。

避坑指南:我曾经接手过一个项目,算法团队直接拿原始RAW数据做处理,没做黑电平校正。结果在暗光场景下,信噪比比预期低了3dB。后来加上校正,指标立刻达标。记住:黑电平校正是最基础、最容易被忽略的一步。

3.3 不同传感器的黑电平值获取方法

获取黑电平值,没有万能公式。不同传感器厂商、不同架构,方法差异很大。我按实际项目经验,把常见方法分成三类:

3.3.1 方法一:片上黑像素(OB Pixels)

这是最主流的方法。传感器会在感光区域周围布置一些被金属遮挡的像素,这些像素永远接收不到光,专门用来测量黑电平。

传感器类型 OB像素位置 典型数量 注意事项
Sony IMX系列 左侧和顶部 每行/列 4~16个 注意区分有效OB和无效边界
Onsemi PYTHON系列 四周环绕 每行/列 8~32个 温度补偿很重要
CMV系列 右侧和底部 每行/列 4~8个 高速模式下OB值会波动

具体做法:

  1. 从RAW数据中提取OB像素区域
  2. 计算OB像素的平均值(或中位数,防止坏点干扰)
  3. 用这个值作为当前帧的黑电平
// 伪代码:从RAW中提取OB像素并计算黑电平
uint16_t calculateBlackLevel(uint16_t* rawFrame, int width, int height, int obLeft, int obTop, int obWidth, int obHeight) {
    uint64_t sum = 0;
    int count = 0;
    
    for (int y = obTop; y < obTop + obHeight; y++) {
        for (int x = obLeft; x < obLeft + obWidth; x++) {
            // 跳过坏点(阈值判断)
            if (rawFrame[y * width + x] < OB_BAD_PIXEL_THRESHOLD) {
                sum += rawFrame[y * width + x];
                count++;
            }
        }
    }
    
    return (count > 0) ? (uint16_t)(sum / count) : DEFAULT_BLACK_LEVEL;
}

个人经验:我习惯用中位数代替平均值。因为OB像素里偶尔会有坏点,平均值会被拉偏。中位数更鲁棒。另外,建议每帧都重新计算,不要用固定值——温度变化会漂移。

3.3.2 方法二:暗帧采集(Dark Frame)

有些传感器没有OB像素,或者OB像素数量太少。这时候就需要拍暗帧。

做法很简单:盖上镜头盖,在相同曝光参数下拍一张全黑图像,然后取全图平均值作为黑电平。

但要注意几个坑:

  • 温度要一致:暗电流随温度指数增长。我试过在25°C和45°C下采集暗帧,黑电平差了20个DN。
  • 增益要匹配:不同ISO下的黑电平不同。建议每个ISO档位都单独采集。
  • 曝光时间要相同:长曝光下暗电流积累更多,黑电平会更高。

3.3.3 方法三:寄存器读取(Register Read)

部分高端传感器会把黑电平值直接写在寄存器里,或者通过SPI/I2C接口读取。

我遇到过一款工业线阵传感器,它的黑电平值存储在0x3A和0x3B两个寄存器中,读取后拼接即可:

// 从传感器寄存器读取黑电平值
uint16_t readBlackLevelFromRegister(int i2c_fd, uint8_t sensor_addr) {
    uint8_t reg_high = 0x3A;
    uint8_t reg_low = 0x3B;
    uint8_t high_byte, low_byte;
    
    // 读取高8位
    i2c_write(i2c_fd, sensor_addr, &reg_high, 1);
    i2c_read(i2c_fd, sensor_addr, &high_byte, 1);
    
    // 读取低8位
    i2c_write(i2c_fd, sensor_addr, &reg_low, 1);
    i2c_read(i2c_fd, sensor_addr, &low_byte, 1);
    
    return (high_byte << 8) | low_byte;
}

注意:寄存器值通常是出厂标定值,不一定反映当前实际状态。我建议用寄存器值做初始参考,再用OB像素或暗帧做实时修正。

3.4 黑电平校正的完整流程

把上面这些串起来,一个完整的黑电平校正流程应该是这样的:

黑电平校正完整流程 输入RAW数据 判断传感器类型 有OB像素 → 实时提取 无OB像素 → 暗帧采集 计算OB均值/中位数 计算暗帧全图均值 获取黑电平值 校正后RAW数据 校正公式:RAW_out = RAW_in - BlackLevel

3.5 校正后的验证方法

校正做没做对,怎么验证?我一般用三个指标:

  1. 暗场均值:盖上镜头盖拍一张,校正后全图均值应接近0。允许±1 DN的波动。
  2. 暗场标准差:校正后暗场的标准差应该只反映读出噪声,如果标准差异常大,说明校正不彻底。
  3. 暗部线性度:用均匀光源拍不同亮度的灰阶图,检查暗部区域的响应是否线性。如果低光区域出现弯曲,黑电平可能没减干净。

我的习惯:每次换传感器型号或改硬件设计,我都会重新做一遍黑电平标定。别偷懒,不同批次、不同PCB布局,黑电平都可能不一样。

好了,黑电平校正就讲到这里。记住一句话:黑电平是RAW处理的基石,基石不稳,上层算法全白搭。