2、黑电平校正(BLC):原理、参数配置、暗电流补偿技巧
黑电平校正,圈里人常叫它BLC。说白了,就是让传感器在完全没光的时候,输出值归零。你想想看,如果一张全黑的图拍出来是灰的,那暗部细节肯定全废了。
我刚开始调ISP那会儿,总觉得BLC是个小模块,随便配几个参数就行。后来在一个夜视项目上栽了跟头——暗部偏紫,怎么调都调不回来。折腾了两天,最后发现是BLC没校准好。嗯,从那以后我再也不敢小看这个模块了。
2.1 黑电平是怎么来的?
传感器本身有暗电流,即使完全遮光,像素也会积累电荷。温度越高,暗电流越大。另外,ADC转换也有一个基准偏移。这两个因素叠加,就产生了黑电平偏移。
我习惯把黑电平拆成两部分理解:
- 行级黑电平:每行像素的暗电流不一致,导致横条纹
- 全局黑电平:整个画面的整体偏移,影响所有像素
大部分传感器会在像素阵列边缘留一些光学黑像素(OB, Optical Black),这些像素被金属遮挡,不接收光线。我们就是拿它们来估算当前的黑电平值。
核心公式:校正后像素值 = 原始像素值 - 黑电平值
就这么简单?对,但难点在于——黑电平值不是固定的,它会随温度、增益、曝光时间变化。
2.2 暗电流补偿的实战技巧
暗电流补偿,说白了就是动态跟踪黑电平的变化。我见过不少工程师直接写死一个固定值,结果画面在不同温度下颜色飘得厉害。
这里分享几个我踩过的坑:
避坑指南:我曾经在一个宽动态项目里,把BLC放在HDR合成之后做。结果暗部细节全被切掉了,因为HDR合成后的像素分布已经变了。正确的做法是:BLC必须在HDR合成之前,甚至要在去噪之前做。
暗电流补偿的典型流程:
- 读取OB像素的平均值
- 减去OB像素的目标值(通常是0或一个很小的偏移)
- 对R、Gr、Gb、B四个通道分别计算
- 做空间滤波,防止单行OB像素异常导致条纹
- 将校正值应用到有效像素区域
个人习惯:我会在OB区域取64行像素做中值滤波,而不是简单平均。因为偶尔会有坏点混入OB区域,平均值会被拉偏,中值更鲁棒。
2.3 参数配置的要点
BLC的参数其实不多,但每个参数都牵一发动全身。我整理了一个常用配置表:
| 参数名称 | 典型范围 | 我的建议 |
|---|---|---|
| OB行数 | 4~64行 | 至少16行,太少统计不稳定 |
| OB列数 | 4~32列 | 左右各留8列,避开边缘效应 |
| 目标黑电平 | 0~64 LSB | 10bit传感器建议设为16~32 |
| 滤波系数 | 0.1~0.9 | 温度变化大时用0.3,稳定场景用0.7 |
| 通道独立使能 | ON/OFF | 必须开启,R/Gr/Gb/B分开校正 |
你可能会问:目标黑电平为什么不设成0?
嗯,这里有个细节。如果硬把黑电平砍到0,暗部噪声会被截断,出现黑切现象——暗部细节丢失,看起来像死黑一片。留一点余量,反而能保留更多暗部纹理。
2.4 BLC在ISP流水线中的位置
我画了一张图,帮你理清BLC在整个ISP流程中的位置。这张图我每次培训都会拿出来讲:
从图上你能看到,BLC紧跟在RAW Sensor之后,是所有图像处理的第一步。为什么?因为后面的去噪、白平衡、颜色校正,都假设输入的黑电平已经是正确的。如果BLC没做好,后面的模块全在错误的数据上工作,越调越乱。
2.5 调试中的常见问题
我这些年遇到的BLC问题,归纳起来就三类:
- 暗部偏色:R/Gr/Gb/B四个通道的黑电平不一致,导致暗部偏紫或偏绿。解决办法是开启通道独立校正,分别统计各通道的OB像素。
- 横条纹噪声:行级黑电平没处理好。我建议增加OB行数,或者对每行单独做BLC。
- 温度漂移:开机后画面颜色慢慢变化。这是暗电流随温度变化导致的。加上温度传感器反馈,做动态补偿就能解决。
调试小技巧:我习惯在调试时先拍一张全黑图(盖上镜头盖),然后看直方图。理想情况下,直方图应该集中在0附近,且R/Gr/Gb/B四个通道的峰值位置相差不超过2个LSB。如果偏差超过5个LSB,暗部偏色肉眼可见。
2.6 代码示例:BLC参数配置
下面是一段我常用的BLC配置代码,基于一个通用的ISP驱动框架:
// BLC参数结构体
typedef struct {
uint16_t ob_rows; // OB行数
uint16_t ob_cols; // OB列数
uint16_t target_black; // 目标黑电平
float filter_coeff; // 滤波系数 0.0~1.0
bool channel_independent; // 通道独立校正
uint16_t black_level_r; // R通道黑电平
uint16_t black_level_gr; // Gr通道黑电平
uint16_t black_level_gb; // Gb通道黑电平
uint16_t black_level_b; // B通道黑电平
} blc_config_t;
// 初始化BLC参数
void blc_init(blc_config_t *cfg) {
cfg->ob_rows = 16;
cfg->ob_cols = 8;
cfg->target_black = 24; // 10bit传感器,留一点余量
cfg->filter_coeff = 0.5f; // 中等滤波强度
cfg->channel_independent = true;
// 初始值从OTP读取,这里给默认值
cfg->black_level_r = 48;
cfg->black_level_gr = 50;
cfg->black_level_gb = 49;
cfg->black_level_b = 47;
}
// 动态更新黑电平(每帧调用)
void blc_update(blc_config_t *cfg, uint16_t *ob_buffer, int len) {
if (!cfg->channel_independent) {
// 简单模式:所有通道共用
uint32_t sum = 0;
for (int i = 0; i < len; i++) {
sum += ob_buffer[i];
}
uint16_t avg = sum / len;
cfg->black_level_r = avg;
cfg->black_level_gr = avg;
cfg->black_level_gb = avg;
cfg->black_level_b = avg;
} else {
// 通道独立模式:分别统计
// 实际项目中需要根据Bayer pattern解析
// 这里简化处理
uint16_t r_avg = median_filter(ob_buffer, len/4);
uint16_t gr_avg = median_filter(ob_buffer + len/4, len/4);
uint16_t gb_avg = median_filter(ob_buffer + len/2, len/4);
uint16_t b_avg = median_filter(ob_buffer + 3*len/4, len/4);
// 一阶低通滤波,防止突变
cfg->black_level_r = cfg->filter_coeff * cfg->black_level_r
+ (1 - cfg->filter_coeff) * r_avg;
cfg->black_level_gr = cfg->filter_coeff * cfg->black_level_gr
+ (1 - cfg->filter_coeff) * gr_avg;
cfg->black_level_gb = cfg->filter_coeff * cfg->black_level_gb
+ (1 - cfg->filter_coeff) * gb_avg;
cfg->black_level_b = cfg->filter_coeff * cfg->black_level_b
+ (1 - cfg->filter_coeff) * b_avg;
}
}
这段代码里,我特别想强调中值滤波和一阶低通滤波这两个点。中值滤波能扛住OB区域的坏点,低通滤波能防止黑电平跳变导致画面闪烁。这两个技巧,是我在量产项目中反复验证过的。
总结一下BLC的核心要点:
- 位置要放对:ISP流水线的最前端
- 通道要分开:R/Gr/Gb/G独立校正
- 动态要跟上:温度、增益变化时自动补偿
- 余量要留够:目标黑电平不要设0
黑电平校正看起来简单,但它是整个图像质量的基石。基座歪了,上面盖的楼再漂亮也是危房。我见过太多工程师花大量时间调AWB、调CCM,结果暗部偏色问题一直解决不了,最后发现是BLC没做好。嗯,希望这一章能帮你少走这些弯路。