2. 黑电平校正:从暗电流到硬件实现

黑电平校正,圈里人常叫它“减黑帧”。听起来简单,就是把每个像素减掉一个固定值。但你要是真这么干,八成会翻车。

我刚开始做ISP的时候,就吃过这个亏。当时觉得黑电平嘛,不就是个offset吗?结果调出来的图像暗部偏紫,亮部偏绿,折腾了两天才发现是黑电平没处理好。嗯,这玩意儿远比你想象的要复杂。

2.1 黑电平是怎么来的?

先说说为什么会有黑电平。理论上,你把镜头盖盖上,传感器应该输出0。但现实不是这样的——传感器会输出一个非零值。为什么?

  • 暗电流:半导体器件在无光照时,热激发产生的电子-空穴对。温度每升高6-8°C,暗电流翻一倍。我在实验室测过,60°C环境下暗电流能比室温高出一个数量级。
  • 读出噪声:像素电荷转换成电压、再经过ADC量化,这个过程会引入一个固定的偏置。说白了,就是电路自己“手抖”了一下。
  • ADC偏置:模数转换器为了保证信号不削波,会故意加一个偏置电压。这个值通常是几十个DN(Digital Number)。

关键概念:黑电平 = 暗电流贡献 + 读出噪声贡献 + ADC偏置

这个值不是固定的。它会随温度、增益、曝光时间变化。你想想看,手机拍夜景时传感器发热,黑电平跟白天拍蓝天能一样吗?

2.2 暗电流的影响有多大?

暗电流最烦人的地方,是它不均匀。我见过一个12-bit的传感器,中心区域暗电流是32 DN,边缘能到48 DN。这种差异在暗光环境下特别明显——拍出来的照片四角发亮,像漏光一样。

温度 暗电流(e-/s) 黑电平偏移(DN) 影响
25°C 10 32 可忽略
45°C 40 48 暗部偏色
60°C 120 72 动态范围损失

还有一个坑:暗电流跟像素大小有关。大像素暗电流大,小像素反而小。但小像素信噪比差,减完黑电平后噪声更明显。这就是为什么高端手机传感器像素越做越大,但黑电平校正反而更难做。

2.3 全局黑电平校正

最简单的做法:从整帧图像中减去一个常数。这个常数怎么来?

  • OB(Optical Black)行/列:传感器边缘有被遮挡的像素,它们输出的值就是黑电平参考。我习惯取OB行的中位数,不用均值——均值对异常点太敏感。
  • 暗帧法:盖上镜头盖拍一帧,存下来当参考。但这个方法有个问题:暗帧跟实际帧的噪声不相关,减完反而会引入额外噪声。
// 全局黑电平校正 - 伪代码
uint16_t black_level = get_ob_median(sensor, ob_rows);
for (each pixel in frame) {
    pixel = (pixel > black_level) ? (pixel - black_level) : 0;
}

注意:减完黑电平后一定要做clamp(钳位),防止下溢。我曾经见过一个项目,没做clamp,结果暗部像素变成65535,整张图花掉了。

全局校正的优点是简单、省资源。但缺点也很明显——它假设整个传感器暗电流是均匀的。实际上,传感器边缘的暗电流通常比中心高,尤其是大靶面传感器。这时候就需要局部校正了。

2.4 局部黑电平校正

局部校正的思路:每个像素或每个区域用不同的黑电平值。常见做法有两种:

方法一:2D LUT校正

把传感器分成M×N个网格,每个网格存一个黑电平值。校正时做双线性插值。我做过一个项目,把1080p的传感器分成16×12的网格,效果不错,但内存开销大了点——要存192个16-bit值。

// 2D LUT校正 - 伪代码
#define GRID_X 16
#define GRID_Y 12

uint16_t black_lut[GRID_Y][GRID_X];

uint16_t get_local_black(int x, int y) {
    // 双线性插值
    int gx = x * GRID_X / SENSOR_WIDTH;
    int gy = y * GRID_Y / SENSOR_HEIGHT;
    // ... 插值计算
    return interpolated_value;
}

for (each pixel at (x, y)) {
    uint16_t local_bl = get_local_black(x, y);
    pixel = (pixel > local_bl) ? (pixel - local_bl) : 0;
}

方法二:逐行/逐列校正

利用OB行和OB列的信息,分别计算每行和每列的黑电平偏移。这种方法对卷帘快门传感器特别有效——因为卷帘快门逐行读出,每行的暗电流状态不一样。

方法 精度 内存开销 计算量 适用场景
全局校正 低 几乎为0 极低 低端传感器、温度稳定环境
2D LUT 高 中等 中等 大靶面、高精度应用
逐行/逐列 中高 低 低 卷帘快门、视频

2.5 硬件实现考量

在FPGA或ASIC上做黑电平校正,有几个地方要特别注意:

流水线延迟

黑电平校正通常在ISP流水线的第一级。我建议把它放在RAW域处理的最前面,在去马赛克之前。因为黑电平没减干净,后面的白平衡、颜色校正都会出错。

位宽管理

传感器输出通常是10-bit或12-bit。减完黑电平后,有效位宽会变小。比如12-bit传感器,黑电平是64,那有效信号范围就是0-4032。这时候要不要保留12-bit输出?我个人习惯是保留,因为后面的增益处理需要这个动态范围。

实时更新

黑电平不是一成不变的。我建议每帧都重新计算OB行的中位数,然后更新校正参数。特别是视频模式下,传感器温度会逐渐升高,黑电平也在漂。不更新的话,拍几分钟后画面就偏色了。

工程技巧:在硬件实现时,可以把黑电平校正和数字增益合并成一个乘加操作:out = (in - bl) * gain。这样能省一个流水线级,减少延迟。

边界处理

传感器边缘的OB像素不参与成像,但校正时要用到它们的数据。我见过一个方案,把OB行和有效像素行分开处理,用两个FIFO分别缓存。这样OB数据不会污染有效数据,但硬件资源多了一倍。

黑电平校正流程与硬件架构 RAW传感器输入 OB行/列提取 中位数计算 参数更新 黑电平校正 (减法+钳位) 校正后RAW数据 关键设计点 • 每帧更新OB参数 • 减法后必须钳位 • 保留原始位宽 硬件资源 • 1个减法器 • 1个比较器(钳位) • 少量FIFO/寄存器

最后说一句:黑电平校正看起来是ISP里最不起眼的模块,但它的质量直接影响后面所有处理。我见过太多项目,白平衡调了半天调不好,最后发现是黑电平没校好。所以,别小看这个“减法”。