在机器人数据采集里,图像和本体传感器是否对齐,直接决定了这批数据能不能用于学习。
举个例子:
如果多路相机看到的是“夹爪接触物体前一刻”,而触觉、六维力、夹爪宽度记录的是“接触后一刻”,那么模型学到的就不是同一个物理瞬间。
对于模仿学习、力控、接触丰富的操作任务,这类误差会被进一步放大,最终表现为:
动作迟滞、接触判断错误、力反馈异常,甚至训练不收敛。
在数采系统里,我们面对的是两类数据源:
第一类是多路相机,负责采集多视角 RGB 视频,典型频率为 30/60 fps。
第二类是手持采集设备,包含触觉指尖、六维力、夹爪宽度、IMU等传感器,频率大约在 50–200 Hz 之间。
核心难点在于:
相机和手持采集设备并不天然共享同一个硬件时钟。
所以我们最终采用了一个分层同步方案:
相机之间,用硬同步。
相机和手持采集设备之间,用增强软同步。

具体来说:
- 多路相机通过同一个 PWM 触发边沿完成硬同步;
- 手持采集设备保留原始高频数据;
- 后处理阶段用主机时间、视频 PTS 和 MCU 时间戳建立跨时钟域映射。
这篇文章就围绕这个问题展开:一个多相机、多传感器机器人采集系统,如何把所有数据放到同一条可信时间轴上。
一、为什么不能只靠“开始录制时间”?
最直觉的做法是:
点击录制时记录一个 t0,然后认为第 N 帧相机时间就是:
t_camera[N] = t0 + N / fps
传感器也从同一时刻开始记录,之后按最近时间对齐。
这个方案看起来简单,但实际误差很大。
原因主要有两个。
第一,相机管道不是点击录制后立刻出第一帧。
GStreamer、Argus ISP、编码器、MKV 封装都会引入启动延迟。首帧真正进入视频流,可能比录制命令晚 100–500 ms。
第二,手持采集设备通常通过 UDP 或 SDK 上报数据。
Python 侧轮询到数据的时间,并不等于传感器真正采样的时间。UDP 传输、SDK 缓存、线程调度,都会引入 1–3 ms 级别的随机抖动。
所以,“软件开始时间”不能直接当成真实采集时间。
一个可靠的同步方案,至少要回答三个问题:
- 多路相机之间,是否真的在同一物理瞬间曝光?
- 视频文件里的第 0 帧,实际对应主机时间轴上的哪个时刻?
- 手持采集设备里的 MCU 时间戳,如何换算到主机时间轴?
二、系统里其实有三个时钟域
Thor 当前涉及三个时钟域:
第一个是 PWM 硬件时钟。
它来自 Jetson 的 pwmchip 输出,用来产生 60 Hz 方波,触发多路相机曝光。
第二个是主机 wall-clock。
也就是 Linux 里的 time.time() 或 CLOCK_REALTIME。它是软件层的公共参考时间,负责把视频和传感器日志放到同一个时间轴上。
第三个是 MCU 时钟。
它来自手持采集设备内部的晶振,用来给触觉、六维力、IMU 等传感器打时间戳。
可以这样理解:
PWM 边沿负责触发相机曝光。
MCU 时钟负责标记传感器采样。
主机时间负责在后处理阶段,把两者桥接起来。
硬同步解决的是“相机之间是否同时曝光”。
软同步解决的是“相机和手持采集设备之间如何对齐”。
三、相机间硬同步:所有快门锁到同一个 PWM 边沿
多路 GMSL2 相机通过 SG16A deserializer 共享同一路 PWM 触发信号。
每个 AR0234C 传感器都配置为 slave mode。
也就是说,相机不再由自己决定何时曝光,而是等待外部触发边沿。
可以理解成:
60 Hz PWM 同时分发给 camera 0、camera 1、camera 2,一直到 camera 10。
同一个上升沿,触发所有相机曝光。
关键配置类似这样:
hardware_sync:
enabled: true
fps: 60
sensor_trig_mode: 1
trig_pin: 0x00020007
启动时主要做两件事:
第一,配置 Jetson pwmchip,输出 60 Hz 方波。
第二,对每路相机写入 trig_mode=1,切到外部触发模式。
一旦这条链路生效,多路相机的帧采集就被物理锁定到同一个 PWM 边沿。
这是真正的硬同步。
它不依赖 Python 线程调度,也不依赖 GStreamer 什么时候拿到帧。只要相机已经进入 slave mode,曝光时刻就由硬件边沿决定。
在理想情况下,相机间同步精度可以达到亚微秒级。
而 60 fps 下,每帧间隔固定为:
1 / 60 s = 16.6667 ms
四、硬同步里最容易忽略的问题:曝光时间
硬同步不是只要给 PWM 就可以。
还必须保证:
曝光时间 + 读出时间 < PWM 周期
在 60 fps 下,一个周期是 16.6667 ms。
如果曝光时间过长,传感器还没完成读出,下一个触发边沿就已经来了,结果就可能出现异常低帧率、丢帧或者视频不稳定。
因此实际系统里,会把曝光时间上限限制在周期的 85% 左右:
exposure_us <= 0.85 × 1e6 / fps
60 fps 下约等于:
exposure_us <= 14166 us
也就是大约 14.2 ms。
这样相机在每个触发周期内都有足够余量,能够稳定响应下一次 PWM 触发。
五、逐路错峰启动,不会破坏硬同步
多路 nvarguscamerasrc 如果同时启动,容易触发 Argus ISP 或 NVMM buffer 分配竞争,出现空视频、中途 EOS,或者某一路相机没有正常进入录制状态。
所以工程上会采用 stagger 策略,也就是逐路错峰启动。例如每路相机间隔约 1 秒启动。
这会导致一个现象:
不同相机的视频起始帧编号可能不同。
但这不会破坏相机间硬同步。
原因很简单:
错峰启动只决定“这路相机从第几个 PWM 边沿开始录”。
它不决定“每一帧在哪个物理时刻曝光”。
只要相机已经进入 slave mode,后续每一帧仍然会锁定到同一组 PWM 边沿上。
所以:
错峰启动影响起点。
但不影响帧锁定。
六、手持传感器:先保留原生频率,再做逐帧对齐
手持采集设备包含多类传感器:
触觉指尖、六维力、夹爪宽度、IMU、扳机等。
这些传感器不是严格同频的。
有些接近 200 Hz,有些只有 50 Hz。
如果我们用 20 Hz 或 60 Hz 去读取一个“最新缓存快照”,所有传感器都会被压到这个读取频率,高频信息会直接丢掉。
所以采集端采用更高频率轮询 SDK,例如 500 Hz。
然后根据每个传感器自己的 MCU 时间戳,判断是否出现了新样本。
核心逻辑是:
每个传感器都维护自己的 last_recorded_ts。
如果当前 mcu_ts 和上一次不同,就说明这个传感器产生了新样本。
此时记录:
mcu_ts
host_wall_time
decoded_sensor_data
这里有两个关键点。
第一,轮询频率要高于所有传感器的原生上报频率。
500 Hz 轮询可以覆盖 200 Hz 以内的传感器更新。
第二,去重要按“单个传感器”的 MCU 时间戳做,而不是按整包快照做。
这样每类传感器都可以保留自己的真实采样频率。
最终原始数据大致会呈现这样的结构:
IMU:约 200 Hz
六维力:约 200 Hz
夹爪宽度:约 200 Hz
扳机:约 200 Hz
左触觉指尖:约 50 Hz
右触觉指尖:约 50 Hz
也就是说,一个 10 秒 episode 里:
200 Hz 传感器大约会有 2000 个样本。
50 Hz 传感器大约会有 500 个样本。
这样做的好处是,原始数据里保留了高频传感器的完整时间序列。
后续写入训练数据时,再按 60 fps 的相机帧时间做最近邻或插值。
七、基础软同步:用主机时间做桥
基础版本的软同步,使用主机 wall-clock 作为桥。
相机帧时间近似为:
相机第 N 帧时间 ≈ t_start + N / 60
传感器样本时间近似为:
传感器样本时间 ≈ Python 轮询收到该样本时的 time.time()
之后对每个相机帧时间点 t,在每个传感器时间序列里查找最近样本。
例如某一帧相机图像,需要匹配:
最近的 IMU 样本
最近的六维力样本
最近的夹爪宽度样本
最近的左触觉样本
最近的右触觉样本
这样就可以拼出该视频帧对应的 observation.state。
这个方案已经可以解决“不同传感器频率不同”的问题。
但它的精度仍然受两个误差源限制:
第一,传感器采样间隔带来的量化误差。
第二,Python 轮询时间与真实 MCU 采样时间之间的随机抖动。
理论误差大致是:
对齐误差 ≈ 传感器采样间隔 / 2 + 主机侧抖动
例如:
200 Hz 传感器,采样间隔是 5 ms,最近邻误差约 ±2.5 ms。加上 UDP 和轮询抖动后,典型误差大约是 ±3–5 ms。
50 Hz 传感器,采样间隔是 20 ms,最近邻误差约 ±10 ms。加上主机侧抖动后,典型误差大约是 ±10–13 ms。
对于很多低速任务,这已经比简单按 episode 起点对齐好很多。
但如果要处理接触瞬间、力控变化、快速夹爪动作,还需要进一步提升。
八、增强软同步一:用视频 PTS 修正相机首帧偏移
基础方案里,相机帧时间使用 t_start + N / fps 推算。
但 t_start 往往只是软件开始录制的时间,不是第一帧真正进入视频文件的时间。
视频文件里其实有一个更可靠的信息:
PTS。
录制时,nvarguscamerasrc do-timestamp=true 会把帧进入 GStreamer pipeline clock 的时间写入视频流。
episode 结束后,可以用 ffprobe 提取参考相机的视频 PTS:
ffprobe -v quiet -select_streams v:0
-show_entries packet=pts_time -of csv=p=0 camera_00.mkv
拿到首帧 PTS 后,就可以修正管道启动延迟:
actual_frame_time[N] = t_start + pts[0] + N / fps
这里为什么只需要参考一路相机?
因为多路相机已经被同一个 PWM 边沿硬同步。
只要知道参考相机第 0 帧在主机时间轴上的位置,再结合固定的 60 Hz 帧间隔,就可以重建整组相机的时间网格。
PTS 修正主要消除的是 100–500 ms 级别的全局首帧偏移。
这不是小优化。
它决定了相机和传感器是否真的处于同一个 episode 时间轴上。
九、增强软同步二:用线性回归校准 MCU 时钟
传感器样本本身带有 MCU 时间戳。
录制过程中,我们同时记录:
mcu_timestamp
host_wall_time
一个 10 秒 episode 内,200 Hz 传感器大约能提供 2000 对观测。
于是可以对每个传感器单独拟合一个线性映射:
host_time = slope × mcu_timestamp + intercept
其中:
slope 表示 MCU tick 到秒的换算比例,也反映 MCU 晶振频率。
intercept 表示 MCU 时间域相对主机时间域的偏移。
拟合完成后,就不再直接使用 Python 轮询收到样本的时间。
而是用 MCU 时间戳反推样本在主机时间轴上的时间:
calibrated_time = slope × mcu_ts + intercept
这样可以显著消除 UDP 收包、SDK 缓存和 Python 调度带来的随机抖动。
直观理解是:
单次 Python 轮询时间有噪声。
但 MCU 时间戳是传感器侧连续递增的。
我们用大量观测点估计两条时间轴之间的关系,再把单次观测噪声平均掉。
实际系统里还需要防止错误拟合。

以下情况应该自动回退到未校准的 host wall-clock:
样本数少于 10:观测点太少,不足以稳定拟合。
回归残差标准差大于 50 ms:MCU 时间戳可能不稳定、回绕或存在异常。
MCU 时间戳全为 0:说明该传感器没有上报有效时间戳。
在拟合质量正常时,传感器侧随机时间抖动可以从约 1–3 ms 降到 0.5 ms 级别。
十、最终对齐流程
一个 episode 的完整同步流程可以概括为:
第一步,开始录制。
多路相机已经处于 PWM slave mode,所有相机帧都锁定到同一个 60 Hz PWM 边沿。
第二步,手持采集设备开始记录。
采集程序以 500 Hz 轮询 SDK,并按照每个传感器自己的 MCU 时间戳去重,保留原始高频数据。
第三步,录制结束。
视频正常封装为 MKV,传感器原始样本写入 JSONL。
第四步,提取参考相机 PTS。
用视频 PTS 修正相机首帧时间偏移。
第五步,对每个传感器做 MCU 到 Host 的线性回归。
得到校准后的传感器时间轴。
第六步,生成训练数据。
对每个 60 Hz 相机帧,在各传感器时间序列中查找最近样本,组成 observation.state。
最终同步效果可以分成三层:
第一层:相机间硬同步。
解决多路相机是否在同一瞬间曝光的问题。机制是 PWM slave mode,典型精度可以达到亚微秒级。
第二层:基础软同步。
解决不同频率传感器如何对齐到相机帧的问题。机制是高频轮询、MCU 时间戳去重、最近邻匹配,典型误差约 ±3–13 ms。
第三层:增强软同步。
解决首帧偏移和轮询抖动的问题。机制是 MKV PTS 加 MCU/Host 线性回归,典型误差可以推进到约 ±0.5–1 ms。
这里的关键不是某一个单独技巧,而是分层处理:
能硬同步的部分,尽量硬同步。
不能硬同步的部分,保留原始高频数据。
最后在后处理阶段,用可验证的时间戳,把不同时间域映射到同一条时间轴上。

十一、几个工程经验
1. 原始数据和训练数据要分开
训练数据通常是 60 Hz,因为它要跟相机帧率一致。
但传感器原始数据不应该提前压成 60 Hz。
更稳妥的做法是:
原始传感器数据以 JSONL 保存。
每一行对应一个传感器样本,保留原生频率、MCU 时间戳和 host wall-clock。
训练用 parquet 再按相机帧时间生成 60 Hz 的 observation.state。
这样后续如果要做高频力控分析、IMU 积分、接触检测,还可以回到原始数据。
2. wall-clock 要尽可能稳定
软同步依赖主机 wall-clock。
采集主机需要确保系统时间稳定:
开启 NTP 同步。
录制过程中不要手动修改系统时间。
如果只测持续时间,可以使用 monotonic clock;但跨进程、跨文件对齐,仍然需要一个公共 wall-clock。
3. 回退机制非常重要
同步系统不能假设每个 episode 都完美。
某个传感器可能断流。
某段 MCU 时间戳可能异常。
ffprobe 也可能不可用。
所以每个增强步骤都应该有明确回退:
PTS 提取失败时,回退到基础帧时间网格。
MCU 拟合失败时,回退到轮询时记录的 host wall-clock。
传感器样本缺失时,在训练数据中显式标记无效,而不是默默填入一个看起来合理的值。
十二、下一步:从毫秒级走向全链路硬件时间基准
当前方案已经把多路相机间同步做到硬件级,并把相机与手持采集设备之间的对齐推进到毫秒级。
下一步可以继续往“全链路硬件时间基准”演进。
可以考虑几个方向:
第一,将 PWM 触发信号同步送入手持采集设备 MCU,让 MCU 在同一个触发边沿上打硬件时间戳。
第二,在 MCU 固件中记录触发边沿计数,把相机 frame index 和传感器样本直接关联起来。
第三,增加传感器侧硬件 timestamp,而不是只依赖 SDK 缓存快照。
第四,引入 PTP、gPTP 或等价的主机-设备时钟同步机制,进一步降低跨设备时钟漂移。
第五,为每个 episode 输出同步质量报告,包括 PTS 提取状态、MCU 回归残差、丢包率、各传感器有效样本率。
第六,建立物理验证夹具,比如 LED 闪烁、敲击触觉指尖、已知力脉冲等,用同一个事件同时刺激相机和传感器,量化真实端到端误差。
第七,在训练数据生成阶段支持更多对齐策略,例如最近邻、线性插值、窗口统计特征,并根据任务类型选择默认策略。
第八,长时间采集中增加漂移监控,避免单个 episode 内拟合正常,但跨 episode 时钟基准缓慢偏移。
结语
多相机、多传感器采集系统里,同步不是一个“录制开始时打个时间戳”就能解决的问题。
真正可靠的做法,是把问题拆开:
相机之间,用硬件边沿锁住曝光。
传感器侧,保留原始高频数据。
后处理阶段,用 PTS、MCU 时间戳和主机时间建立跨时钟域映射。
最终目标是让采集数据里的每一个图像帧、每一次触觉变化、每一个六维力脉冲、每一个夹爪动作,都能被放到同一条可信时间轴上。
只有时间轴可信,机器人学习到的因果关系才可信。
2025年,人形机器人产业迎来爆发拐点。特斯拉Optimus量产在即,华为、宇树等企业加速技术突破,行业正从“实验室研发”向“规模化落地”跃迁为打通产业链上下游协作壁垒,艾邦机器人正式组建"人形机器人全产业链交流群",覆盖金属材料、复合材料、传感器、电机、减速器等全硬件环节,助力企业精准对接资源、共享前沿技术!
扫码关注公众号,底部菜单申请进群
