3. 黑电平的物理意义:暗电流的产生机制、黑电平在传感器中的定义、为什么需要减去黑电平
好,我们进入第三章。前两章我们把 RAW 域的基础概念和传感器结构聊清楚了。这一章,咱们来啃一个硬骨头——黑电平。
很多刚入行的朋友,拿到 RAW 图第一件事就是直接做去马赛克或者调颜色。结果发现暗部偏绿,或者画面整体有一层灰蒙蒙的雾。我当年也犯过这个错。后来被一位老前辈点醒:“你黑电平减了吗?”
嗯,这一章我们就彻底搞明白:黑电平到底是什么?它从哪来?为什么非减不可?
3.1 暗电流:传感器里的“幽灵信号”
先问一个问题:你把镜头盖盖上,让传感器完全不见光。这时候读出的一帧数据,应该是全 0 吗?
理论上是的。但实际不是。
传感器里的每个像素,本质上是一个光电二极管加一个电容。即使没有光子打进来,硅材料内部的热激发也会产生电子-空穴对。这些电子会被电容收集,形成微弱的电流。这就是暗电流。
暗电流的核心特征:
- 温度敏感:温度每升高 6-8°C,暗电流大约翻一倍。我做过一个车载摄像头项目,夏天中午传感器温度能到 70°C,暗电流比实验室 25°C 时大了将近 20 倍。
- 时间累积:曝光时间越长,暗电流积累的电荷越多。长曝光摄影里,暗电流噪声是主要噪声源。
- 像素不均匀:每个像素的暗电流大小不完全一样,这就形成了固定模式噪声(FPN)。
说白了,暗电流就是传感器在“无光”状态下自己产生的假信号。它叠加在真实的光电信号上,像幽灵一样挥之不去。
3.2 黑电平在传感器中的定义
既然暗电流存在,那传感器输出的 RAW 数据里,纯黑像素的数值就不是 0。这个“纯黑像素对应的 RAW 值”,就是黑电平。
传感器厂商会在芯片内部做一件事:在每行像素的起始位置,放置一些被金属遮挡的像素(称为光学黑像素,Optical Black Pixels)。这些像素永远不见光,只用来测量当前的暗电流水平。
我画个图帮你理解:
你看,传感器左右两侧各有一列被遮光的像素。它们输出的 RAW 值,就是当前帧的黑电平参考值。ISP 会读取这些像素的平均值,作为整帧图像的黑电平。
我的经验:不同传感器的黑电平值差异很大。我见过 8-bit RAW 的黑电平是 16,也见过 10-bit RAW 的黑电平是 64,还有 12-bit 的黑电平在 256 左右。拿到一个新传感器,第一件事就是读 datasheet 里的黑电平参数,或者自己拍一张全黑帧来统计。
3.3 为什么需要减去黑电平?
这个问题看似简单,但背后有很深的道理。我分三点来讲:
3.3.1 恢复真实的“零光”响应
如果不减黑电平,纯黑区域的 RAW 值就不是 0。比如一个 10-bit 传感器,黑电平是 64。那么纯黑像素输出是 64,而不是 0。
这会导致什么问题?
- 暗部偏色:RGB 三个通道的黑电平可能不完全一致。我遇到过一颗传感器,R 通道黑电平 64,G 通道 63,B 通道 66。如果不减,暗部就会偏青。
- 动态范围浪费:本来 0-1023 的动态范围,被压缩成了 64-1023,损失了低端 64 个码值的细节。
3.3.2 消除温度漂移的影响
暗电流随温度变化。同一颗传感器,冬天和夏天的黑电平不一样。如果不做黑电平校正,图像的整体亮度会随着温度波动。
我曾经调试一个安防摄像头,白天画面正常,到了晚上红外灯一开,传感器温度升高,画面整体变亮,暗部噪声也变大了。查了半天,发现是黑电平没有实时更新。后来改成每帧从光学黑像素读取黑电平值,问题就解决了。
3.3.3 为后续处理提供正确的数据基础
RAW 域后面的处理流程——去马赛克、白平衡、颜色校正、Gamma 映射——都假设输入数据是“零光为 0”的线性数据。如果黑电平没减干净,这些算法都会出错。
举个例子:白平衡算法通过统计场景中的“灰色”区域来估计色温。如果黑电平没减,暗部像素的 RAW 值偏高,会被误判为灰色区域,导致白平衡偏移。
注意:黑电平减法必须在 RAW 域的最前端完成,在所有其他处理之前。我见过一些方案把黑电平减法放在去马赛克之后,结果导致颜色矩阵计算错误,画面出现奇怪的色偏。
3.4 黑电平校正的两种方式
实际工程中,黑电平校正有两种主流做法:
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 全局固定值 | 从 datasheet 读取固定黑电平值,对所有像素统一减去该值 | 实现简单,计算量小 | 无法适应温度变化和像素不均匀性 |
| 逐行动态校正 | 每行读取光学黑像素的平均值,作为该行的黑电平 | 能实时跟踪暗电流变化,校正精度高 | 需要额外的行缓存和统计逻辑 |
我个人习惯用逐行动态校正。虽然硬件开销大一点,但效果确实好。特别是做手机摄像头这种对画质要求高的产品,固定值校正根本过不了评测。
3.5 黑电平校正的代码实现
最后,给一段简单的 C 代码示例,展示黑电平校正的核心逻辑:
// 黑电平校正函数
// 输入:RAW 图像数据,10-bit,黑电平参考值从光学黑像素读取
void black_level_correction(uint16_t *raw_data, int width, int height,
uint16_t black_level_r,
uint16_t black_level_gr,
uint16_t black_level_gb,
uint16_t black_level_b) {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = y * width + x;
uint16_t pixel = raw_data[idx];
// 根据 Bayer 模式选择对应的黑电平
uint16_t bl;
if ((y % 2 == 0) && (x % 2 == 0)) {
bl = black_level_r; // R 像素
} else if ((y % 2 == 0) && (x % 2 == 1)) {
bl = black_level_gr; // Gr 像素
} else if ((y % 2 == 1) && (x % 2 == 0)) {
bl = black_level_gb; // Gb 像素
} else {
bl = black_level_b; // B 像素
}
// 减去黑电平,并钳位到 0
if (pixel > bl) {
raw_data[idx] = pixel - bl;
} else {
raw_data[idx] = 0;
}
}
}
}
注意:减法后一定要做钳位(clamp),防止出现负数。有些硬件实现会直接截断低 bit,但软件实现里最好显式判断一下。