黑电平校正(BLC)原理与调优
黑电平校正,圈里人常叫 BLC。这玩意儿看着简单,但调不好,整张图就废了。我刚开始做 ISP 时,觉得 BLC 不就是减个值嘛,后来被暗部偏色和噪声问题狠狠教育了一顿。
说白了,BLC 要解决一个核心矛盾:传感器在完全无光时,输出并不是 0。因为电路本身有暗电流、有偏置电压,读出来的 raw 数据会带一个底数。这个底数不扣掉,暗部就会发灰、发绿,甚至出现奇怪的条纹。
核心结论:BLC 是 ISP 流水线的第一道减法。它做不准,后面所有模块都在错误的数据上折腾。
BLC 的物理意义
传感器每个像素,即使盖上镜头盖、曝光时间设为 0,也会读出非零值。这个值来自三部分:
- 暗电流:硅基半导体热激发产生的电子,温度越高越明显
- 电路偏置:ADC 和读出电路为了避开噪声死区,故意加的一个直流分量
- 像素复位噪声:每个像素复位后残留的电荷
我遇到过一款 sensor,在 60°C 环境下,暗电流能飙到正常温度的 3 倍。如果不做温度补偿的 BLC,暗部直接变成紫红色。你想想看,监控摄像头装在户外暴晒,画面能看吗?
BLC 的物理模型很简单:
// 校正公式
pixel_out = pixel_raw - black_level
// 如果 black_level 是 64(10bit raw 数据范围 0-1023)
// 那么 raw 值 64 对应真实亮度 0
// raw 值 0 对应真实亮度 -64(会被 clamp 到 0)
注意这个 clamp 操作。校正后负值会被截断为 0,这意味着暗部细节的损失。所以 BLC 值设大了,暗部细节就丢了;设小了,暗部发灰,噪声还大。
BLC 参数对暗部细节和噪声的影响
这是调优中最纠结的地方。我画了张图,帮你理清关系:
具体到数值上,我整理了一个对照表,方便你快速判断问题:
| BLC 状态 | 暗部 raw 值分布 | 直方图表现 | 主观画质 |
|---|---|---|---|
| 偏大 8-16 LSB | 大量 0 值,暗部被削平 | 左侧陡峭截断 | 暗部死黑,细节丢失 |
| 偏小 8-16 LSB | 暗部均值偏高 8-16 | 左侧整体右移 | 暗部发灰,噪声明显 |
| 精确校正 | 暗部均值接近 0,标准差小 | 左侧平滑下降 | 暗部干净,细节保留 |
我的经验:判断 BLC 是否准确,最快的方法是看暗部 raw 数据的直方图。如果 0 值像素占比超过 1%,说明 BLC 偏大了。如果暗部均值超过 5 LSB,说明 BLC 偏小了。
BLC 校准流程与工具使用
校准 BLC 不是拍一张黑图就完事。我见过不少团队,拿个镜头盖一盖,采 10 帧平均就完事了。结果换了个温度,画面就崩了。真正的校准流程是这样的:
- 准备暗室环境:确保完全无光,sensor 前用遮光布+镜头盖双重遮挡
- 设置目标温度:至少覆盖常温(25°C)和高温(60°C)两个点
- 采集多帧暗场 raw 图:每温度点采 100 帧以上,用于统计
- 计算每列/每行的黑电平:sensor 通常有光学黑像素(OB)区域
- 生成校正表:按行或按列存储 black_level 值
- 验证:在正常光照下拍灰卡,检查暗部偏色情况
工具方面,我常用的组合是:
| 工具 | 用途 | 注意事项 |
|---|---|---|
| dcraw / raw2tiff | 提取 raw 数据,查看原始值 | 确保不经过任何 ISP 处理 |
| Python + numpy | 统计暗场均值、标准差、直方图 | 注意区分 R/Gr/Gb/B 四个通道 |
| Imatest / IE | 分析暗部噪声和偏色 | 适合批量验证 |
| 自写校准脚本 | 生成行/列校正表 | 建议做温度补偿插值 |
这里给一段我常用的 Python 分析脚本片段:
import numpy as np
import rawpy
# 读取暗场 raw 图
with rawpy.imread('dark_frame.dng') as raw:
raw_data = raw.raw_image.copy()
# 分离 Bayer 通道(以 RGGB 为例)
h, w = raw_data.shape
R = raw_data[0:h:2, 0:w:2]
Gr = raw_data[0:h:2, 1:w:2]
Gb = raw_data[1:h:2, 0:w:2]
B = raw_data[1:h:2, 1:w:2]
# 统计每个通道的黑电平
channels = {'R': R, 'Gr': Gr, 'Gb': Gb, 'B': B}
for name, ch in channels.items():
mean_val = np.mean(ch)
std_val = np.std(ch)
zero_pct = np.sum(ch == 0) / ch.size * 100
print(f"{name}: mean={mean_val:.1f}, std={std_val:.1f}, zero%={zero_pct:.2f}%")
曾经踩过的坑:有一次我直接用整帧均值做 BLC,忽略了行噪声。结果 sensor 每行有 2-3 LSB 的差异,校正后出现了横条纹。后来改成按行统计 OB 像素,问题才解决。记住:BLC 最好是逐行或逐列的,不要用全局均值。
调优实战要点
最后总结几个我在项目中反复验证过的要点:
- 温度补偿必须做:BLC 随温度变化,不做补偿的话,夏天和冬天的画质不一样
- 增益联动:模拟增益增大时,BLC 值也要等比放大,否则暗部噪声会异常
- OB 像素要足够:每行至少留 16-32 个光学黑像素,太少统计不准
- 校准后验证暗部 SNR:在 0.1 lux 下拍灰卡,暗部 SNR 应不低于 20dB
嗯,BLC 这块就聊到这儿。它虽然基础,但确实是 ISP 调优的基石。下一层我们就要讲 Lens Shading 了,那个更考验耐心。