3. 黑电平校正实现

黑电平校正,说白了就是把传感器在完全无光状态下产生的暗电流给扣掉。这个偏移量如果不处理,你拍出来的黑色就不是纯黑,而是发灰的。我最早接触这个模块时,觉得不就是减个固定值嘛,后来踩了坑才发现——没那么简单。

3.1 全局黑电平校正

这是最基础的做法。整个图像传感器只用一个黑电平值,从每个像素中减去这个常数。

核心公式:

Pout = max(Pin - B, 0)

其中 B 是全局黑电平值,通常从传感器数据手册或光学暗区统计得到。

实现起来很简单:

// 全局黑电平校正示例
uint16_t global_black_level = 64;  // 假设12-bit RAW,黑电平64
for (int i = 0; i < width * height; i++) {
    raw[i] = (raw[i] > global_black_level) ? 
             (raw[i] - global_black_level) : 0;
}

什么时候用?

  • 传感器一致性很好,行间噪声小
  • 对校正精度要求不高的场景(比如预览流)
  • 硬件资源受限,没法做逐行处理

注意:全局校正最大的问题是——它假设整个传感器的暗电流完全一致。实际上,CMOS传感器边缘和中心、不同行之间的黑电平可能有明显差异。我曾经在一个项目中直接用全局值,结果图像左右两侧偏色严重,查了两天才发现是黑电平不均匀导致的。

3.2 局部/行级黑电平校正

为什么需要行级校正?因为CMOS传感器的读出方式是逐行扫描的。每一行从曝光到读出的时间差,会导致暗电流积累量不同。你想想看,第一行和最后一行读出的时间差可能有几十毫秒,暗电流差异就出来了。

行级校正的做法是:

  1. 传感器每行末尾有光学屏蔽像素(OB像素,Optical Black)
  2. 取这些OB像素的平均值作为该行的黑电平
  3. 逐行减去对应的黑电平值
// 行级黑电平校正示例
for (int row = 0; row < height; row++) {
    // 假设每行末尾有16个OB像素
    uint32_t ob_sum = 0;
    for (int ob = 0; ob < 16; ob++) {
        ob_sum += raw[row * stride + width + ob];
    }
    uint16_t row_black = ob_sum / 16;
    
    // 对该行有效像素进行校正
    for (int col = 0; col < width; col++) {
        int idx = row * stride + col;
        raw[idx] = (raw[idx] > row_black) ? 
                   (raw[idx] - row_black) : 0;
    }
}
校正方式 精度 计算量 适用场景
全局校正 低 低 预览、低端传感器
行级校正 中 中 主流手机、安防相机
动态校正 高 高 专业相机、医疗影像

经验之谈:行级校正要注意OB像素的稳定性。如果OB像素中有坏点,直接取平均值会引入误差。我习惯先对OB像素做中值滤波,剔除异常值后再计算均值,效果会好很多。

3.3 动态黑电平校正算法

动态校正,说白了就是让黑电平值随着环境变化自动调整。为什么需要动态?因为暗电流受温度影响很大——温度每升高10°C,暗电流大约翻一倍。你想想看,手机在夏天户外拍视频,传感器温度从30°C升到60°C,黑电平可能漂移几十个DN值。

动态校正的常见策略:

  • 温度补偿法:通过温度传感器实时查表调整黑电平
  • 暗帧更新法:在每帧的消隐期采集暗帧,更新黑电平
  • 统计自适应法:利用图像直方图的最低百分位估算黑电平

我比较推荐的是暗帧更新法,实现起来相对稳定:

// 动态黑电平校正(暗帧更新法)
static uint16_t dynamic_black_level = 64;
static uint32_t frame_count = 0;

void update_black_level(uint16_t* ob_buffer, int ob_size) {
    // 每30帧更新一次黑电平
    frame_count++;
    if (frame_count < 30) return;
    frame_count = 0;
    
    // 对OB像素做中值滤波
    uint32_t sum = 0;
    int valid_count = 0;
    for (int i = 0; i < ob_size; i++) {
        if (ob_buffer[i] > 0 && ob_buffer[i] < 4095) {
            sum += ob_buffer[i];
            valid_count++;
        }
    }
    if (valid_count > 0) {
        // 用指数移动平均平滑更新
        uint16_t new_black = sum / valid_count;
        dynamic_black_level = (dynamic_black_level * 7 + new_black) / 8;
    }
}

避坑指南:动态校正最怕的是场景突变。比如从暗室走到阳光下,OB像素的统计值会剧烈变化。我曾经遇到过这种情况——黑电平突然跳变,导致连续几帧图像出现闪烁。解决方案是加平滑滤波,让黑电平缓慢变化,不要一步到位。

3.4 三种算法的选择策略

实际项目中怎么选?我个人的经验是:

  • 成本敏感型产品(比如低端IPC):全局校正就够了,省内存省带宽
  • 主流消费电子(手机、运动相机):行级校正+温度补偿,性价比最高
  • 高端专业设备(医疗内窥、工业检测):动态校正必须上,精度第一

嗯,这里要注意一点——不管用哪种算法,校正后的数据一定要做饱和处理。黑电平减过头变成负数,后面去马赛克会出大问题。

黑电平校正算法选择与实现流程 RAW图像输入 精度要求 高/中/低? 低 全局校正 单值减,速度快 中 行级校正 逐行OB像素统计,精度适中 高 动态校正 温度/暗帧自适应 饱和处理 max(P-B, 0) 校正后RAW输出 选择建议 低端产品→全局校正 主流产品→行级校正 高端设备→动态校正

3.5 实现中的常见陷阱

最后聊几个我踩过的坑:

  • OB像素数量不足:有些传感器每行只有4个OB像素,统计噪声很大。我建议至少用16个以上,取中位数比均值更鲁棒。
  • 黑电平与增益的耦合:模拟增益放大后,黑电平也会被放大。校正顺序必须是先减黑电平,再做增益,顺序反了会放大噪声。
  • 边界处理:校正后的数据可能溢出(比如12-bit RAW最大4095),一定要做clamp。我习惯用min(max(x, 0), 4095)兜底。

个人习惯:我在调试黑电平校正时,会先拍一张全黑图(盖上镜头盖),然后看直方图。如果直方图峰值不在0附近,说明黑电平没扣干净。这个方法简单粗暴,但非常有效。