黑电平校正(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 算法分三步:
- 暗行/暗列采样:传感器边缘有被遮挡的像素(光学黑像素,简称 OB 像素)。这些像素接收不到光,它们的输出就是当前帧的黑电平样本。
- 统计滤波:对 OB 像素的采样值做中值滤波或均值滤波,剔除异常点。我习惯用中值滤波,对单点坏像素不敏感。
- 逐行/逐列校正:将统计得到的黑电平值,按行或按列从有效像素中减去。有些传感器还需要做二维插值,因为黑电平在画面不同位置有梯度变化。
避坑指南:我曾经遇到过一个案例,OB 像素区域有灰尘遮挡,导致采样值偏低,校正后画面暗部发紫。后来我加了一道 OB 像素有效性检测——如果某行 OB 像素的均值偏离历史值超过 3 个标准差,就丢弃该行采样,用相邻行插值替代。
BLC 校准流程
BLC 校准分为两个阶段:产线标定和在线自适应。
产线标定是在暗室中完成的,目的是建立黑电平与增益、温度的关系模型。在线自适应则是在实际使用中,利用 OB 像素实时微调黑电平值。
下面这张图展示了完整的 BLC 校准流程:
产线标定详细步骤
产线标定是 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 以内,再往下走。