3. 黑电平与暗电流校正:从源头消除固定噪声
传感器不是完美的。它天生就有底噪。
你拍一张全黑的照片——盖上镜头盖,最短曝光——得到的像素值不会是零。可能是 512,可能是 1024,取决于你的相机和 ISO。这个偏移量,就是黑电平。而暗电流,是传感器在曝光过程中持续积累的热噪声,时间越长越明显。
我刚开始做 ISP 时,觉得黑电平校正就是简单减个值。后来发现,不同增益、不同温度下,暗电流的表现完全不同。不处理好这两步,后面的白平衡、去马赛克都会带着系统性偏差。
3.1 黑电平的定义与作用
黑电平,说白了就是传感器在零光照条件下的输出值。理想传感器应该输出 0,但实际电路有偏置电压、有 ADC 偏移,所以会有一个固定的 baseline。
核心概念:黑电平 = 传感器在完全黑暗下的平均输出值。它不是一个噪声,而是一个固定的直流偏移。
为什么要有黑电平?两个原因:
- 避免信号削波:传感器输出可能有微小负值,加上偏置后保证所有信号都在 ADC 量程内
- 统一参考点:让后续处理(白平衡、色彩校正)基于真实的零光响应
不同相机的黑电平差异很大。我见过一些手机传感器,黑电平在 10-bit RAW 里是 64,而某些工业相机只有 16。这个值不是随便设的,它和传感器设计、ADC 位数、模拟增益都有关系。
| 传感器类型 | 典型黑电平 (12-bit RAW) | 说明 |
|---|---|---|
| 手机 CMOS (IMX系列) | 256 - 512 | 偏置较高,保证低光信噪比 |
| 单反 CMOS | 128 - 256 | 偏置较低,动态范围更宽 |
| 工业/科学传感器 | 16 - 64 | 追求极致动态范围 |
| CCD (老式) | 32 - 128 | 读出噪声结构不同 |
3.2 暗电流噪声的物理成因
暗电流和黑电平不是一回事。黑电平是固定偏移,暗电流是随时间积累的热噪声。
为什么会这样?传感器里的硅晶格不是完美的。温度会让电子从价带跃迁到导带,产生自由电子。这些电子被像素收集,就成了暗电流。温度每升高 6-8°C,暗电流大约翻一倍。
注意:暗电流是温度敏感型的。长曝光时,暗电流会显著增加,形成固定的"热像素"模式。这不是坏点,但如果不校正,画面会发红发亮。
暗电流有几个特点:
- 线性积累:曝光时间越长,暗电流贡献越大
- 空间不均匀:传感器边缘和中心温度不同,暗电流分布也不同
- 增益相关:高 ISO 下,暗电流噪声被放大,更明显
我记得有一次做星空摄影的 ISP 优化,30 秒曝光下暗电流已经明显可见。如果不做暗场减除,星星周围全是热噪声。嗯,那次之后我养成了拍暗场的习惯。
3.3 黑电平减除实操方法
黑电平减除是最基础的校正。方法很简单:从每个像素值里减去黑电平值。
// 最简单的黑电平减除
uint16_t black_level = 512; // 从传感器手册或OB行读取
for (int i = 0; i < width * height; i++) {
raw[i] = (raw[i] > black_level) ? (raw[i] - black_level) : 0;
}
但实际没那么简单。不同通道的黑电平可能不同。拜耳阵列的 R、Gr、Gb、B 四个通道,因为电路布局差异,黑电平可能差几个 DN(数字数)。
我的习惯:从 RAW 文件的元数据里读取黑电平值。如果元数据不可靠,就用 OB(光学黑)行来估算。OB 行是被遮挡的像素行,它们的平均值就是当前帧的黑电平。
更精确的做法是逐通道减除:
// 逐通道黑电平减除
uint16_t bl_R = 508, bl_Gr = 512, bl_Gb = 514, bl_B = 510;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint16_t bl = get_black_level(x, y, bl_R, bl_Gr, bl_Gb, bl_B);
raw[y * width + x] = (raw[y * width + x] > bl) ?
(raw[y * width + x] - bl) : 0;
}
}
3.4 暗场减除实操方法
暗场减除比黑电平减除更进一步。它不只是减一个固定值,而是减掉一张完整的暗场图像。
暗场图像怎么拍?盖上镜头盖,用和拍摄完全相同的参数(ISO、曝光时间、温度)拍一张全黑照片。这张照片里只有暗电流和固定模式噪声。
关键流程:暗场减除 = 原始图像 - 暗场图像 + 黑电平偏移
为什么要加回黑电平偏移?因为暗场图像本身也包含黑电平。减掉暗场后,黑电平也被减掉了,需要补回来。
// 暗场减除(带黑电平补偿)
uint16_t black_level = 512;
for (int i = 0; i < width * height; i++) {
int corrected = raw[i] - dark_frame[i] + black_level;
raw[i] = (corrected > 0) ? (uint16_t)corrected : 0;
}
暗场减除的效果非常明显。我做过对比测试:
| 处理方法 | 30秒曝光暗部噪声 (σ) | 热像素残留 |
|---|---|---|
| 仅黑电平减除 | 4.2 DN | 明显 |
| 暗场减除 | 1.8 DN | 几乎不可见 |
| 暗场减除 + 中值滤波 | 1.5 DN | 完全消除 |
但暗场减除也有代价。你需要存储一张暗场图像,而且暗场会随温度变化。我建议在拍摄环境稳定时使用,比如天文摄影、显微摄影。日常拍照用黑电平减除就够了。
避坑指南:我曾经在批量处理时用了错误的暗场——ISO 100 的暗场去减 ISO 1600 的图。结果暗部全是负值,画面惨不忍睹。暗场必须和原图参数完全一致,包括增益、曝光时间、甚至传感器温度。
3.5 知识结构总览
下面这张图把黑电平、暗电流、校正方法的关系梳理清楚了。你可以把它当作本章的思维导图。
3.6 实操中的几个关键点
最后分享几个我在项目中积累的经验:
- 黑电平不是一成不变的。不同增益、不同温度下黑电平会漂移。我建议在 ISP pipeline 里做动态黑电平校正,每帧从 OB 行读取最新值。
- 暗场减除要配合坏点校正。暗场减除能消除固定模式噪声,但热像素(hot pixel)可能随时间变化。先做暗场减除,再做坏点检测,效果更好。
- 注意数据精度。减除操作后像素值可能接近零,用 uint16 存储时要小心下溢。我习惯用 int32 做中间计算,最后 clamp 到有效范围。
- 暗场库要定期更新。传感器老化后暗电流特性会变。我建议每三个月重新采集一次暗场库,或者根据温度模型自动补偿。
小技巧:如果你没有条件拍暗场,可以用多帧平均法。拍 4-8 张全黑帧,取中值或平均值,也能得到一个不错的暗场估计。虽然不如专用暗场精确,但对付日常拍摄足够了。
黑电平与暗电流校正,是整个 RAW 处理流程的第一步。这一步做扎实了,后面的色彩重建、降噪、锐化才能建立在干净的数据基础上。别小看这几行减法代码,它决定了你成片的暗部质量。