2、黑电平校正算法原理:全局减法的局限性、动态黑电平校正、基于行/列的黑电平校正
黑电平校正,说白了就是去掉传感器在暗场下的固定底噪。你想想看,哪怕把镜头盖死,传感器依然会输出一个非零的数值。这个数值如果不扣掉,整幅画面就会偏灰,暗部细节全被埋没了。
但校正这事儿,远没有「减一个固定值」那么简单。我在项目中踩过不少坑,今天就把这些经验掰开了讲。
2.1 全局减法的局限性
最直观的做法,就是取一帧暗场图像,算出一个全局平均值,然后从每一帧的每个像素里减去这个值。代码写起来很简单:
// 全局减法示例(伪代码)
uint16_t global_black_level = 64; // 假设暗场均值
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = (pixel[y][x] > global_black_level) ?
pixel[y][x] - global_black_level : 0;
}
}
但实际效果呢?一塌糊涂。
我遇到过这样一个案例:某款手机传感器,左边暗场均值是60,右边却到了72。用全局64去减,左边欠校正(残留4个DN),右边过校正(多减了8个DN)。结果拍出来的夜景照片,左边偏灰,右边暗部细节直接丢失。
全局减法的三大硬伤:
- 空间不均匀性:传感器不同区域的暗电流不一致,一个值管不了全幅
- 温度漂移:黑电平随温度变化,开机冷机和运行热机完全是两码事
- 增益耦合:模拟增益改变时,黑电平也会跟着变,全局减法根本跟不上
说白了,全局减法只适合玩具级应用。真正量产的产品,没人敢这么干。
2.2 动态黑电平校正
既然黑电平会变,那就动态跟踪它。怎么做?
大多数传感器会在像素阵列的边缘预留一些光学黑像素(Optical Black, OB)。这些像素被金属层遮挡,收不到光,但电路和正常像素完全一样。它们的输出,就是当前帧的实时黑电平参考。
我习惯的做法是这样的:
- 从每一帧中提取OB区域的像素值
- 剔除异常点(比如坏像素或受串扰影响的边缘像素)
- 计算OB区域的均值或中位数
- 用这个值作为当前帧的校正量
// 动态黑电平校正(基于OB区域)
uint32_t ob_sum = 0;
uint16_t ob_count = 0;
for (int y = ob_start_y; y < ob_end_y; y++) {
for (int x = ob_start_x; x < ob_end_x; x++) {
if (!is_defect_pixel(y, x)) { // 剔除坏点
ob_sum += pixel[y][x];
ob_count++;
}
}
}
uint16_t dynamic_black = ob_sum / ob_count;
// 逐像素校正
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = (pixel[y][x] > dynamic_black) ?
pixel[y][x] - dynamic_black : 0;
}
}
经验之谈:OB区域通常有4~16行/列。别用全部OB像素,边缘几行容易受光泄露影响。我一般会砍掉最外面2行,只取中间稳定的部分。
动态校正的好处很明显——它能跟上温度和增益的变化。但问题也来了:OB区域只能反映全局趋势,解决不了空间不均匀性。
2.3 基于行/列的黑电平校正
这才是工业级方案的核心。
传感器读出电路是按行扫描的。每一行在读出时,都会经过相同的模拟链(放大器、ADC)。但不同行的读出时间不同,温度分布也不同,导致行与行之间的黑电平存在差异。这种现象叫行噪声(Row Noise)或列方向固定模式噪声(Column FPN)。
我记得有一次调试一款全局快门传感器,暗场图像上出现了明显的横条纹。用全局校正根本去不掉,后来一查,每行的黑电平偏差最大达到5个DN。这就是典型的行噪声。
解决方案?逐行校正。
具体做法是:在每一行的有效像素之前,传感器会输出几个哑像素(Dummy Pixel)或行参考像素。这些像素不感光,只反映该行当前的黑电平状态。
// 基于行的黑电平校正
for (int y = 0; y < height; y++) {
// 提取当前行的参考像素(前4个哑像素)
uint32_t row_ob_sum = 0;
for (int x = 0; x < 4; x++) {
row_ob_sum += pixel[y][x]; // 假设前4列是OB
}
uint16_t row_black = row_ob_sum / 4;
// 校正当前行的有效像素
for (int x = 4; x < width; x++) {
int32_t corrected = (int32_t)pixel[y][x] - (int32_t)row_black;
pixel[y][x] = (corrected > 0) ? (uint16_t)corrected : 0;
}
}
同理,有些传感器还会提供列参考像素,用于校正列方向的不均匀性。两者结合,就是2D黑电平校正。
注意:行/列参考像素的数量通常很少(2~8个),容易受随机噪声影响。我建议做一下中值滤波或剔除最大最小值后再取平均,能有效抑制单点噪声的干扰。
2.4 三种方案的对比
| 方案 | 校正粒度 | 抗温度漂移 | 抗空间不均匀 | 硬件开销 | 适用场景 |
|---|---|---|---|---|---|
| 全局减法 | 整帧 | 差 | 差 | 极低 | 低端玩具、调试阶段 |
| 动态校正 | 整帧(实时) | 好 | 差 | 低 | 消费级摄像头、安防 |
| 行/列校正 | 逐行/逐列 | 好 | 好 | 中 | 工业相机、医疗、车载 |
从表中能看出来,行/列校正是目前最均衡的方案。但硬件上需要额外存储每行的校正值,对行缓冲(Line Buffer)有要求。
2.5 知识体系:黑电平校正的核心逻辑
下面这张图,是我自己总结的黑电平校正知识体系。它把三种方案的关系和适用边界画清楚了。
2.6 避坑指南
最后,分享几个我实际项目中踩过的坑:
- OB区域被光污染:有些传感器的OB区域密封不严,强光下会有光泄露。我曾经在户外强光测试时发现OB值异常偏高,排查了两天才找到原因。解决方案是增加OB区域的屏蔽层,或者在算法中做光泄露检测。
- 行校正引入新噪声:如果行参考像素太少(比如只有2个),随机噪声会导致校正后的图像出现新的行噪声。我建议至少用4个参考像素,并做中值滤波。
- 增益切换时的跳变:自动增益控制(AGC)切换时,黑电平会突变。如果校正跟不上,画面会出现闪烁。解决办法是在增益切换后,强制更新OB参考值,并做帧间平滑过渡。
- 温度补偿系数:不同传感器的黑电平温度系数不一样。我习惯在产线标定阶段,记录-20°C到80°C的黑电平曲线,然后做线性插值补偿。
一个小技巧:如果你用的是行/列校正,建议在ISP流水线中把黑电平校正放在最前面。因为后面的模块(如去噪、伽马校正)都会放大黑电平误差,早校正早安心。