黑电平校正(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 参数对画质的影响关系图 BLC 校正值 BLC 偏大(过校正) 暗部细节被截断 暗部偏色(偏蓝/紫) 整体对比度异常 BLC 偏小(欠校正) 暗部发灰、发雾 暗部噪声被放大 动态范围浪费 目标:BLC 值恰好等于 sensor 真实黑电平 偏大丢细节,偏小加噪声,只有刚刚好才是最优

具体到数值上,我整理了一个对照表,方便你快速判断问题:

BLC 状态 暗部 raw 值分布 直方图表现 主观画质
偏大 8-16 LSB 大量 0 值,暗部被削平 左侧陡峭截断 暗部死黑,细节丢失
偏小 8-16 LSB 暗部均值偏高 8-16 左侧整体右移 暗部发灰,噪声明显
精确校正 暗部均值接近 0,标准差小 左侧平滑下降 暗部干净,细节保留

我的经验:判断 BLC 是否准确,最快的方法是看暗部 raw 数据的直方图。如果 0 值像素占比超过 1%,说明 BLC 偏大了。如果暗部均值超过 5 LSB,说明 BLC 偏小了。

BLC 校准流程与工具使用

校准 BLC 不是拍一张黑图就完事。我见过不少团队,拿个镜头盖一盖,采 10 帧平均就完事了。结果换了个温度,画面就崩了。真正的校准流程是这样的:

  1. 准备暗室环境:确保完全无光,sensor 前用遮光布+镜头盖双重遮挡
  2. 设置目标温度:至少覆盖常温(25°C)和高温(60°C)两个点
  3. 采集多帧暗场 raw 图:每温度点采 100 帧以上,用于统计
  4. 计算每列/每行的黑电平:sensor 通常有光学黑像素(OB)区域
  5. 生成校正表:按行或按列存储 black_level 值
  6. 验证:在正常光照下拍灰卡,检查暗部偏色情况

工具方面,我常用的组合是:

工具 用途 注意事项
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 了,那个更考验耐心。