黑电平校正(BLC):从暗电流到纯净图像

黑电平校正,圈里人常叫 BLC(Black Level Correction)。说白了,就是解决一个很基础的问题:当传感器完全看不到光时,它输出的像素值应该是多少?

理想情况当然是 0。但现实世界没这么完美。传感器在暗场下会输出一个非零的基底值,这个值如果不扣掉,整张画面就会发灰、发雾,暗部细节全被埋没。

我早年调第一款手机摄像头时,就吃过这个亏。当时觉得画面偏色,折腾了半天白平衡,最后发现是黑电平没校好。嗯,从那以后,BLC 成了我每次调试的第一道工序。

黑电平是怎么产生的?

黑电平的根源,主要是三个物理效应叠加的结果:

  • 暗电流:半导体器件在无光照时,热激发产生的电子-空穴对。温度每升高 6-8°C,暗电流翻一倍。夏天户外拍摄时,黑电平会比实验室高出一截。
  • 像素复位噪声:CMOS 传感器每个像素在读出前需要复位,复位电平本身有波动。这个波动会叠加到信号上。
  • ADC 偏移:模数转换器内部有固定的偏置电压,保证输入信号在转换范围内。这个偏置会整体抬高输出值。

这三个因素加在一起,就形成了传感器的黑电平基底。不同传感器、不同增益、不同温度下,这个基底都不一样。

关键指标:黑电平通常用 DN(Digital Number)表示。常见 10-bit 传感器的黑电平在 32-64 DN 之间,12-bit 传感器在 64-128 DN 之间。低于这个范围,说明传感器可能被过度裁剪了暗部信息。

BLC 算法原理

BLC 的核心思想很简单:从每个像素值中减去一个参考黑电平值。但具体怎么减、减多少,这里面有讲究。

最基本的公式是:

P_out = P_raw - B

其中 P_raw 是原始像素值,B 是黑电平偏移量。但实际工程中,B 不是固定值,它随增益、温度、行位置变化。

我常用的 BLC 算法分三步:

  1. 暗行/暗列采样:传感器边缘有被遮挡的像素(光学黑像素,简称 OB 像素)。这些像素接收不到光,它们的输出就是当前帧的黑电平样本。
  2. 统计滤波:对 OB 像素的采样值做中值滤波或均值滤波,剔除异常点。我习惯用中值滤波,对单点坏像素不敏感。
  3. 逐行/逐列校正:将统计得到的黑电平值,按行或按列从有效像素中减去。有些传感器还需要做二维插值,因为黑电平在画面不同位置有梯度变化。

避坑指南:我曾经遇到过一个案例,OB 像素区域有灰尘遮挡,导致采样值偏低,校正后画面暗部发紫。后来我加了一道 OB 像素有效性检测——如果某行 OB 像素的均值偏离历史值超过 3 个标准差,就丢弃该行采样,用相邻行插值替代。

BLC 校准流程

BLC 校准分为两个阶段:产线标定和在线自适应。

产线标定是在暗室中完成的,目的是建立黑电平与增益、温度的关系模型。在线自适应则是在实际使用中,利用 OB 像素实时微调黑电平值。

下面这张图展示了完整的 BLC 校准流程:

黑电平校正校准流程图 黑电平校正(BLC)校准流程 阶段一:产线标定(暗室) 暗室采集多帧 OB像素统计 建立B(G,T)模型 多增益/温度扫描 写入OTP/EEPROM 产线验证 阶段二:在线自适应(运行时) 实时读取OB行 滑动窗口滤波 逐像素减法 温度补偿查表 增益联动调整 输出校正后图像 说明:B(G,T) 模型表示黑电平是增益(Gain)和温度(Temperature)的函数

产线标定详细步骤

产线标定是 BLC 精度的基础。我一般按以下步骤操作:

步骤 操作内容 注意事项
1 暗室环境准备,遮光罩完全封闭 漏光检测:长曝光 2 秒,检查画面是否有亮斑
2 设置传感器为最小增益(通常 0dB) 记录当前传感器温度
3 连续采集 16 帧暗场图像 帧间间隔 100ms,避免传感器发热影响
4 提取 OB 像素区域,计算每行均值 剔除首尾各 2 行,边缘效应
5 对 16 帧结果做时域平均 降低随机噪声对黑电平估计的影响
6 切换增益档位(1x, 2x, 4x, 8x...),重复 2-5 至少覆盖 4 个增益点
7 改变环境温度(-10°C, 25°C, 60°C),重复全流程 温度箱控温精度 ±1°C
8 拟合 B(G,T) 模型,写入传感器 OTP 推荐使用二次多项式拟合

重要警告:产线标定时,传感器必须处于稳定热平衡状态。上电后至少等待 30 秒再开始采集。我曾经见过产线为了赶速度,上电 5 秒就开始标定,结果黑电平偏差了 8 个 DN,导致整批模组暗部偏色。返工成本极高。

在线自适应算法实现

产线标定给出了黑电平的基准模型,但实际使用中,温度变化、电源波动、传感器老化都会导致黑电平漂移。所以在线自适应是必须的。

下面是我常用的自适应算法伪代码:

// 每帧处理
void blc_adaptive(uint16_t *raw_frame, int width, int height, int ob_start_row, int ob_rows) {
    static float blc_estimate = 64.0f;  // 初始值来自产线标定
    static float alpha = 0.1f;          // 滤波系数
    
    // 1. 从OB行采样
    float ob_sum = 0.0f;
    int ob_count = 0;
    
    for (int r = ob_start_row; r < ob_start_row + ob_rows; r++) {
        for (int c = 0; c < width; c++) {
            // 剔除OB区域内的坏点(预存坏点表)
            if (!is_defect_pixel(r, c)) {
                ob_sum += raw_frame[r * width + c];
                ob_count++;
            }
        }
    }
    
    float ob_mean = ob_sum / ob_count;
    
    // 2. 一阶低通滤波,防止帧间抖动
    blc_estimate = (1.0f - alpha) * blc_estimate + alpha * ob_mean;
    
    // 3. 温度补偿:从产线模型查表
    float temp_comp = get_temp_compensation(current_temperature, current_gain);
    float blc_final = blc_estimate + temp_comp;
    
    // 4. 逐像素校正
    for (int r = 0; r < height; r++) {
        for (int c = 0; c < width; c++) {
            int pixel = raw_frame[r * width + c];
            pixel = (int)(pixel - blc_final);
            // 钳位到有效范围
            if (pixel < 0) pixel = 0;
            raw_frame[r * width + c] = (uint16_t)pixel;
        }
    }
}

经验之谈:alpha 值的选择很关键。alpha 太大,黑电平会跟着帧间噪声抖动,画面出现闪烁;alpha 太小,对温度变化的响应太慢。我一般取 0.05-0.15 之间,具体要看帧率。30fps 用 0.1,60fps 用 0.05,这样时间常数差不多。

BLC 的常见陷阱

做了这么多年 ISP,BLC 踩过的坑不少。列几个典型的:

  • OB 像素污染:镜头组装时,胶水或灰尘可能污染 OB 区域。我遇到过一批模组,OB 像素被遮光胶带压住,导致黑电平偏低,画面整体偏亮。后来在产线增加了 OB 区域亮度检测,超标即判 NG。
  • 行噪声与 BLC 耦合:有些传感器的行噪声会叠加到 OB 行上,导致 BLC 误判。解决方案是先做行噪声校正,再做 BLC,顺序不能乱。
  • 高增益下的黑电平非线性:增益超过 16x 时,黑电平与增益的关系不再是线性。我习惯在产线标定时增加高增益段的采样密度,用分段线性插值代替全局拟合。
  • BLC 后的暗部偏色:如果 R、G、B 三个通道的黑电平不一致,校正后暗部会出现色偏。需要分别对每个通道做独立的 BLC,不能共用同一个值。

嗯,BLC 看起来简单,但要做好、做稳,确实需要花心思。它是 ISP 流水线的第一关,这关过不好,后面的 AWB、CCM 都会跟着跑偏。我个人习惯在调试新传感器时,先把 BLC 的精度做到 ±1 DN 以内,再往下走。