暗电流与固定模式噪声(FPN)标定
说到传感器标定,暗电流和FPN是绕不开的第一道坎。我刚开始做ISP的时候,总觉得只要把增益调好、白平衡做对就行了,结果拍出来的夜景全是花屏——暗部区域一条条的竖纹,看着就头疼。后来才明白,不把暗场标定做好,后面的处理全是白搭。
说白了,暗电流就是传感器在没有光照的情况下,像素依然会积累电荷。温度越高,积累越快。而FPN呢,是每个像素对暗电流响应不一致造成的固定图案。这两兄弟如果不干掉,你的暗部信噪比永远上不去。
核心认知:暗电流是物理现象,FPN是空间分布不均。标定的目标就是测量出每个像素的暗响应,然后从RAW数据中减掉。
暗场采集方法
暗场采集听起来简单——盖上镜头盖拍一张就行了。但实际操作中,坑特别多。我踩过的坑就有好几个,这里给你列出来:
- 避光:盖上镜头盖还不够,最好用黑色胶带缠住镜头接口。有一次我在实验室测,结果机箱上的电源指示灯漏光,暗场图左上角明显偏亮,排查了半天。
- 曝光时间匹配:暗场采集的曝光时间必须和实际拍摄一致。你不能用1秒的暗场去校正30秒的长曝光,暗电流是线性累积的,时间不匹配校正就失效。
- 温度稳定:传感器需要预热到工作温度再采集。冷启动时的暗电流和稳定后的差很多,我建议开机后等5-10分钟再采。
- 多帧平均:单帧暗场有随机噪声,至少采集16帧以上做平均,才能把随机噪声压下去,提取出真正的固定模式。
我的习惯:每次标定前先拍一张全黑图看一眼直方图。如果直方图均值超过预期(比如12bit传感器超过200DN),说明可能有漏光或者温度太高了,先排查再继续。
暗电流计算
暗电流的计算其实不复杂,核心公式就一个:
// 暗电流计算
// D(t) 为曝光时间t秒时的暗场像素值
// D(0) 为0秒曝光(即黑电平)的像素值
// t 为曝光时间(秒)
暗电流 = (D(t) - D(0)) / t // 单位:DN/s
// 实际工程中,我们通常采集多组不同曝光时间的暗场
// 然后做线性拟合,斜率就是暗电流
举个例子。我采集了0秒、1秒、2秒、4秒四组暗场,每个像素的响应是这样的:
| 曝光时间 (s) | 像素(100,200) 值 (DN) | 像素(300,400) 值 (DN) |
|---|---|---|
| 0 | 64 | 64 |
| 1 | 72 | 70 |
| 2 | 80 | 76 |
| 4 | 96 | 88 |
你看,像素(100,200)的暗电流是 (96-64)/4 = 8 DN/s,而像素(300,400)是 (88-64)/4 = 6 DN/s。这就是FPN的由来——每个像素的暗电流不一样。
注意:暗电流计算前一定要先减去黑电平(Black Level)。很多传感器的黑电平不是0,而是64或128这样的固定偏移。不减黑电平算出来的暗电流是错的。
FPN校正矩阵生成
FPN校正矩阵说白了就是一张和传感器分辨率一样大的图,每个像素存一个校正值。生成流程是这样的:
生成校正矩阵的代码实现也不复杂:
// FPN校正矩阵生成示例(C风格伪代码)
#define WIDTH 1920
#define HEIGHT 1080
#define FRAMES 16
uint16_t dark_frames[FRAMES][HEIGHT][WIDTH];
uint16_t black_level = 64; // 传感器黑电平
// 1. 帧平均
float mean_frame[HEIGHT][WIDTH] = {0};
for (int f = 0; f < FRAMES; f++) {
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
mean_frame[y][x] += dark_frames[f][y][x];
}
}
}
// 除以帧数取平均
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
mean_frame[y][x] /= FRAMES;
}
}
// 2. 减去黑电平,得到暗电流偏移
int16_t fpn_offset[HEIGHT][WIDTH];
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
fpn_offset[y][x] = (int16_t)(mean_frame[y][x] - black_level);
}
}
// 3. 保存校正矩阵(偏移量)
// 校正时:corrected_pixel = raw_pixel - fpn_offset[y][x]
工程经验:校正矩阵不要用float存,太占空间。我一般用int16_t,精度够用。如果传感器有坏点,记得在矩阵里标记出来,单独做坏点校正。
温度对暗电流的影响
温度对暗电流的影响,可以用一个经验公式来描述:
// 暗电流与温度的关系(经验模型)
// I_dark(T) = I_dark(T0) × 2^((T - T0) / T_double)
// 其中:
// T_double 是暗电流翻倍的温度增量,通常为 6-8°C
// T0 是标定时的参考温度
// I_dark(T0) 是参考温度下的暗电流
你想想看,温度每升高6-8°C,暗电流就翻一倍。这是什么概念?
| 温度 (°C) | 相对暗电流 (25°C为基准) | 实际表现 |
|---|---|---|
| 25 | 1.0x | 基准温度,标定条件 |
| 33 | 2.0x | 暗电流翻倍,暗部噪点明显 |
| 41 | 4.0x | 长曝光开始出现热像素 |
| 49 | 8.0x | 暗部几乎不可用,必须降温 |
| 57 | 16.0x | 传感器可能损坏风险 |
我曾经踩过的坑:有一次在户外做测试,夏天太阳直射,传感器温度飙到50°C以上。我用25°C标定的FPN矩阵去校正,结果暗部反而更花了——因为暗电流已经翻了8倍,校正量完全不够。从那以后,我养成了两个习惯:一是标定时记录温度,二是做多温度点的校正矩阵,运行时根据温度插值。
解决温度漂移问题,工程上有几种做法:
- 多温度标定:在25°C、35°C、45°C三个温度点分别标定,运行时根据传感器温度线性插值。
- 主动温控:加TEC制冷片,把传感器温度锁定在一个固定值。高端工业相机和天文相机都这么干。
- 实时暗场更新:在每帧图像的空白行(光学黑区)采集暗电流,实时更新校正参数。
我个人比较推荐第三种方案。虽然实现起来复杂一点,但能自适应温度变化,不用频繁重新标定。很多手机ISP芯片里就内置了这个功能,叫"OB(Optical Black)Clamp"。
总结一下:暗电流和FPN标定是RAW数据预处理的第一步,也是最基础的一步。温度是最大的变量,标定时一定要记录温度,运行时一定要考虑温度补偿。别问我怎么知道的——都是拿流片回来的芯片试出来的教训。