4. 累积帧与曝光控制:自适应曝光时间、动态帧累积策略、防饱和机制
低光照环境下,ToF传感器就像在黑暗中找东西。信号弱,噪声大,测距结果飘忽不定。怎么办?
我个人的经验是,核心就两招:延长曝光时间和多帧累积。但这两招用不好,反而会出问题。比如曝光太长,像素饱和了,数据全废。帧累积太多,运动物体糊成一片。
这一章,我们就来聊聊怎么把这两招用好。说白了,就是让传感器在暗光下既能看得清,又不会“过曝”或“拖影”。
核心思路:低光照下,信号强度与曝光时间成正比,信噪比与累积帧数的平方根成正比。但两者都有天花板——饱和与运动模糊。
4.1 自适应曝光时间:让传感器自己“看情况”调亮度
固定曝光时间在低光照下很尴尬。设短了,信号太弱,测距不准。设长了,强反射区域直接饱和。
我建议的做法是:让曝光时间根据实际信号强度动态调整。
4.1.1 基本原理
每个像素或每个区域,都有一个目标信号强度范围。比如,我习惯把目标设定在最大量化值的60%-80%。如果当前信号低于60%,就增加曝光时间;高于80%,就减少曝光时间。
小技巧:曝光时间调整步长不宜过大。我一般用1.2倍或0.8倍系数,每次调整后等待几帧稳定再继续。否则画面会像呼吸灯一样忽明忽暗。
4.1.2 代码示例:自适应曝光控制
// 伪代码:自适应曝光时间调整
#define TARGET_LOW 0.6f // 目标信号下限
#define TARGET_HIGH 0.8f // 目标信号上限
#define GAIN_STEP 1.2f // 调整步长
float current_exposure = 1000; // 初始曝光时间(us)
float signal_strength = getAverageSignal(); // 获取当前平均信号
if (signal_strength < TARGET_LOW) {
// 信号太弱,增加曝光
current_exposure *= GAIN_STEP;
if (current_exposure > MAX_EXPOSURE) {
current_exposure = MAX_EXPOSURE; // 限制最大曝光
}
} else if (signal_strength > TARGET_HIGH) {
// 信号太强,减少曝光
current_exposure /= GAIN_STEP;
if (current_exposure < MIN_EXPOSURE) {
current_exposure = MIN_EXPOSURE;
}
}
setExposureTime(current_exposure);
注意:曝光时间不是越长越好。超过一定阈值(比如33ms对应30fps),帧率会下降。而且长时间曝光下,环境光噪声也会累积。我曾经在一个项目中,为了追求信号强度把曝光拉到50ms,结果帧率掉到20fps,运动物体拖影严重。
4.2 动态帧累积策略:用时间换信噪比
曝光时间有上限,那信号还不够怎么办?多累积几帧。
帧累积的原理很简单:把连续N帧的深度数据叠加平均。信号是相关的,噪声是随机的,叠加后信噪比提升√N倍。
4.2.1 静态场景 vs 动态场景
这里有个矛盾:静态场景下,帧数越多越好。但场景一有运动,帧数多了就糊。
我的做法是:先检测运动,再决定帧数。
| 场景类型 | 推荐累积帧数 | 说明 |
|---|---|---|
| 静态场景(如室内监控) | 8-16帧 | 信噪比提升明显,适合高精度测量 |
| 低速运动(如人走动) | 4-8帧 | 平衡信噪比与运动模糊 |
| 高速运动(如机械臂) | 1-2帧 | 优先保证运动清晰度 |
4.2.2 滑动窗口累积
我推荐用滑动窗口的方式做帧累积。不是等N帧攒齐了再输出,而是每来一帧新数据,就替换掉最旧的一帧,重新计算平均。
这样做的好处是:输出帧率不变,只是延迟增加了N/2帧。对于实时性要求不高的场景,非常实用。
// 伪代码:滑动窗口帧累积
#define WINDOW_SIZE 8
float depth_buffer[WINDOW_SIZE];
int buffer_index = 0;
float depth_sum = 0;
void processNewFrame(float new_depth) {
// 减去最旧的一帧
depth_sum -= depth_buffer[buffer_index];
// 存入新帧
depth_buffer[buffer_index] = new_depth;
depth_sum += new_depth;
// 更新索引
buffer_index = (buffer_index + 1) % WINDOW_SIZE;
// 输出累积结果
float output_depth = depth_sum / WINDOW_SIZE;
}
关键点:帧累积不能无脑叠加。一定要配合运动检测。如果检测到运动,立即清空缓冲区,或者减少窗口大小。我曾经踩过坑——在快速移动的AGV上用了16帧累积,结果深度图全是拖影,定位直接偏了10厘米。
4.3 防饱和机制:别让像素“亮瞎眼”
低光照下我们拼命增加曝光和帧数,但高反射区域(比如镜子、金属表面)会率先饱和。饱和像素的深度值完全不可信,必须处理。
4.3.1 饱和检测
大多数ToF传感器会提供一个“饱和标志位”。如果某个像素的原始信号强度接近满量程,就标记为饱和。
我习惯的做法是:设置一个阈值,比如最大值的90%。超过这个值,就认为该像素饱和。
4.3.2 曝光回退
当检测到大量饱和像素时,说明当前曝光时间太长。需要立即回退。
但注意:不能全局回退。如果只是局部高反,全局回退会导致暗部信号更弱。
我的策略是:区域曝光控制。把画面分成多个区块,每个区块独立调整曝光时间。饱和区块减少曝光,正常区块保持不变。
// 伪代码:区域曝光回退
#define SATURATION_THRESHOLD 0.9f
#define SATURATED_PIXEL_RATIO 0.05f // 区块内饱和像素比例阈值
void checkAndRollbackExposure() {
for (int block = 0; block < NUM_BLOCKS; block++) {
float sat_ratio = getSaturatedRatio(block);
if (sat_ratio > SATURATED_PIXEL_RATIO) {
// 该区块饱和过多,减少曝光
float block_exposure = getBlockExposure(block);
block_exposure /= 1.5f;
setBlockExposure(block, block_exposure);
// 标记该区块的饱和像素为无效
markBlockInvalid(block);
}
}
}
重要提醒:饱和像素的深度值不要直接使用。即使你做了帧累积,饱和像素的数据也是错的。正确的做法是:在累积之前就把饱和像素剔除,或者用周围有效像素插值填补。
4.4 三者协同:一个完整的控制流程
自适应曝光、动态帧累积、防饱和机制,这三者不是孤立的。它们需要协同工作。
我总结了一个典型的控制流程:
- 第一步:检测当前帧的饱和像素比例。如果超过阈值,立即回退曝光时间,并标记饱和区域。
- 第二步:根据非饱和区域的信号强度,调整曝光时间,使其落在目标范围内。
- 第三步:检测场景运动量。运动小,增加累积帧数;运动大,减少累积帧数。
- 第四步:在帧累积时,排除饱和像素,只对有效像素做平均。
这个流程每帧都跑一遍。嗯,听起来复杂,但实际代码量也就几百行。关键是调参——阈值设多少,步长多大,都需要根据具体传感器和场景来微调。
个人经验:刚开始调试时,先把防饱和机制调好。饱和数据会污染整个系统。等饱和问题解决了,再调曝光和帧累积。一步一步来,别想一口吃成胖子。
好了,这一章的内容就到这里。低光照下的稳定性提升,说白了就是跟噪声和饱和做斗争。曝光时间、帧累积、防饱和,这三板斧用好,大部分场景都能搞定。