3. 黑电平校正(BLC)
黑电平校正,圈内人常叫 BLC。这名字听着挺唬人,其实说白了就一件事:把传感器在「全黑」环境下不该有的那点底噪信号给抠掉。
我刚开始调 ISP 时,觉得这步最简单——不就是减个固定值嘛。后来被一块 sensor 的暗部偏色问题折腾了三天,才发现黑电平远没我想的那么单纯。
3.1 黑电平是怎么冒出来的?
先搞清楚一个问题:为什么全黑环境下,sensor 输出的像素值不是 0?
理想情况下,没有光子打到像素上,ADC 应该输出 0。但现实是,sensor 内部有暗电流、有电路噪声、有 ADC 的偏移量。这些因素叠加起来,让全黑像素也输出一个非零值。这个值,就是黑电平。
我遇到过一块 sensor,黑电平高达 64(12bit 数据)。如果不做校正,暗部全是灰蒙蒙一片,根本没法看。
黑电平的主要来源,我整理了一下:
| 来源 | 说明 | 典型影响 |
|---|---|---|
| 暗电流 | 热激发产生的电子,温度越高越严重 | 随温度漂移,长曝光时明显 |
| ADC 偏移 | 模数转换器的固定偏置 | 固定值,每颗 sensor 略有差异 |
| 像素复位噪声 | 复位操作残留的电荷 | 随机性较大,帧间不稳定 |
| 电路串扰 | 电源/时钟耦合到像素输出 | 与 sensor 布局相关 |
3.2 校正原理:到底减多少?
校正原理其实就一句话:从每个像素值里减去黑电平值。
但问题来了——黑电平值是多少?怎么测?
大多数 sensor 会预留一些「光学黑像素」(Optical Black, OB)。这些像素被金属遮挡,永远接收不到光。它们的输出,就是当前帧的黑电平参考值。
校正公式很简单:
// 12bit 数据,黑电平目标值 64
// 实际黑电平从 OB 区域统计得到
uint16_t blc_value = get_ob_mean(); // 比如 68
uint16_t target_blc = 64; // 目标黑电平
for (int i = 0; i < frame_size; i++) {
int16_t corrected = raw_data[i] - (blc_value - target_blc);
if (corrected < 0) corrected = 0;
raw_data[i] = (uint16_t)corrected;
}
这里有个细节:为什么要减到目标值而不是直接减到 0?
嗯,因为后续的 ISP 模块(比如去噪、Gamma)需要一点底数来正常工作。直接砍到 0,暗部细节就全丢了。一般 12bit 数据,目标黑电平设在 64 左右比较常见。
核心要点:BLC 不是简单减到 0,而是减到一个稳定的目标值。这个目标值要保证暗部有足够的动态范围,又不能让黑电平残留太多。
3.3 参数调试方法
调试 BLC 参数,我一般分三步走。每一步都有坑,我一个个说。
第一步:确定 OB 区域
先看 sensor datasheet,找到 OB 像素的位置和数量。有的 sensor 在左边,有的在上下两边。我见过最离谱的,OB 区域分布在四个角落,统计起来特别麻烦。
确定 OB 区域后,要算它的均值和中位数。我个人习惯用中位数——均值容易被异常像素带偏。
// 从 OB 区域统计黑电平
uint32_t sum = 0;
uint16_t min_val = 4095, max_val = 0;
for (int i = 0; i < ob_pixel_count; i++) {
uint16_t val = ob_buffer[i];
sum += val;
if (val < min_val) min_val = val;
if (val > max_val) max_val = val;
}
uint16_t mean = sum / ob_pixel_count;
// 去掉最大最小各 5% 后算均值,更鲁棒
uint16_t robust_mean = get_robust_mean(ob_buffer, ob_pixel_count, 0.05);
第二步:逐行/逐列校正
有些 sensor 的黑电平不是全图一致的。行与行之间、列与列之间,可能有差异。这时候就要做逐行或逐列的 BLC。
我调过一块卷帘快门的 sensor,每行的黑电平都不一样,从第 1 行到第 1080 行,黑电平慢慢往上漂。如果不做逐行校正,图像从上到下会有一条渐变亮带。
逐行校正的做法:
// 逐行统计 OB 区域
for (int row = 0; row < height; row++) {
uint16_t row_blc = get_row_ob_mean(row);
for (int col = 0; col < width; col++) {
int idx = row * width + col;
int16_t corrected = raw_data[idx] - (row_blc - target_blc);
if (corrected < 0) corrected = 0;
raw_data[idx] = (uint16_t)corrected;
}
}
第三步:温度补偿
这是最容易被忽略的一步。sensor 温度升高,暗电流增大,黑电平跟着涨。如果不做补偿,白天调好的参数,晚上就偏了。
我一般会在 sensor 附近放一个温度传感器,每隔几秒读一次温度,然后查表补偿黑电平目标值。
| 温度范围 | 黑电平偏移量(12bit) | 补偿策略 |
|---|---|---|
| -20°C ~ 0°C | -8 ~ -4 | 降低目标值 |
| 0°C ~ 40°C | 0 | 不补偿 |
| 40°C ~ 60°C | +4 ~ +12 | 提高目标值 |
| 60°C ~ 85°C | +12 ~ +30 | 大幅提高,同时检查暗部噪声 |
3.4 BLC 调优的常见问题
调了这么多年 BLC,踩过的坑不少。挑几个典型的说说。
- 暗部偏色:BLC 没做对,暗部会偏绿或偏紫。检查一下 R、G、B 三个通道的黑电平是不是分别校正的。有些 sensor 每个通道的黑电平不一样。
- 暗部闪烁:帧与帧之间黑电平波动太大。可以加一个 IIR 滤波器,让黑电平值平滑变化。
- 过校正:减太多,暗部变成纯黑,细节全没了。检查一下目标值是不是设得太低。
- OB 区域污染:OB 像素被光漏进来了。我遇到过镜头安装偏位,光线从侧面漏进 OB 区域,导致黑电平统计偏高。
3.5 BLC 在 ISP 流程中的位置
BLC 是 ISP 流水线的第一道工序。它处理的是 raw 域的数据,在去噪、去马赛克、白平衡之前就要做完。
为什么?因为后面的模块都假设黑电平已经被校正到标准值了。如果 BLC 没做好,后面的白平衡会算错,Gamma 会拉偏,整个图像质量都受影响。
下面这张图,是我自己画的 BLC 在 ISP 流程中的位置和内部逻辑:
你看,BLC 虽然只是 ISP 流水线里的一小步,但它直接影响后面所有模块的输入质量。我见过太多项目,图像调不好,最后追根溯源,都是 BLC 没做扎实。
所以我的建议是:别急着调后面的参数,先把 BLC 搞稳了。拍一张全黑图,看看暗部是不是均匀的、稳定的。这一步做好了,后面的调优才能站得住脚。