2. 黑电平校正(BLC)
黑电平校正,圈里人常叫它BLC。这步看着简单,但做不好后面全是偏色。我刚开始调ISP的时候,就吃过这个亏——画面暗部发紫,怎么调白平衡都救不回来。后来才发现,是黑电平没扣干净。
说白了,黑电平就是传感器在完全无光环境下,依然会输出的那个“假信号”。你要是不把它减掉,暗部就不是纯黑,而是带颜色的灰。咱们这一章,就把这个事儿彻底讲透。
核心一句话:黑电平校正是RAW域的第一个校正环节,目的是消除传感器暗电流和读出电路引入的固定偏置,让“纯黑”在数字域真正归零。
2.1 黑电平是怎么产生的?
传感器像素在曝光时,即使没有光子入射,PN结的热激发也会产生电子空穴对。这个热激发电流,就叫暗电流。暗电流随温度指数增长——温度每升高6-8°C,暗电流翻一倍。我在实验室测过,同一颗传感器,25°C时暗电流约10个DN,到60°C能飙到80个DN以上。
除了暗电流,读出电路本身也有偏置。ADC的参考电压偏移、列放大器的失调电压,都会叠加一个固定的直流分量。这两个加起来,就是黑电平。
| 来源 | 特性 | 影响范围 |
|---|---|---|
| 暗电流(Dark Current) | 随温度升高、曝光时间增长而增大 | 所有像素,但不同像素有差异 |
| 读出电路偏置 | 固定值,不随温度变化 | 整帧图像,列方向可能有差异 |
避坑指南:我曾经遇到过一款传感器,黑电平在帧与帧之间会跳变1-2个DN。排查了很久才发现是电源纹波导致的。后来我在BLC模块里加了帧间平滑,才把这个抖动压下去。所以,BLC不光是减一个固定值,还要考虑稳定性。
2.2 BLC校正原理:减掉那个“假底”
校正原理其实很简单:
I_corrected = I_raw - BLC_offset
但问题来了——这个BLC_offset怎么取?
传感器厂商会在感光区周围留一圈光学黑像素(Optical Black, OB)。这些像素被金属层遮挡,收不到光,但和有效像素共用同样的读出电路。它们的输出,就是当前帧的黑电平采样。
我习惯的做法是:取OB区域所有像素的中位数,而不是均值。为什么?因为OB区域偶尔会有坏点或受串扰影响的像素,均值会被这些异常值拉偏,中位数更鲁棒。
关键参数:OB区域通常位于传感器左侧和上侧,每侧有几十到几百行/列。具体位置和大小,需要从传感器的datasheet里查。
2.3 全局BLC vs 局部BLC
全局BLC最简单——整帧图像减同一个值。适用于读出电路偏置主导、暗电流均匀的场景。但现实没那么理想。
局部BLC更精细。我见过三种主流实现:
- 行BLC:每行独立计算OB值,减去该行的偏置。适合列读出电路有行间差异的传感器。
- 列BLC:每列独立计算OB值,适合列放大器失调不一致的场景。
- 分块BLC:把画面分成M×N块,每块独立计算OB值。适合暗电流分布不均匀的大靶面传感器。
| 方法 | 精度 | 计算量 | 适用场景 |
|---|---|---|---|
| 全局BLC | 低 | 极低 | 低成本、低精度场景 |
| 行BLC | 中 | 低 | 滚动快门、行间差异明显 |
| 列BLC | 中 | 低 | 列放大器失调 |
| 分块BLC | 高 | 中 | 大靶面、温度梯度明显 |
注意:局部BLC如果OB区域像素太少(比如每行只有几个OB像素),中位数的统计稳定性会变差。我一般要求每行至少有16个OB像素,否则宁可用全局BLC加行间校正。
2.4 暗电流补偿:温度与时间的函数
暗电流不是常数。它随温度升高而增大,随曝光时间延长而累积。如果你在高温长曝光场景下还用固定BLC值,暗部会明显偏亮。
暗电流补偿的通用模型:
I_dark(T, t) = I_dark_ref × 2^((T - T_ref) / T_double) × (t / t_ref)
其中:
I_dark_ref:参考温度下的暗电流T_double:暗电流翻倍的温度增量(通常6-8°C)t_ref:参考曝光时间
我在项目中实现过一套自适应暗电流补偿:
- 在产线标定阶段,采集不同温度、不同曝光时间下的OB值,拟合出暗电流模型参数。
- 运行时,读取传感器温度寄存器(大部分工业传感器都有内置温度传感器),结合当前曝光时间,查表或计算得到动态BLC值。
- 如果温度变化剧烈(比如从室内到室外),用IIR滤波器做平滑,避免BLC值突变导致画面闪烁。
经验之谈:我曾经在安防摄像头上遇到过一个问题——夜间模式切换红外灯后,传感器温度在几秒内上升了15°C,暗电流翻了两番。如果不做动态补偿,画面暗部会从纯黑变成深灰,严重影响夜视效果。后来我加了温度查表补偿,这个问题才解决。
2.5 BLC校正的完整流程
下面这张图,是我在实际项目中总结的BLC处理流程:
2.6 代码实现:一个轻量级BLC模块
下面是我在嵌入式平台上用过的一个BLC实现,C语言风格,方便移植:
typedef struct {
uint16_t ob_left; // 左侧OB列数
uint16_t ob_top; // 顶部OB行数
uint16_t ob_width; // OB区域宽度
uint16_t ob_height; // OB区域高度
uint16_t blc_offset; // 全局BLC偏移
uint8_t mode; // 0:全局 1:行BLC 2:列BLC
int16_t temp_coeff; // 温度补偿系数
} blc_config_t;
int blc_process(uint16_t *raw, uint16_t width, uint16_t height,
blc_config_t *cfg, int16_t temp) {
if (!raw || !cfg) return -1;
// 提取OB区域,计算中位数
uint32_t ob_sum = 0;
uint16_t ob_count = 0;
for (uint16_t y = 0; y < cfg->ob_height; y++) {
for (uint16_t x = 0; x < cfg->ob_width; x++) {
ob_sum += raw[y * width + x];
ob_count++;
}
}
// 实际项目中用中位数,这里简化用均值
cfg->blc_offset = (uint16_t)(ob_sum / ob_count);
// 温度补偿
int16_t temp_delta = temp - 25; // 参考温度25°C
int16_t comp = (temp_delta * cfg->temp_coeff) >> 8;
int16_t final_offset = cfg->blc_offset + comp;
// 执行校正
for (uint32_t i = 0; i < width * height; i++) {
if (raw[i] > final_offset) {
raw[i] -= final_offset;
} else {
raw[i] = 0; // 防止下溢
}
}
return 0;
}
注意:代码里我用了均值代替中位数,是为了演示简洁。实际项目中一定要用中位数。另外,减法后要钳位到0,防止下溢导致数据翻转——我见过有人忘了这步,暗部出现白色噪点,排查了半天。
2.7 本章小结
黑电平校正,说白了就是给RAW图像“清零”。它不复杂,但细节很多。我总结几个要点:
- OB区域是核心:用好它,BLC就成功了一半。
- 中位数优于均值:抗异常点能力强。
- 温度补偿不能省:尤其是安防、车载这类宽温场景。
- 钳位防下溢:减法后记得判断,别让数据翻车。