黑电平校正基础:从数学模型到工程实践
黑电平校正,说白了就是让传感器在「全黑」环境下输出正确的零值。你想想看,一个理想的图像传感器,挡住镜头一片漆黑时,输出应该是0。但现实世界哪有这么完美?
我在项目中遇到过不少刚入行的工程师,上来就套用现成的校正公式,结果暗部细节全丢了。嗯,这里面的门道,咱们得从根上讲清楚。
黑电平的物理来源
先说说黑电平是怎么来的。传感器每个像素都有暗电流,温度越高越明显。再加上读出电路的偏置,就形成了固定的基底噪声。这个基底值,就是黑电平。
核心概念:黑电平 ≠ 噪声。它是信号的一个固定偏移量,可以校正。而噪声是随机的,只能抑制。
黑电平校正的数学模型
校正模型其实很简单。假设原始像素值为 D_raw,黑电平值为 B,校正后的像素值为 D_corrected,那么:
D_corrected = D_raw - B
等等,就这么简单?是的,数学上就这么简单。但工程实现上,麻烦事在后面。
为什么?因为 B 不是固定值。它会随温度变化、随增益变化、甚至随像素位置变化。我见过一个项目,白天调试好的校正参数,晚上温度降了10度,暗部直接偏绿。
全局校正 vs 局部校正
这就引出了两个概念:全局校正和局部校正。
| 对比项 | 全局校正 | 局部校正 |
|---|---|---|
| 校正粒度 | 整帧图像用一个B值 | 每个像素或区域用不同B值 |
| 计算量 | 低,一次减法 | 高,需要查表或插值 |
| 适用场景 | 传感器一致性好的高端芯片 | 低端传感器、大靶面CMOS |
| 暗角校正 | 无法处理 | 可以同时校正暗角 |
| 存储开销 | 1个参数 | 整张校正表 |
我个人习惯,在项目初期先用全局校正快速验证算法流程。等系统稳定了,再换成局部校正提升画质。
校正参数的计算方法
参数怎么算?最常用的方法是「暗场采集法」。
- 采集暗场:盖上镜头盖,用和正常拍摄相同的曝光时间、增益,拍N帧全黑图像。
- 多帧平均:把N帧逐像素平均,消除随机噪声的影响。
- 提取黑电平:对于全局校正,取整帧均值;对于局部校正,保留每个像素的均值。
实战技巧:N一般取16~64帧。太少噪声压不下去,太多浪费时间。我曾经试过128帧,效果提升微乎其微,但采集时间翻了好几倍。
代码实现也很直接:
import numpy as np
def compute_black_level(dark_frames):
"""
计算全局黑电平
dark_frames: shape = (N, H, W)
"""
# 多帧平均
mean_frame = np.mean(dark_frames, axis=0)
# 全局黑电平
B_global = np.mean(mean_frame)
return B_global
def compute_black_level_map(dark_frames):
"""
计算局部黑电平校正表
"""
mean_frame = np.mean(dark_frames, axis=0)
# 保留每个像素的校正值
return mean_frame
避坑指南:暗场采集的陷阱
我曾经在一个项目中吃过亏。采集暗场时,没注意传感器温度,结果校正后的图像反而出现了奇怪的条纹。
注意事项:
- 暗场采集必须在传感器达到热平衡后进行
- 曝光时间要和实际拍摄一致
- 增益设置也要一致
- 如果传感器有黑电平校准行(OB行),优先使用
黑电平校正的完整流程
下面这张图,是我在项目中总结的校正流程。你看一眼就能明白整体脉络。
校正后的截断处理
这里有个细节很多人会忽略。校正后像素值可能变成负数,尤其是暗部区域。你不能直接让负数参与后续处理,否则图像会出问题。
常用的处理方式有两种:
- 截断到0:简单粗暴,但会丢失暗部细节
- 整体加偏移:把所有像素加上一个固定值,保证最小值≥0。我建议用这种方式,暗部信息保留得更完整
def black_level_correction(raw, B, offset=64):
"""
黑电平校正,带偏移保护
raw: 原始RAW图像
B: 黑电平值或校正表
offset: 偏移量,防止负值
"""
corrected = raw.astype(np.float32) - B + offset
# 截断到有效范围
corrected = np.clip(corrected, 0, 1023) # 假设10bit RAW
return corrected.astype(np.uint16)
我的经验:offset值一般取黑电平值的1/4到1/2。太小了保护不够,太大了浪费动态范围。你可以根据实际图像直方图来调整。