暗电流与固定模式噪声(FPN)标定

说到传感器标定,暗电流和FPN是绕不开的第一道坎。我刚开始做ISP的时候,总觉得只要把增益调好、白平衡做对就行了,结果拍出来的夜景全是花屏——暗部区域一条条的竖纹,看着就头疼。后来才明白,不把暗场标定做好,后面的处理全是白搭。

说白了,暗电流就是传感器在没有光照的情况下,像素依然会积累电荷。温度越高,积累越快。而FPN呢,是每个像素对暗电流响应不一致造成的固定图案。这两兄弟如果不干掉,你的暗部信噪比永远上不去。

核心认知:暗电流是物理现象,FPN是空间分布不均。标定的目标就是测量出每个像素的暗响应,然后从RAW数据中减掉。

暗场采集方法

暗场采集听起来简单——盖上镜头盖拍一张就行了。但实际操作中,坑特别多。我踩过的坑就有好几个,这里给你列出来:

  1. 避光:盖上镜头盖还不够,最好用黑色胶带缠住镜头接口。有一次我在实验室测,结果机箱上的电源指示灯漏光,暗场图左上角明显偏亮,排查了半天。
  2. 曝光时间匹配:暗场采集的曝光时间必须和实际拍摄一致。你不能用1秒的暗场去校正30秒的长曝光,暗电流是线性累积的,时间不匹配校正就失效。
  3. 温度稳定:传感器需要预热到工作温度再采集。冷启动时的暗电流和稳定后的差很多,我建议开机后等5-10分钟再采。
  4. 多帧平均:单帧暗场有随机噪声,至少采集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校正矩阵生成流程 采集N帧暗场 帧平均降噪 减去黑电平 计算暗电流图 生成校正矩阵 保存为LUT/偏移 关键说明: • 校正矩阵 = 暗电流图 × 曝光时间 + 黑电平偏移 • 实际校正时:校正后像素 = 原始像素 - 校正矩阵对应值 • 校正矩阵通常以16位整数存储,精度控制在0.1 DN级别 • 建议每3-6个月重新标定一次,传感器会老化

生成校正矩阵的代码实现也不复杂:

// 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数据预处理的第一步,也是最基础的一步。温度是最大的变量,标定时一定要记录温度,运行时一定要考虑温度补偿。别问我怎么知道的——都是拿流片回来的芯片试出来的教训。