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();
}
}
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);
}
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);
}
}
4.3.2 丢帧与插帧策略
有时候,调整帧率也来不及。比如从摄像头突然慢了半帧,怎么办?
我的做法是:丢帧优先于插帧。丢一帧,后面的帧就能对齐。插帧反而会引入额外的延迟和画面瑕疵。
- 丢帧策略: 丢弃最旧的一帧,保持时间戳连续性。
- 插帧策略: 复制最近的一帧,或者做运动补偿插值。
- 混合策略: 误差小时插帧,误差大时丢帧。
小结
帧同步算法,说白了就是三个步骤:
- 时间戳对齐: 让帧在时间轴上对齐。
- 时钟同步: 让摄像头的时钟保持一致。
- 动态帧率调整: 实时修正帧率偏差。
这三个步骤环环相扣。时间戳对齐是基础,时钟同步是保障,动态帧率调整是最后的“保险丝”。
我个人习惯在项目初期就把这三个层次都考虑进去。哪怕一开始只用时间戳对齐,也要预留好PLL和动态调整的接口。不然等发现问题再改,成本就高了。
嗯,这一章就到这里。下一章咱们聊聊多摄像头同步的调试和验证方法,那又是另一番天地了。