4、黑电平校正算法实现:全局减除与局部校正、动态黑电平跟踪算法、基于统计的OB值计算
黑电平校正,说白了就是把传感器在无光环境下产生的暗电流给扣掉。这个暗电流不是固定的,它会随着温度、增益、曝光时间变化。我刚开始调ISP时,觉得这步很简单——减一个固定值不就完了?后来发现,没那么简单。
今天咱们就把黑电平校正的几种实现方式掰开揉碎了讲。从最简单的全局减除,到更精细的局部校正,再到动态跟踪和统计计算,一层层深入。
4.1 全局减除:最直接的做法
全局减除是最基础的黑电平校正方式。做法就是:从每个像素值里减去一个固定的OB值。
// 全局减除示例
uint16_t global_ob = 64; // 假设OB值为64
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = (pixel[y][x] > global_ob) ?
(pixel[y][x] - global_ob) : 0;
}
}
这个做法简单,但有个问题:暗电流在传感器不同位置是不一样的。我遇到过一款传感器,左上角OB值只有60,右下角却到了72。用全局减除,左上角扣多了,右下角扣少了,画面就会出现亮度不均匀。
注意:全局减除只适用于暗电流一致性较好的传感器。如果传感器有较大的行间差异或区域差异,千万别用全局减除,否则画面会出现明显的“暗角”或“条纹”。
4.2 局部校正:按区域精细处理
既然暗电流分布不均匀,那就按区域来处理。局部校正的思路是:把图像分成若干块,每块单独计算OB值,然后逐块减除。
我习惯把图像分成16x16或32x32的网格。每个网格的OB值从对应的OB行(光学黑行)中统计得到。
// 局部校正示例:按行校正
uint16_t ob_per_row[height]; // 每行的OB值
// 从OB行中统计每行的OB值
for (int y = 0; y < height; y++) {
ob_per_row[y] = calculate_ob_from_ob_rows(y);
}
// 逐行减除
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pixel[y][x] = (pixel[y][x] > ob_per_row[y]) ?
(pixel[y][x] - ob_per_row[y]) : 0;
}
}
| 校正方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全局减除 | 计算量小,实现简单 | 无法处理空间不均匀性 | 暗电流一致性好的传感器 |
| 逐行校正 | 能消除行间差异 | 需要额外存储OB行数据 | 行间暗电流差异明显的传感器 |
| 逐块校正 | 能处理区域不均匀性 | 计算量较大,需要插值 | 大尺寸传感器或温度变化大的场景 |
经验之谈:我在调试一款手机摄像头时,发现画面左侧偏暗、右侧偏亮。排查了半天,最后发现是OB行在传感器左侧和右侧的暗电流不一致。改用逐行校正后,问题立刻解决了。所以,先看OB行的分布,再选校正方式。
4.3 动态黑电平跟踪算法
暗电流不是一成不变的。温度升高,暗电流增大;增益提高,暗电流也增大。如果用一个固定的OB值,画面会随着环境变化出现偏色或亮度漂移。
动态黑电平跟踪,就是实时监测OB行的变化,动态调整减除值。我常用的做法是:
- 实时统计OB行均值:每帧从OB行中采样,计算当前帧的OB值。
- 低通滤波平滑:防止单帧噪声导致OB值跳变。
- 分通道独立跟踪:R、Gr、Gb、B四个通道的暗电流不同,要分开统计。
// 动态黑电平跟踪示例
typedef struct {
float ob_r, ob_gr, ob_gb, ob_b;
float alpha; // 滤波系数,0.1~0.3
} DynamicOB;
void update_ob(DynamicOB *ob, uint16_t *ob_row_data, int ob_length) {
// 从OB行数据中统计各通道均值
float cur_ob_r = 0, cur_ob_gr = 0, cur_ob_gb = 0, cur_ob_b = 0;
// ... 统计逻辑 ...
// 一阶低通滤波
ob->ob_r = ob->ob_r * (1 - ob->alpha) + cur_ob_r * ob->alpha;
ob->ob_gr = ob->ob_gr * (1 - ob->alpha) + cur_ob_gr * ob->alpha;
ob->ob_gb = ob->ob_gb * (1 - ob->alpha) + cur_ob_gb * ob->alpha;
ob->ob_b = ob->ob_b * (1 - ob->alpha) + cur_ob_b * ob->alpha;
}
关键点:滤波系数alpha的选择很讲究。alpha太大,OB值会跟着噪声跳;alpha太小,温度变化时响应太慢。我一般取0.15~0.2,既能抑制噪声,又能跟上温度变化。
4.4 基于统计的OB值计算
有些传感器没有OB行,或者OB行被遮挡了。这时候怎么办?那就得从有效像素区域中统计OB值。
思路是:在画面中找“最暗”的区域。比如镜头盖盖上时,整个画面都是暗的,这时候统计出来的均值就是OB值。但实际场景中,画面里总有亮部,所以不能用简单的均值。
我常用的方法是直方图统计法:
- 统计整帧图像的直方图。
- 找到直方图中像素数量最多的暗部峰值。
- 以该峰值为基准,计算OB值。
// 基于直方图的OB值计算
uint16_t calculate_ob_from_histogram(uint32_t *hist, int bins) {
int peak_bin = 0;
uint32_t max_count = 0;
// 在暗部区域(前20%的bin)找峰值
for (int i = 0; i < bins / 5; i++) {
if (hist[i] > max_count) {
max_count = hist[i];
peak_bin = i;
}
}
// 以峰值bin为中心,计算加权均值
uint32_t sum = 0, count = 0;
int start = max(0, peak_bin - 2);
int end = min(bins - 1, peak_bin + 2);
for (int i = start; i <= end; i++) {
sum += i * hist[i];
count += hist[i];
}
return (count > 0) ? (uint16_t)(sum / count) : 0;
}
注意:基于统计的方法有个前提——画面中必须有足够的暗部区域。如果整个画面都是亮部(比如对着太阳拍),统计出来的OB值会偏大,导致暗部被过度减除,出现“黑切”现象。所以,这种方法只适合作为辅助手段,不能完全替代OB行。
4.5 知识结构图
下面这张图总结了黑电平校正的几种实现方式及其关系:
4.6 避坑指南
做黑电平校正这么多年,我踩过不少坑。分享几个典型的:
- OB行被遮挡:有一次客户反馈画面偏色,查了半天发现是镜头模组装配时把OB行遮住了。OB行读出来的全是0,动态跟踪直接崩了。后来我加了一个OB行有效性检测,如果OB行值异常,就回退到基于统计的方法。
- 温度突变导致OB跳变:手机从空调房拿到室外,温度骤升,暗电流瞬间增大。如果滤波系数太小,OB值跟不上,画面会先偏色再慢慢恢复。我后来加了一个温度传感器联动,温度变化大时自动增大滤波系数。
- 分通道OB差异:R通道和B通道的暗电流差异最大,Gr和Gb也有细微差别。如果不分开统计,画面会出现固定的色偏。尤其是低照度下,这个色偏特别明显。
我的建议:调试黑电平校正时,先做静态测试——固定温度、固定增益,看OB值的稳定性。再做动态测试——改变温度或增益,看OB值的跟踪速度。两个测试都过了,才能上线。