ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

给启元 Q1 录一份机器人学习数据:MuJoCo 多视角采集、时间同步与轨迹回放

给启元 Q1 录一份机器人学习数据:MuJoCo 多视角采集、时间同步与轨迹回放

一段机器人录像,和一份能用于学习的数据,中间差了什么?

做具身数据时,我经常需要确认一些看起来很细的事情:图像对应动作执行前还是执行后?保存的是下发指令,还是实际关节角?三路视频的第 25 帧,真的是同一个时刻吗?这些问题没有处理好,后面训练报出的 loss 再漂亮也很难放心。

这次我用启元官方 Q1 模型,录了一份可以打开检查的小数据包:三段动作、三台虚拟相机、240 个采样时刻、720 张 RGB 图像,再把指令送回仿真器,核对它能不能复现。

同一帧的三个视角。正面能看左右手与身体的关系,侧面更容易判断前伸幅度,俯视补上平面位置。

先看看这次要采什么

任务从最简单的三段动作开始:左臂伸展、右臂伸展、双臂抬起并转腰。每段运行 4 秒,以 20 Hz 采样,每个时刻记录三路 480×360 RGB。

动作由脚本生成。每段的英文 instruction 是任务标签,不是输入给语言模型的指令。先用这种容易检查的轨迹,把采集格式跑顺,再接遥操作或策略输出会省事得多。

每行一条 episode,从同一台正面相机抽出四个时刻。采完先快速翻一遍,能发现镜头朝向、左右手标注和动作是否明显等问题。

模型使用启元官方q1_v3_mc.xml。为了把注意力放在采集上,固定骨盆、关闭机器人碰撞,保留重力和关节动力学。控制器是普通 PD,物理步长 1 ms,每 50 步更新一次目标。这里采的是关节运动,不涉及接触抓取。

完整代码和数据放在配套仓库。Ubuntu 环境可以这样复现:

gitclone https://atomgit.com/gcw_JBmPZT05/primebot-mujoco-lab.gitcdprimebot-mujoco-lab# Ubuntu 缺少 ensurepip 时,先安装 python3-venv。bashcode/setup.sh# 新的多视角采集、回放和错帧检查都在这个脚本中。MUJOCO_GL=egl .venv/bin/python code/collect_multiview.py

本文在 Python 3.10.12、MuJoCo 3.17.0、RTX 5070 Ti 上运行。生成结果在multiview/,和控制实验的results/分开存放。若已有配套环境,直接执行最后一条命令即可。

给场景装上三台“固定相机”

我把三台相机挂在worldbody下,而不是挂在手臂或头部上。它们都是外部观察相机,名字分别为front、side、overhead。

相机世界坐标位置,米朝向的观察点,米
front(1.5, 0, 1.0)(0, 0, 0.65)
side(0, 1.5, 1.0)(0, 0, 0.65)
overhead(0, 0, 2.0)(0, 0, 0.55)

三路的垂直视场角均为 45°。MuJoCo 相机沿自身−Z方向看,xyaxes指定的是自身 X、Y 轴方向,不是“看向哪里”的坐标。代码先用相机位置和观察点算出这组轴,再写入 XML。

渲染时按名字选相机就行:

renderer.update_scene(data,camera="front")rgb=renderer.render().copy()

.copy()让这一帧成为独立数组,避免后续处理共享缓冲区时把旧数据覆盖。分辨率、视场角、相机世界位置和旋转矩阵都会写入schema.json。以后做投影或换数据格式,不用再翻代码猜相机参数。

三路图像分三次画,怎么保证时间同步?

渲染不是同时发生的,但只要三次渲染之间不调用mj_step,它们看到的就是同一个仿真状态。我把采集循环写成下面这个顺序:

# 此时物理世界停在 t,先刷新派生状态。mujoco.mj_forward(model,data)t=float(data.time)state=data.qpos[q_addr].copy()velocity=data.qvel[v_addr].copy()action=make_target(frame_id)images={}forcamerain("front","side","overhead"):renderer.update_scene(data,camera=camera)images[camera]=renderer.render().copy()assertdata.time==t# 三路图像与状态都采完,再执行本次动作。for_inrange(50):apply_pd(action)mujoco.mj_step(model,data)next_state=data.qpos[q_addr].copy()

这里使用的是data.time,不是time.time()。磁盘写入、渲染耗时会改变程序跑了多久,却不应该改变样本属于哪个仿真时刻。

每段共 80 个样本,观测时刻是0.00、0.05……3.95 s。最后一个动作还会执行到4.00 s,所以“4 秒轨迹”和“最后一张图时间为 3.95 秒”并不矛盾。

采集顺序固定为“观察 → 下发 → 积分”。20 Hz 下,错一帧就是 50 ms。

文件怎么存,才方便别人接着用?

我没有把所有信息塞进一段 MP4。视频方便看,真正的样本使用 PNG、NPZ 和 JSONL 分开保存:

multiview/ ├── schema.json # 关节顺序、单位、相机参数、控制周期 ├── report.json # 汇总检查结果 ├── viewer.html # 三路画面逐帧查看 └── left_reach/ ├── front/0000.png ... # 80 张,另有 side/ 和 overhead/ ├── trajectory.npz # 状态、速度、指令、下一状态、仿真快照 ├── index.jsonl # 帧号、仿真时间、图片路径、SHA256 └── three_views.mp4 # 三路拼接预览,20 fps

right_reach、both_turn使用相同结构。每个 episode 都从初始状态重新开始,避免前一段动作的残余速度混进下一段。

核心字段的含义如下:

字段单段形状对应什么
state[80, 22]动作执行前的关节角,rad
velocity[80, 22]同时刻关节速度,rad/s
action[80, 22]随后 0.05 秒保持的绝对目标角,rad
next_state[80, 22]本次动作执行后的实际关节角
三路 RGB各 80 张同一时刻、不同视角的观察

指令和结果必须分开。action是我要求机器人去哪里,next_state是它实际到了哪里。两者的差值包含控制滞后和重力影响,不能为了凑成一个“整齐”的字段就混在一起。

index.jsonl中,每张图片除了路径,还有源帧号、仿真时间和 SHA256。文件校验值用于确认图片有没有被改动,帧号与时间用于确认它应该和哪条状态记录配对;这两种检查不能互相替代。

采完之后,我做了三种检查

先检查有没有错配。三台相机的帧号和仿真时间应与该行记录一致,相邻采样间隔应为 0.05 秒。同时检查文件存在、RGB 尺寸、校验值,以及next_state[t]是否接上state[t+1]。

只跑正常数据不够。我复制了一份索引,把 side 的第 k 帧替换成第 k+1 帧,保留它原本的源帧号和时间。单段可构造 79 条错位记录,三段共237 条,全部检出。另外删掉一行记录,连续性检查也能发现帧号断档。原始数据文件不做修改。

再把 action 重新跑一遍。从保存的初始仿真状态启动,逐条下发同样的绝对目标,并使用同样的 PD 和物理步长。三段轨迹的全部采样点,关节状态最大绝对误差都是0 rad。这是同一软件环境、相同场景和初始条件下的结果。

最后恢复快照,重新画出图像。仅保存 22 维关节角适合训练输入,却不足以完整描述用于续跑的仿真状态。因此我额外保存了mjSTATE_INTEGRATION快照:

spec=mujoco.mjtState.mjSTATE_INTEGRATION snapshot=np.empty(mujoco.mj_stateSize(model,spec))mujoco.mj_getState(model,data,snapshot,spec)# 复核时恢复快照,再刷新派生量并渲染。mujoco.mj_resetData(model,replay_data)mujoco.mj_setState(model,replay_data,snapshot,spec)mujoco.mj_forward(model,replay_data)

左侧是采集时保存的 PNG,中间是恢复状态后重渲染的画面,右侧为逐像素绝对差放大 50 倍。

这一步也暴露了一个细节:状态回放误差为零,不等于重新渲染的 RGB 逐字节相同。对每段的 240 张图做比较,结果如下;像素差按 0–255 的通道数值计算:

episode动作回放最大关节误差重渲染平均像素 MAE最大单通道像素差
左臂伸展0 rad0.0003853
右臂伸展0 rad0.0003883
双臂抬起并转腰0 rad0.0003853

这里如实保留差异,没有把“肉眼看不出”写成“完全相同”。图片 hash 仍用于校验原文件完整性,重渲染则另外计算误差。

如果错位时连元数据也被一起改掉,仅比较时间戳就不够了。以左臂轨迹为例,用恢复状态后的 side 画面去对比下一帧图像,平均像素 MAE 约为0.207。它提醒我们要保留独立的复核手段;但静止画面可能难以区分,不能把像素差当成适用于所有数据的错帧检测器。

打开检查台,比只看一条曲线直观

生成的viewer.html可以切换 episode、拖动帧号、播放三路画面,也能用左右方向键逐帧查看。它直接展示文件中的图像、源帧号和仿真时间。

检查台的实际界面。切换帧号后,三路图像一起更新。

双击文件即可打开;也可以在仓库根目录运行python3 -m http.server 8769,然后访问http://localhost:8769/multiview/viewer.html。

现在这份数据规模很小,适合检查采集流程和练习格式转换。以后接遥操作时,我会保留这套索引和回放检查,把脚本动作换成真实输入。新增相机或调整采样频率后,先跑完这三段短轨迹,比等到录了几十小时再找错帧轻松得多。

代码与参考

  • 多视角采集源码、完整样例数据与检查台
  • 启元官方 Q1 模型
  • MuJoCo 相机配置、仿真状态类型

这次最有用的收获,是让每张图都能找到对应的状态与动作,并且能回放检查。以后接遥操作或新增相机,我会保留这套索引和三段短轨迹,结合启元机器人开发者社区的模型与接口资料逐项核对;采集流程先跑得明白,后面的数据扩展才有依据。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进