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数据不会污染有效数据,但硬件资源多了一倍。
最后说一句:黑电平校正看起来是ISP里最不起眼的模块,但它的质量直接影响后面所有处理。我见过太多项目,白平衡调了半天调不好,最后发现是黑电平没校好。所以,别小看这个“减法”。