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 三者协同:一个完整的控制流程

自适应曝光、动态帧累积、防饱和机制,这三者不是孤立的。它们需要协同工作。

我总结了一个典型的控制流程:

  1. 第一步:检测当前帧的饱和像素比例。如果超过阈值,立即回退曝光时间,并标记饱和区域。
  2. 第二步:根据非饱和区域的信号强度,调整曝光时间,使其落在目标范围内。
  3. 第三步:检测场景运动量。运动小,增加累积帧数;运动大,减少累积帧数。
  4. 第四步:在帧累积时,排除饱和像素,只对有效像素做平均。

这个流程每帧都跑一遍。嗯,听起来复杂,但实际代码量也就几百行。关键是调参——阈值设多少,步长多大,都需要根据具体传感器和场景来微调。

个人经验:刚开始调试时,先把防饱和机制调好。饱和数据会污染整个系统。等饱和问题解决了,再调曝光和帧累积。一步一步来,别想一口吃成胖子。

好了,这一章的内容就到这里。低光照下的稳定性提升,说白了就是跟噪声和饱和做斗争。曝光时间、帧累积、防饱和,这三板斧用好,大部分场景都能搞定。


专注资料整理