← 返回文章库

具身智能的数据从哪来?unitree_sim_isaaclab 仿真环境解读

最后更新 2026-08-23
⏱ 约 15 分钟 🟡 涉接线/强电
你将学到
  • 搞清楚模仿学习需要什么形态的数据,以及真机采集贵在哪
  • 读懂 unitree_sim_isaaclab 的三种运行模式:遥操作采集、数据回放、数据生成
  • 知道这个仿真环境在「采数据 → 转格式 → 训模型 → 回评估」这条链路里占哪一格

你想训一个模型,让机器人把桌上的杯子拿起来,放到指定位置。

算法有现成的,模型结构也不用自己发明。然后你卡在了第一步:数据在哪。

这个问题在别的 AI 方向上很少这么尖锐。做图像分类,网上有海量带标签的图;做语言模型,整个互联网都是语料。可机器人操作任务需要的东西,互联网上一份都没有——它需要的是「这一帧画面里,机器人的每个关节处在什么位置,然后它下一步把每个关节转到了哪里」。这种数据不存在于任何公开语料里,只能一条一条采出来。

宇树开源的 unitree_sim_isaaclab 就是干这件事的工具之一。这篇我们把它读一遍,重点不在怎么装环境(那部分官方文档写得比我清楚),而在于它在整条数据链路里占哪一格,以及为什么这一格必须存在

先说边界:本文基于该仓库公开的 README、目录结构与文件清单。我手上没有 G1 也没有对应的显卡环境,所以不会出现任何「我跑起来之后效果如何」的描述,涉及运行行为的地方一律以官方文档的说法为准。

一条「训练数据」到底长什么样

先把需求说清楚,不然看仓库时抓不住重点。

模仿学习的思路朴素得近乎笨:让模型看人是怎么做的,然后学着做。 它需要的一条样本是一个二元组:

观测 observation  →  动作 action

观测这边通常包含两类东西:

  • 外部感知:相机图像,让模型知道杯子在哪、桌子有多高、手离目标还差多少
  • 本体状态:机器人自己每个关节当前的角度、速度,末端夹爪张开了多少

动作那边是模型要输出的东西——下一时刻每个关节的目标位置。

关键在于这两者必须成对,而且时间对齐。图像是 t 时刻拍的,动作就必须是 t 时刻发出的那条指令。差一帧,模型学到的因果关系就是错的:它会以为「看到杯子已经在手里了」应该输出「张开手指去抓」。这类错位不会让训练报错,只会让模型表现莫名其妙地烂,排查起来极其痛苦。

一整段连续的观测-动作序列叫一个 episode,也就是「一次完整的任务演示」。训练要的是成千上万个 episode。

真机采这种数据,贵在哪

理解了数据形态,就能理解成本。

得有人。 这种数据没法自动产生,必须由人通过遥操作设备把机器人当提线木偶一样操控着完成任务。这一路的接入方式可以看遥操作那一篇,宇树这边对应的是 xr_teleoperate 项目。人力成本是实打实的。

一次只能采一条。 真机是物理实体,同一时刻只能演示一个任务。没有并行这一说。

机器人会坏。 抓取任务里手指反复接触物体,减速器、编码器、指尖胶皮都是消耗品。而且一旦策略跑飞了撞到桌子,维修停机是按天算的。

场景布置耗时。 每采完一条,杯子得摆回原位,光照要保持一致,桌面要清干净。这个动作重复几千遍,采集效率被它锁死。

仿真绕开的正是这四条。它不能绕开的那部分,我们放到后面讲。

这个仓库提供了什么

README 开头一句话把定位说完了:这个项目基于 Isaac Lab 构建,用来在多种任务中仿真宇树机器人,便于数据采集、回放、生成与模型验证,并且可以和 xr_teleoperate 配合使用来采集数据集。

四个动词——采集、回放、生成、验证——就是这个仓库的四条主线。

机型与末端的组合

README 里那张任务表格是按「机型 × 末端执行器」组织的,四列分别是:

G1-29dof-gripper     两指夹爪
G1-29dof-dex3        三指灵巧手
G1-29dof-inspire     Inspire 手
H1-2-inspire         另一款人形 + Inspire 手

这个二维结构本身就说明了一件事:在操作任务里,机器人本体和末端是可以解耦替换的两块。 同一个「把圆柱体拿起来放好」的任务,换个末端就是一套新的数据、新的动作空间。仓库里 tasks/common_observations/ 下的文件清单把这层结构映得很清楚:

g1_29dof_state.py    本体关节状态
h12_27dof_state.py   另一机型的本体状态
gripper_state.py     夹爪状态
dex3_state.py        三指手状态
inspire_state.py     Inspire 手状态
camera_state.py      相机数据

本体一个文件、每种末端一个文件、相机一个文件——观测被拆成了正交的几块,组装成哪种配置就取哪几块。上面提到的「观测三件套」(图像 + 本体 + 末端),在这个目录里一一对上了号。

任务名是一套编码

任务名长这样:

Isaac-PickPlace-Cylinder-G129-Dex1-Joint
Isaac-PickPlace-RedBlock-G129-Dex3-Joint
Isaac-Stack-RgyBlock-G129-Dex1-Joint
Isaac-Move-Cylinder-G129-Dex1-Wholebody
Isaac-PickPlace-Cylinder-H12-27dof-Inspire-Joint

拆开看是「前缀 - 任务类型 - 操作对象 - 机型标识 - 末端 - 控制模式」。任务类型目前有拿放(PickPlace)、堆叠(Stack)、搬运(Move)三类,对象是圆柱体、红色方块、红绿黄三色方块。文件树里还能看到 README 表格中没有的 pick_redblock_into_drawer(把红块放进抽屉)目录。

结尾那一段是控制模式,README 特意点了一句:任务名里带 Wholebody 的才是移动操作任务,才能控制机器人走动;其余的 Joint 类任务机器人是站定不动的。想发移动指令,README 指向了仓库自带的 send_commands_8bit.pysend_commands_keyboard.py

这种把配置信息编进名字的做法,好处是命令行里一眼能看出跑的是什么,坏处是名字很长。工程上这是个合算的交易——数据集攒到几十个任务之后,你会庆幸当初没起 task_01 这种名字。

相机是配置项,不是硬编码

tasks/common_config/camera_configs.py 的注释写的是「相机摆放相关配置」。相机位姿被抽成配置文件而不是写死在场景里,这个设计在下面讲数据生成时会显出价值。

下游的 unitree_lerobot 那边给了一条旁证:它的数据转换脚本支持一个叫 Unitree_G1_Dex1_Simrobot_type,README 明确注明这是「用于 unitree_sim_isaaclab 中数据采集的机器人类型,头部配单视角相机」。仿真采的数据在下游有独立的身份标识,说明两边是认真对接过的,不是各写各的。

三种运行模式

sim_main.py 是唯一的入口,靠命令行参数切换模式。这个设计我挺喜欢——三种模式共享同一套场景和同一套观测代码,不容易出现「采集时的观测」和「回放时的观测」不一致这种隐蔽 bug。

模式一:遥操作采集

python sim_main.py --device cpu --enable_cameras \
  --task Isaac-PickPlace-Cylinder-G129-Dex1-Joint \
  --enable_dex1_dds --robot_type g129

--enable_cameras 打开相机,--enable_dex1_dds / --enable_dex3_dds 分别为两指夹爪、三指灵巧手启用 DDS 通道。

这里有个设计值得停一秒。README 的「重要提示」里写着:仿真器启动后,收发的 DDS 话题与真机完全一致,并且专门警告——如果同一网络里还有真机在跑,注意区分两者。

这句警告本身就是设计意图的最好说明。这个仿真环境在通信层面伪装成一台真机。你为真机写的遥操作程序、图像客户端、控制脚本,一行不改就能接到仿真上。这条 DDS 通道正是unitree_sdk2 那套通信机制,仓库的致谢里也确认了对 unitree_sdk2_python 的依赖。

代价是那句警告所指的风险:真机和仿真在同一个局域网里,话题名撞在一起,你以为在遥操作仿真,实际上真机也跟着动了。这不是设计缺陷,是「接口完全一致」这个优点的必然反面。

图像走的不是 DDS。image_server 模块的注释写明用 ZMQ 发布图像。这个分工很合理:DDS 适合高频小包的状态与指令,图像是大块二进制,另开一条通道更干净。README 也提醒了,配合 xr_teleoperate 采数据时,要把那边 image_server 的 IP 改成仿真运行的地址。

模式二:数据回放

python sim_main.py ... --replay --file_path "<数据集目录>"

--replay 把之前录的动作序列重新喂给仿真里的机器人。README 注明,这里用的数据集格式与通过 xr_teleoperate 遥操作录制的格式一致。

回放的直接用途是检查数据质量——哪条 episode 中途失败了、哪条动作抖得不像话,看一遍就知道。但它真正的价值在下一个模式。

模式三:数据生成

这是整个仓库里我认为最值得讲的一段。README 的原话是:在数据回放期间,通过修改光照条件和相机参数并重新采集图像数据,可以生成更多样的视觉特征用于数据增强,从而提升模型的泛化能力。

python sim_main.py ... --replay --file_path "<原始数据>" \
  --generate_data --generate_data_dir "./data2"

配套开关有 --modify_light(改光照,需要相应调整 main 里的 update_light 函数)、--modify_camera(改相机参数,对应 batch_augment_cameras_by_name 函数)、--rerun_log(生成过程记录日志)。

把这件事的逻辑摊开:动作序列不变,只换光照和视角,重新渲染一遍图像。 同一条演示轨迹,就能变出很多条视觉上不同、动作上相同的训练数据。

这正是**域随机化(domain randomization)**的工程化形态。它背后的假设是:如果模型在光照、视角、颜色都乱变的情况下依然能完成任务,那它学到的就不是「像素点 (320,240) 是红的就往左伸手」这种脆弱的相关性,而是更接近「找到红块的位置」这件事本身。

真机上你做不到这个。你没法把同一条演示重新走一遍、只把灯光换掉——人的操作不可能复现到帧级一致。这是仿真相对真机的一个结构性优势,不是「便宜一点」的量变。

README 后面跟了一句很实在的提醒:如果要改光照或相机参数,请先仔细调试和测试参数,再进行大规模数据生成。这句话的潜台词大概每个做过数据管线的人都懂——参数没调好就批量生成,产出的是一整批废数据,而你要过很久才会发现。

代码结构:分层在哪里

unitree_sim_isaaclab/
├── action_provider      动作来源:读文件 / 收 DDS / 策略生成
├── dds                  DDS 通信模块
├── image_server         图像发布服务(ZMQ)
├── layeredcontrol       底层控制:取动作并写进虚拟环境
├── robots               机器人基础配置
├── tasks                任务相关
│   ├── common_config        相机摆放、机器人设置
│   ├── common_event         事件注册管理
│   ├── common_observations  各类观测数据获取
│   ├── common_scene         各任务的公共场景
│   ├── common_rewards       各任务的奖励
│   ├── common_termination   物体是否超出工作范围的判断
│   ├── g1_tasks             G1 相关的全部任务
│   └── utils
├── tools                USD 转换与修改工具
├── usd                  USD 模型文件
└── sim_main.py          主函数

action_provider 这个模块名起得好。它的注释说:提供读取文件动作、接收 DDS 动作、策略生成动作等接口,当前主要使用基于 DDS 的动作获取。目录下能看到 action_provider_dds.pyaction_provider_replay.pyaction_provider_wh_dds.py(对应 Wholebody)和一个 create_action_provider.py 工厂。

这一层是整个仓库的枢纽。 往下走,layeredcontrol 只管「拿到动作,写进仿真环境」,它不关心动作是人遥操作来的、从文件读的,还是模型推理出来的。往上走,三种数据来源被抽象成同一个接口。

这个抽象让上面那三种模式几乎是免费的——采集是 DDS 来源,回放是文件来源,模型验证是策略来源。想不通这一层,你会觉得 sim_main.py 一个入口塞三种模式很乱;想通了就会发现,模式切换只是换了个 provider。

tasks/common_termination/ 的注释是「判断不同任务中的物体是否超出指定工作范围」,示例代码里露出来的函数名是 reset_object_estimate。配合 dds/reset_pose_dds.py 和根目录的 reset_pose_test.py 看,物体位姿重置是被当成一等公民对待的。这也符合数据采集的实际节奏:采完一条就得把场景恢复原状,这个动作在真机上要靠人手,在仿真里是一行代码。

tools/ 下有 convert_urdf.pyedit_usda.py 这类文件。从文件名读出来的结论是:进 Isaac Sim 的资产要转成 USD 格式,仓库自带了从 URDF 转过去以及后续修改的工具链。还有 episode_writer.py(episode 落盘)、data_convert.py / data_json_load.py(格式转换与读取)、augmentation_utils.py(增强工具)、rerun_visualizer.py(可视化)。

新增一个任务的流程 README 也给了:在 common_scene 加场景配置,在 common_termination 加终止/重置判断,然后在 g1_tasks 下建目录,写 observations.pyterminations.py、环境配置类,最后在 __init__.py 里注册:

gym.register(
    id="Isaac-PickPlace-Cylinder-G129-Dex1-Joint",
    entry_point="isaaclab.envs:ManagerBasedRLEnv",
    kwargs={...},
    disable_env_checker=True,
)

用的是 gymnasium 的标准注册机制,entry_point 指向 Isaac Lab 的 ManagerBasedRLEnv。注册表这套模式在强化学习那个仓库里也见过——命令行 --task 后面那个字符串能生效,靠的就是它。

和强化学习训练环境的差别在哪

unitree_rl_gym 也是仿真环境,也在里面跑机器人,为什么要另起一个仓库?

因为目标不同,环境的设计取向就完全不同

强化学习训练环境练的是运动控制——怎么走稳、怎么不摔。它的核心诉求是吞吐量:几千个机器人实例并行、关掉渲染只跑物理、地形随机生成。画面好不好看完全不重要,官方文档甚至直接建议训练时用无图形界面模式。

操作数据采集环境要的是另一套东西:

运动控制训练 操作数据采集
数据从哪来 策略自己探索 人遥操作演示
视觉渲染 可以关掉 必须开,图像就是观测
物体交互 主要是脚与地面 手与物体的接触是任务核心
场景 地形程序化生成 桌面、物体、抽屉,逐个搭
并行度 越高越好 受人的操作速度限制

看这个仓库的具体设计就能对上:--enable_cameras 是常规参数不是可选项;usd/ 目录装着物体模型;common_scene/ 下每个任务一套场景;末端执行器有三种可选。这些在运动控制训练里全都不是重点。

反过来,unitree_rl_gym 里那些地形生成、并行环境数量的设计,在这个仓库里也看不到对应物。两个仓库解决的是具身智能栈里相邻但不同的两层问题:一个负责让机器人站得住走得动,一个负责让它知道该做什么。想把这两层的关系理清楚,具身智能这篇是个合适的起点。

下游:数据怎么变成模型

采出来的数据不能直接喂给训练框架,中间要转格式。这一段的承接方是 unitree_lerobot

那边的 README 显示,原始数据是 JSON 格式,目录结构长这样:

datasets/
└── task_name/
    ├── episode_0001
    │    ├── audios/     音频
    │    ├── colors/     彩色图像
    │    ├── depths/     深度图像
    │    └── data.json   状态与动作
    ├── episode_0002
    └── ...

一个 episode 一个目录,图像按类型分文件夹,状态和动作统一进 data.json——正是文章开头说的那个二元组。

转换脚本 convert_unitree_json_to_lerobot.py 把它转成 LeRobot 格式。LeRobot 是 Hugging Face 那套机器人学习框架,转过去之后就能接上它支持的一系列模仿学习策略——unitree_lerobot 的 README 里给出了 ACT、Diffusion Policy、Pi0 等几种训练命令的示例。这些方法与 VLA 路线的关系,可以顺着视觉-语言-动作模型那篇往下读。

链路还有回程。unitree_leroboteval_robot/ 下有个 eval_g1_sim.py,README 说明它用于在 unitree_sim_isaaclab 仿真环境里跑推理测试,并且带一个 --save_data 选项——注释明确写着这个选项目前仅限仿真环境。

训完的模型回到同一个仿真环境里被评估,评估过程还能顺手再产生数据。 采集、训练、评估形成闭环,而闭环的两端都落在这个仿真环境上。它的 Release Note 里 v0.2 那条「新增仿真环境验证」,说的就是这件事。

顺带一提,unitree_lerobot 里还有个 PyQt 写的数据编辑器,能裁掉 episode 里多余的片段、删掉失败的那几条。这个工具的存在提醒了一件容易被忽略的事:采集出来的数据不是拿来就能用的,中间有一道人工质检。 一条中途失败的演示混进训练集,教给模型的就是错的东西。

仿真绕不开的那一关

把优势说完了,得说代价。

仿真里训出来的模型搬到真机上会掉性能,这个现象叫 sim-to-real gap。在操作任务上,它主要来自两处:

物理差异。 摩擦系数、接触刚度、物体质量分布,仿真器里都是近似模型。夹爪能不能夹稳一个圆柱体,恰恰对这些参数极其敏感。

视觉差异。 这一处在操作任务里往往比物理差异更棘手。渲染出来的图像和真实相机拍出来的图像,在噪声、动态范围、色彩响应、运动模糊上都不一样。真实相机有自动曝光、有滚动快门、有镜头畸变;渲染图像干净得不真实。模型在干净图像上学会的特征,换到真实成像上可能直接失效。

常见的缓解手段有两条,都不神秘:

一是域随机化——就是上面数据生成那一节做的事。既然不知道真实分布长什么样,那就把训练分布撑得足够宽,让真实情况落在里面。

二是真实数据与仿真数据混合训练。 仿真数据管量,真实数据管准。少量真机采的数据配上大量仿真数据,常常比纯仿真好得多。仿真在这里的角色不是替代真机采集,而是把真机采集的边际成本摊薄——原本需要一万条真机数据,现在可能一千条加上仿真就够了。

这两条我在事实源里读到了第一条的直接实现,第二条属于领域内的通行做法,仓库文档没有对此表态,我也不替它表态。

收束:这一格在整个栈里的位置

把具身智能这条栈从下往上摆:

硬件本体与关节驱动
    ↓
运动控制(走、站、平衡)        ← unitree_rl_gym 这一层
    ↓
数据采集与仿真环境              ← unitree_sim_isaaclab 在这
    ↓
模仿学习训练与格式转换          ← unitree_lerobot
    ↓
任务理解与高层决策

unitree_sim_isaaclab 卡在中间那一格,两头都连着东西:上游接遥操作设备和真机通信协议,下游接训练框架的数据格式。它不训练模型,也不控制真机,它是这条链路上负责「把演示变成数据」的那一环

读完这个仓库,我认为有三件事值得记住:

第一,具身智能当前最紧的瓶颈不在算法,在数据。 一个仓库把「采集、回放、生成、验证」四件事都做进去,而不是只做一个漂亮的物理引擎,这个取舍本身就说明了行业的焦点在哪。

第二,「与真机使用同一套 DDS 协议」是这个仿真环境最要紧的设计决策。 它带来的不是性能优势,而是代码复用——遥操作程序、图像客户端、部署脚本都不用为仿真单独写一份。同一个决策也带来了「别把真机和仿真放同一个网段」的风险,README 把它放在最前面警告,说明这个坑真实存在。

第三,数据生成那一步值得单独记住。 保持动作不变、只改光照和视角重新渲染——这个操作在真机上物理上不可能实现,是仿真独有的能力。它对应的是缓解 sim-to-real gap 的主流思路之一。

想接着往下看,遥操作数据采集是这条链路的上游入口,模仿学习与 LeRobot是下游出口,而强化学习那条运动控制路线和这条并行不悖——一台人形机器人身上,两条链路同时在跑。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明