4、黑电平校正(BLC):原理、暗电流补偿、动态BLC算法实现
黑电平校正,圈里人常叫BLC。说白了,就是告诉传感器:「你觉得是黑色的地方,到底是不是真的黑?」
我刚入行那会儿,总觉得BLC是个小角色。不就是减个固定值嘛,有什么好调的?直到有一次,一个项目在暗光下拍出来的画面偏紫,怎么调白平衡都救不回来。折腾了两周,最后发现是黑电平没校好,暗电流把整个底色抬上去了。嗯,从那以后,我再也不敢小看BLC了。
4.1 黑电平是怎么来的?
理想情况下,传感器完全遮光时,输出应该是0。但现实不是这样的。即使没有光,像素也会读出一些信号。这个非零的底,就是黑电平。
原因主要有两个:
- 暗电流:半导体热激发产生的电子,温度越高越严重。夏天和冬天,同一个传感器的暗电流能差好几倍。
- 读出电路偏移:ADC、放大器这些模拟电路,天生就有直流偏置。不同通道的偏移还不一样。
你想想看,如果黑电平是64,而真实信号只有10,那这10个单位的信号就被淹没了。所以必须先把这64减掉,才能看到真实的信号。
核心公式:
P校正 = P原始 - BLC值
就这么简单?嗯,原理确实简单。但难在「BLC值」怎么取。
4.2 暗电流补偿:温度和时间是两大变量
暗电流不是常数。它随温度指数增长,随曝光时间线性增长。我见过一个项目,在60℃环境下,暗电流比25℃时高了将近8倍。如果只用一组BLC参数,画面直接就废了。
所以,正经的暗电流补偿,至少要考虑两个维度:
| 变量 | 影响方式 | 补偿策略 |
|---|---|---|
| 温度 | 每升高6-8℃,暗电流翻倍 | 查表法:预存多组温度-暗电流曲线 |
| 曝光时间 | 暗电流与曝光时间成正比 | 线性模型:BLC = BLC0 + k × Texp |
| 增益 | 模拟增益放大暗电流 | 增益归一化:BLC需除以当前增益 |
我建议,在产品开发阶段,先做一组温度标定实验。把传感器放在温箱里,从-20℃到70℃,每10℃记录一次全黑帧的均值。然后拟合出一条曲线。虽然麻烦,但这是最靠谱的做法。
小技巧: 如果硬件上有温度传感器,可以实时查表补偿。如果没有,那就用曝光时间和增益做粗估计。虽然精度差一些,但比固定值强得多。
4.3 动态BLC算法:别让黑电平「飘」了
固定BLC最大的问题是什么?是它跟不上环境变化。温度变了、增益变了、甚至传感器老化都会让黑电平漂移。所以,现代ISP里都用动态BLC。
动态BLC的核心思路:从每一帧图像里,实时估算当前的黑电平。
怎么估算?传感器通常会在像素阵列边缘留几行/几列「光学黑像素(OB,Optical Black)」。这些像素被金属层遮住了,收不到光。它们的输出,理论上就是当前的黑电平。
算法流程大致如下:
- 采集OB像素:从传感器读出OB行/列的原始数据。
- 去噪预处理:去掉OB像素中的坏点和随机噪声。我习惯用中值滤波,简单有效。
- 计算均值/中值:对处理后的OB数据求统计值。注意,不同颜色通道要分开算。
- 时间域平滑:当前帧的BLC值,要和历史值做IIR滤波。防止单帧抖动导致画面闪烁。
- 逐像素校正:用平滑后的BLC值,对整帧图像做减法。
这里有个坑,我曾经踩过:OB像素的数量不能太少。太少的话,统计噪声太大,BLC值跳来跳去。我一般要求至少保留16行OB像素,每行至少64个像素。
注意: OB像素本身也有暗电流,而且边缘像素的暗电流和中心区域可能不一样。所以,直接用OB均值做全图校正,有时会引入新的误差。更精细的做法是:用OB数据拟合一个二维曲面,然后逐像素补偿。
4.4 代码实现:一个轻量级动态BLC模块
下面是我在一个项目里用过的简化版实现。去掉了平台相关的寄存器操作,只保留核心逻辑。
// 动态BLC校正模块
// 假设传感器提供16行OB像素,每行128个像素
#define OB_ROWS 16
#define OB_COLS 128
#define IIR_ALPHA 0.8f // 时间平滑系数
typedef struct {
uint16_t blc_r; // R通道黑电平
uint16_t blc_gr; // Gr通道
uint16_t gb; // Gb通道
uint16_t blc_b; // B通道
uint8_t init; // 初始化标志
} blc_state_t;
static blc_state_t g_blc = {0};
void blc_update(uint16_t ob_data[OB_ROWS][OB_COLS]) {
uint32_t sum_r = 0, sum_gr = 0, sum_gb = 0, sum_b = 0;
uint32_t cnt_r = 0, cnt_gr = 0, cnt_gb = 0, cnt_b = 0;
// 步骤1:按Bayer模式分离OB像素
for (int r = 0; r < OB_ROWS; r++) {
for (int c = 0; c < OB_COLS; c++) {
// 假设Bayer为RGGB
if (r % 2 == 0 && c % 2 == 0) { // R
sum_r += ob_data[r][c]; cnt_r++;
} else if (r % 2 == 0 && c % 2 == 1) { // Gr
sum_gr += ob_data[r][c]; cnt_gr++;
} else if (r % 2 == 1 && c % 2 == 0) { // Gb
sum_gb += ob_data[r][c]; cnt_gb++;
} else { // B
sum_b += ob_data[r][c]; cnt_b++;
}
}
}
// 步骤2:计算当前帧各通道均值
uint16_t cur_r = (uint16_t)(sum_r / cnt_r);
uint16_t cur_gr = (uint16_t)(sum_gr / cnt_gr);
uint16_t cur_gb = (uint16_t)(sum_gb / cnt_gb);
uint16_t cur_b = (uint16_t)(sum_b / cnt_b);
// 步骤3:IIR时间平滑
if (!g_blc.init) {
g_blc.blc_r = cur_r;
g_blc.blc_gr = cur_gr;
g_blc.blc_gb = cur_gb;
g_blc.blc_b = cur_b;
g_blc.init = 1;
} else {
g_blc.blc_r = (uint16_t)(IIR_ALPHA * g_blc.blc_r + (1 - IIR_ALPHA) * cur_r);
g_blc.blc_gr = (uint16_t)(IIR_ALPHA * g_blc.blc_gr + (1 - IIR_ALPHA) * cur_gr);
g_blc.blc_gb = (uint16_t)(IIR_ALPHA * g_blc.blc_gb + (1 - IIR_ALPHA) * cur_gb);
g_blc.blc_b = (uint16_t)(IIR_ALPHA * g_blc.blc_b + (1 - IIR_ALPHA) * cur_b);
}
}
// 对单个像素做BLC校正
uint16_t blc_correct_pixel(uint16_t raw, uint8_t color) {
int16_t corrected;
switch (color) {
case 0: corrected = (int16_t)raw - (int16_t)g_blc.blc_r; break;
case 1: corrected = (int16_t)raw - (int16_t)g_blc.blc_gr; break;
case 2: corrected = (int16_t)raw - (int16_t)g_blc.blc_gb; break;
case 3: corrected = (int16_t)raw - (int16_t)g_blc.blc_b; break;
default: corrected = (int16_t)raw; break;
}
// 钳位到有效范围
if (corrected < 0) corrected = 0;
return (uint16_t)corrected;
}
关于IIR系数: 我一般把alpha设在0.7到0.9之间。alpha越大,平滑越强,但响应变慢。如果场景切换很快(比如从室内走到室外),alpha太大就会看到黑电平「追不上」画面变化。这时候可以加一个检测逻辑:如果当前帧BLC和历史值偏差超过阈值,就临时降低alpha。
4.5 BLC与3A参数的协同
BLC不是孤立存在的。它和AEC、AGC、AWB都有关系。
- BLC vs AEC:如果BLC没校好,AEC统计的亮度均值就会偏大或偏小。我曾经遇到一个case,BLC偏高了10个LSB,结果AEC以为画面太亮,拼命降曝光,导致暗部细节全丢了。
- BLC vs AGC:增益会放大BLC误差。比如BLC有5个LSB的残差,增益到16倍时,残差就变成了80个LSB。所以,高增益场景下,BLC精度必须更高。
- BLC vs AWB:不同通道的BLC残差,会直接导致色偏。尤其是暗光下,BLC的R/B通道不平衡,AWB怎么调都调不回来。
我的建议是:在3A收敛之前,先把BLC跑稳。BLC不稳,后面的3A全是白费功夫。
4.6 知识结构图:BLC全景
下面这张图,把BLC的原理、变量、算法和协同关系串起来了。你可以把它当作调试时的检查清单。
4.7 调试避坑清单
最后,分享几个我踩过的坑,希望能帮你省点时间:
- OB像素被污染:有些传感器的OB区域会漏光。如果发现BLC值随场景亮度变化,先检查OB区域有没有被意外照亮。
- BLC和LSC打架:镜头阴影校正(LSC)会改变像素的增益。如果先做LSC再做BLC,BLC的减法效果会被LSC放大。我建议先BLC后LSC。
- 整数溢出的坑:BLC校正后的像素值可能变成负数。代码里一定要做钳位,否则后续模块会拿到一个巨大的正数(因为无符号整数下溢)。
- IIR初始化的瞬态:开机前几帧,IIR滤波器还没收敛。这时候的BLC值可能不准。我一般会跳过前5帧的3A统计,等BLC稳定后再启用。
BLC看起来简单,但它是整个图像质量的基石。地基没打好,上面盖的楼再漂亮也经不起推敲。嗯,希望这一章能帮你把这个地基夯实了。