4、帧同步算法:时间戳对齐算法、基于PLL的时钟同步、动态帧率调整

多摄像头同步,说白了就是让几个摄像头“步调一致”。

硬件上我们可以用硬件信号去拉齐,但到了软件层面,尤其是不同模组、不同厂商的摄像头混用的时候,帧同步算法就成了最后的“兜底方案”。

我个人习惯把帧同步算法分成三个层次:时间戳对齐时钟同步动态帧率调整。咱们一个一个聊。

4.1 时间戳对齐算法

时间戳对齐,是最基础的一层。它的目标很简单:让所有摄像头产生的帧,在时间轴上“对齐”。

但现实很骨感。每个摄像头有自己的曝光时间、读出时间、ISP处理时间。哪怕你同时触发,出来的帧时间戳也可能差好几毫秒。

4.1.1 基于硬件SOF的时间戳

我建议优先使用硬件SOF(Start of Frame)信号产生的时间戳。这个信号是传感器内部产生的,精度高,抖动小。

代码里一般这样处理:

// 伪代码:基于SOF的时间戳对齐
void onSensorSOF(int cameraId, uint64_t sofTimestamp) {
    // 记录当前帧的SOF时间
    g_lastSofTimestamp[cameraId] = sofTimestamp;
    
    // 检查所有摄像头是否都有SOF
    bool allReady = true;
    uint64_t maxTimestamp = 0;
    for (int i = 0; i < CAMERA_COUNT; i++) {
        if (g_lastSofTimestamp[i] == 0) {
            allReady = false;
            break;
        }
        if (g_lastSofTimestamp[i] > maxTimestamp) {
            maxTimestamp = g_lastSofTimestamp[i];
        }
    }
    
    if (allReady) {
        // 以最晚的SOF为基准,对齐所有帧
        alignFramesToTimestamp(maxTimestamp);
        // 重置SOF记录,准备下一轮
        resetSofTimestamps();
    }
}
我的经验: 有些平台的SOF时间戳是单调递增的,但可能不是从0开始。记得做归一化处理,不然对齐会出问题。

4.1.2 基于PTP的全局时间戳

如果系统里有PTP(Precision Time Protocol)支持,那就更好了。PTP可以把所有设备的时间同步到纳秒级。

我在项目中遇到过一个问题:两个摄像头挂在不同的I2C总线上,SOF信号到达CPU的时间不一样。后来改用PTP时间戳,问题就解决了。

方法 精度 依赖 适用场景
硬件SOF 微秒级 传感器支持 同平台多摄
PTP全局时间 纳秒级 网络/总线支持 分布式系统
软件时间戳 毫秒级 CPU性能 低端平台

4.2 基于PLL的时钟同步

时间戳对齐解决的是“帧对齐”问题,但更深层的问题是:摄像头的时钟本身就不准

每个摄像头都有自己的晶振,频率会有微小偏差。比如一个跑30fps,另一个跑29.97fps。时间一长,帧就会慢慢错开。

4.2.1 主从时钟模式

我常用的做法是:选一个摄像头做主时钟,其他摄像头做从时钟。从时钟通过PLL锁相环去跟踪主时钟的频率。

// 伪代码:PLL时钟同步
void pllSyncLoop() {
    uint64_t masterTimestamp = getMasterSofTimestamp();
    uint64_t slaveTimestamp = getSlaveSofTimestamp();
    
    // 计算时间差
    int64_t delta = (int64_t)(masterTimestamp - slaveTimestamp);
    
    // PID控制器调整从时钟频率
    static float integral = 0;
    float error = (float)delta;
    integral += error * 0.1f;
    float adjustment = error * 0.5f + integral * 0.01f;
    
    // 调整从摄像头的帧周期
    adjustSlaveFramePeriod(adjustment);
}
注意: PLL调整不能太激进。我曾经把调整系数设得太大,结果从摄像头的帧率剧烈抖动,画面一卡一卡的。后来把积分项系数降到0.01,才稳定下来。

4.2.2 时钟漂移补偿

还有一种情况:两个摄像头之间没有直接的PLL连接,只能靠软件补偿。

嗯,这里要注意。软件补偿的本质是“预测”。你根据过去几帧的时间差,预测下一帧应该什么时候到。如果预测错了,帧就会丢或者重复。

  • 线性补偿: 假设漂移是线性的,用最小二乘法拟合。
  • 自适应补偿: 根据实时误差动态调整补偿系数。
  • 卡尔曼滤波: 精度最高,但计算量大。

4.3 动态帧率调整

最后一步,也是最灵活的一步:动态调整帧率。

你想想看,如果两个摄像头的帧率完全一样,那同步就简单了。但现实是,光照变化、场景复杂度变化,都会导致帧率波动。

4.3.1 帧率匹配策略

我一般用“慢速跟随”策略。主摄像头保持固定帧率,从摄像头动态调整去匹配。

// 伪代码:动态帧率调整
void dynamicFrameRateAdjust() {
    // 获取主摄像头的实际帧率
    float masterFps = getActualFps(MASTER_CAMERA);
    
    // 获取从摄像头的实际帧率
    float slaveFps = getActualFps(SLAVE_CAMERA);
    
    // 计算帧率差
    float fpsDelta = masterFps - slaveFps;
    
    // 如果帧率差超过阈值,调整从摄像头
    if (fabs(fpsDelta) > 0.5f) {
        // 调整VTS(垂直消隐区)来改变帧率
        int newVts = calculateVtsFromFps(masterFps);
        setSensorVts(SLAVE_CAMERA, newVts);
    }
}
核心思路: 不要追求每一帧都完美对齐,而是让误差在一个可接受的范围内(比如1ms以内)。这样系统更稳定,不容易出现抖动。

4.3.2 丢帧与插帧策略

有时候,调整帧率也来不及。比如从摄像头突然慢了半帧,怎么办?

我的做法是:丢帧优先于插帧。丢一帧,后面的帧就能对齐。插帧反而会引入额外的延迟和画面瑕疵。

  • 丢帧策略: 丢弃最旧的一帧,保持时间戳连续性。
  • 插帧策略: 复制最近的一帧,或者做运动补偿插值。
  • 混合策略: 误差小时插帧,误差大时丢帧。
避坑指南: 我曾经在某个项目里用了插帧策略,结果在快速运动场景下,插出来的帧有明显的鬼影。后来改成丢帧策略,虽然帧率降了一点,但画面干净多了。

小结

帧同步算法,说白了就是三个步骤:

  1. 时间戳对齐: 让帧在时间轴上对齐。
  2. 时钟同步: 让摄像头的时钟保持一致。
  3. 动态帧率调整: 实时修正帧率偏差。

这三个步骤环环相扣。时间戳对齐是基础,时钟同步是保障,动态帧率调整是最后的“保险丝”。

我个人习惯在项目初期就把这三个层次都考虑进去。哪怕一开始只用时间戳对齐,也要预留好PLL和动态调整的接口。不然等发现问题再改,成本就高了。

嗯,这一章就到这里。下一章咱们聊聊多摄像头同步的调试和验证方法,那又是另一番天地了。