黑电平校正基础:从数学模型到工程实践

黑电平校正,说白了就是让传感器在「全黑」环境下输出正确的零值。你想想看,一个理想的图像传感器,挡住镜头一片漆黑时,输出应该是0。但现实世界哪有这么完美?

我在项目中遇到过不少刚入行的工程师,上来就套用现成的校正公式,结果暗部细节全丢了。嗯,这里面的门道,咱们得从根上讲清楚。

黑电平的物理来源

先说说黑电平是怎么来的。传感器每个像素都有暗电流,温度越高越明显。再加上读出电路的偏置,就形成了固定的基底噪声。这个基底值,就是黑电平。

核心概念:黑电平 ≠ 噪声。它是信号的一个固定偏移量,可以校正。而噪声是随机的,只能抑制。

黑电平校正的数学模型

校正模型其实很简单。假设原始像素值为 D_raw,黑电平值为 B,校正后的像素值为 D_corrected,那么:

D_corrected = D_raw - B

等等,就这么简单?是的,数学上就这么简单。但工程实现上,麻烦事在后面。

为什么?因为 B 不是固定值。它会随温度变化、随增益变化、甚至随像素位置变化。我见过一个项目,白天调试好的校正参数,晚上温度降了10度,暗部直接偏绿。

全局校正 vs 局部校正

这就引出了两个概念:全局校正和局部校正。

对比项 全局校正 局部校正
校正粒度 整帧图像用一个B值 每个像素或区域用不同B值
计算量 低,一次减法 高,需要查表或插值
适用场景 传感器一致性好的高端芯片 低端传感器、大靶面CMOS
暗角校正 无法处理 可以同时校正暗角
存储开销 1个参数 整张校正表

我个人习惯,在项目初期先用全局校正快速验证算法流程。等系统稳定了,再换成局部校正提升画质。

校正参数的计算方法

参数怎么算?最常用的方法是「暗场采集法」。

  1. 采集暗场:盖上镜头盖,用和正常拍摄相同的曝光时间、增益,拍N帧全黑图像。
  2. 多帧平均:把N帧逐像素平均,消除随机噪声的影响。
  3. 提取黑电平:对于全局校正,取整帧均值;对于局部校正,保留每个像素的均值。

实战技巧: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行),优先使用

黑电平校正的完整流程

下面这张图,是我在项目中总结的校正流程。你看一眼就能明白整体脉络。

黑电平校正完整流程 原始RAW图像 暗场采集(N帧) 校正参数计算 逐像素校正 校正后RAW 关键说明: 1. 暗场采集时,传感器必须达到热平衡,否则校正参数会漂移 2. 全局校正速度快,但无法处理暗角;局部校正效果好,但存储开销大 3. 校正后图像可能出现负值,需要做截断或偏移处理

校正后的截断处理

这里有个细节很多人会忽略。校正后像素值可能变成负数,尤其是暗部区域。你不能直接让负数参与后续处理,否则图像会出问题。

常用的处理方式有两种:

  • 截断到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。太小了保护不够,太大了浪费动态范围。你可以根据实际图像直方图来调整。