3. 黑电平校正(BLC)与线性化
黑电平校正,圈内人常叫它BLC。这活儿看着不起眼,却是ISP Pipeline里第一个真正意义上的“校正”环节。说白了,传感器在完全无光环境下输出的像素值不应该是0,而是一个偏高的底数——这个底数就是黑电平。如果不把它减掉,后面所有算法都会带着一个固定的直流偏置在算,暗部细节直接废掉。
我早年调过一颗安森美的传感器,黑电平居然有64个码值。当时年轻,没仔细做BLC就直接跑AWB,结果白平衡怎么调都偏蓝。后来才发现,暗电流把R通道抬高了整整一截。嗯,从那以后,我拿到任何新Sensor的第一件事,就是先看它的OB(Optical Black)区域数据。
3.1 黑电平是怎么来的
黑电平的成因主要有三个:
- 暗电流:半导体热激发产生的电子,温度越高越严重。我实测过,温度每升高10°C,暗电流差不多翻一倍。
- ADC偏移:模数转换器本身有个固定的输出偏置,不同通道可能还不一样。
- 像素复位噪声:CMOS传感器复位电平的不确定性,这个在全局快门传感器上尤其明显。
关键认知:黑电平不是噪声,是偏置。噪声可以靠降噪滤掉,偏置必须靠BLC减掉。减不干净,暗部细节就永远埋在底数里。
3.2 全局BLC vs 局部BLC
实现方式上,BLC分两种:全局BLC和局部BLC。我列个表,方便你对比选型。
| 对比项 | 全局BLC | 局部BLC |
|---|---|---|
| 原理 | 整帧减去同一个固定值 | 按区域/行/列分别计算减除值 |
| 计算量 | 极低,一次减法 | 中等,需统计OB区域均值 |
| 适用场景 | 暗电流均匀、温度稳定的Sensor | 大靶面Sensor、长曝光、温度变化大 |
| 缺点 | 无法处理行间/列间差异 | 需预留OB像素区域,占用有效像素 |
全局BLC的实现非常简单,就是一行代码的事:
// 全局BLC:整帧减去固定黑电平值
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = max(0, pixel[y][x] - black_level);
}
}
但实际项目中,我很少用全局BLC。为什么?因为暗电流在空间上不是均匀的。传感器边缘的暗电流往往比中心高,尤其是大靶面传感器。我曾经调过一个1英寸的Sensor,左上角和右下角的黑电平差了12个码值。用全局BLC,左上角减干净了,右下角还留着底数;右下角减到位了,左上角就变负值被截断了。
局部BLC的做法是:利用传感器周边的光学黑区(Optical Black,OB)像素,实时统计每行或每列的黑电平均值,然后逐行或逐列减去。
// 局部BLC:逐行统计OB区域均值并减去
for (int y = 0; y < height; y++) {
// 统计该行OB区域(假设前16列为OB像素)的均值
int sum = 0;
for (int x = 0; x < OB_WIDTH; x++) {
sum += pixel[y][x];
}
int row_black = sum / OB_WIDTH;
// 用该行的黑电平值校正整行
for (int x = OB_WIDTH; x < width; x++) {
pixel[y][x] = max(0, pixel[y][x] - row_black);
}
}
我的经验:局部BLC的OB区域宽度建议取16~32个像素。太窄了统计噪声大,太宽了占用有效像素太多。另外,OB区域要避开传感器边缘的暗角区域,否则统计值会偏低。
3.3 线性化校正——BLC的“好搭档”
线性化校正,很多人把它和BLC混为一谈。其实它们是两件事,但通常放在一起做。
BLC解决的是“零点偏移”,线性化解决的是“响应曲线弯曲”。
传感器的光电响应在理想情况下是线性的:光强翻倍,输出码值翻倍。但实际传感器在低照度段和高照度段都有非线性区。低照度段尤其严重,因为暗电流和读出噪声会压缩动态范围。
线性化校正的典型做法是分段线性拟合:
// 线性化校正:分段线性查找表
// lut[input] = output,由标定数据生成
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = lut[pixel[y][x]];
}
}
这个LUT怎么来的?需要在实验室用积分球做标定。我一般取10~15个光强点,记录传感器输出,然后做分段线性插值。注意,线性化一定要在BLC之后做,因为BLC减掉的偏置会改变曲线的截距,先做线性化再减BLC,校正就不准了。
重要提醒:线性化校正必须在Lens Shading Correction之前做。因为LSC的增益是乘性的,如果输入数据本身非线性,乘性增益会把非线性误差放大。我曾经见过一个项目,LSC之后暗角是补平了,但画面出现奇怪的条纹——查了两天才发现是线性化没做好,LSC把非线性失真放大了。
3.4 调试参数与效果评估
BLC和线性化的调试参数不多,但每个都很关键:
| 参数 | 说明 | 典型范围 |
|---|---|---|
| Black Level Target | 校正后的目标黑电平值,通常设为0或16 | 0 ~ 64 |
| OB Region Start | OB像素的起始列/行位置 | 0 ~ 32 |
| OB Region Width | OB像素的宽度 | 8 ~ 64 |
| Linearization LUT | 分段线性校正表,通常256或4096点 | 由标定生成 |
效果评估,我一般看三个指标:
- 暗场均值:盖上镜头盖拍一张全黑图,统计全图均值。理想值应该接近0(或你设定的Target)。偏差超过2个码值就要检查了。
- 暗场行间波动:逐行统计均值,看行与行之间的波动。波动超过1个码值,说明局部BLC没做好,或者OB区域统计噪声太大。
- 线性度残差:用积分球拍不同光强下的灰阶图,拟合响应曲线,看残差。残差超过1%就需要重新标定LUT。
一句话总结:BLC做不好,暗部偏色、细节丢失;线性化做不好,LSC和CCM都会跟着出错。这两个环节是ISP Pipeline的基石,值得花时间调透。
3.5 BLC与线性化在Pipeline中的位置
下面这张图展示了BLC和线性化在整个ISP Pipeline中的位置关系。注意,它们必须在去马赛克(Demosaic)之前完成,因为Bayer域的数据一旦被插值成RGB,黑电平信息就混在一起分不开了。
这张图我画了很多遍,每次给团队新人讲Pipeline都会拿出来。你注意看那个顺序约束框——BLC、线性化、LSC这三个模块的顺序是锁死的。我见过不止一个项目,因为赶进度把线性化和LSC的顺序搞反了,结果画面边缘出现奇怪的色偏,怎么调LSC都调不回来。后来发现是线性化没做,LSC的乘性增益把非线性失真放大了。