← 返回文章库

戴上头显操控人形机器人:xr_teleoperate 完整拆解

最后更新 2026-08-23
⏱ 约 15 分钟 🟡 涉接线/强电
你将学到
  • 看懂遥操作系统的两条链路:动作从头显到关节的正向链路,画面从机器人到眼睛的反向链路
  • 理解重定向(retargeting)这一环为什么不能省,人手和机器手的差异到底卡在哪
  • 明白为什么遥操作是当下采集具身智能训练数据的主力手段,以及它和仿真采集的分工

你大概刷到过这样的视频:一个人戴着头显站在房间里,手在空中比划,旁边的人形机器人跟着抬手、抓取、放下。看起来像是个很酷的玩具演示——人动,机器跟着动,仅此而已。

如果你也是这么理解的,那这篇文章想先扭转一件事:遥操作在今天的价值,不在「远程遥控」,而在「人类操作员的每一次动作都是一条训练数据」。宇树这个仓库自己在引用格式里就把定位写死了,README 末尾的 citation 条目标题是:An Open-Source Teleoperation Framework and Data Collection Toolkit for Embodied Intelligence——遥操作框架,以及具身智能的数据采集工具包。后半句才是重点。

先说清楚本文的边界:下面所有内容都来自 xr_teleoperatetelevuer 两个公开仓库的 README 和文件树,我没有真机可以验证。涉及运行效果的地方,我都会写明「仓库文档里写的是」,不会替它打包票。

它到底支持什么:先把矩阵摊开

拆一个陌生系统,我习惯先看它的「兼容性表格」和「启动参数」,因为这两样东西是作者对外承诺的边界,比任何介绍性文字都实在。

README 的开头列了两类硬件。XR 设备这边,文档里点名的是 Apple Vision Pro、PICO 4 Ultra Enterprise、Meta Quest 3。机器人这边给了一张状态表,配合 --arm 参数的可选值看更清楚:

--arm   G1_29 | G1_23 | H1_2 | H1 | H2 | R1_A5 | R1_A7
--ee    dex1 | dex1_internal | dex3 | inspire_ftp | inspire_dfx | brainco

--arm 是机器人手臂型号,--ee 是末端执行器(end-effector)。末端这一栏里既有夹爪也有灵巧手,README 的表格里对应写的是 Dex1‑1 夹爪、Dex3‑1 灵巧手、Inspire 灵巧手、BrainCo 灵巧手——第三方品牌我只按原文提及,不展开。

这两个枚举在 assets/ 目录里能找到对应物。仓库为每种机型准备了一组区分不同关节配置的 URDF 文件:

assets/g1/g1_body23.urdf
assets/g1/g1_body29_hand14.urdf
assets/h1/h1_with_hand.urdf
assets/h1_2/h1_2.urdf
assets/h2/H2.urdf
assets/r1/r1_a5.urdf
assets/r1/r1_a7.urdf

以及每种手一套「URDF + yml」:

assets/unitree_hand/unitree_dex3.yml   + unitree_dex3_left/right.urdf
assets/inspire_hand/inspire_hand.yml   + inspire_hand_left/right.urdf
assets/brainco_hand/brainco.yml        + brainco_left/right.urdf

这个「一个 URDF 配一个 yml」的规整程度值得停一秒。URDF 描述的是这只手的物理结构——有几根手指、每个关节转哪个轴、连杆多长;那个 yml 显然是另一层配置。它是干什么的?往下看代码结构就知道了,答案藏在「重定向」这一环里。

其余的启动参数分两组。基础控制这组是 --frequency(录制与控制的帧率)、--input-modehand 手部追踪 / controller 手柄追踪)、--display-modeimmersive 沉浸 / ego 透视加小窗 / pass-through 纯透视)、--img-server-ip--network-interface。模式开关那组更有意思:

--motion     运动控制模式,遥操作程序与机器人运动控制程序并行运行
--headless   无显示器设备上运行(如开发计算单元 PC2)
--sim        仿真模式
--ipc        进程间通信模式,便于与 agent 程序交互
--record     数据录制模式
--task-dir / --task-name / --task-goal / --task-desc / --task-steps

最后那五个 --task-* 参数是整个仓库最能说明立意的地方——它们全都是给数据集写元信息用的:任务保存路径、任务名、任务目标、任务描述、任务步骤,后四项会写进 json 文件。一个纯粹的遥控程序不需要记录「这次任务的目标是什么、分几步」。会记这些的,只有数据采集工具。

--ipc 那句注释也别放过:「适合与 agent 程序交互」。也就是说,这套系统的状态机是可以被外部程序驱动的——人不在场时,让一个模型来按 r、按 s。这条路留着做什么,你可以自己想。

安装为什么这么啰嗦:证书那一段不是多余的

README 的安装章节里,篇幅最长的不是装依赖,而是生成 SSL 证书。这一段第一次读会觉得莫名其妙,想明白之后反而是整套系统里我最喜欢的设计约束。

基础环境是 conda 建一个名为 tv 的环境,装 Python、pinocchio、numpy,然后克隆仓库、拉子模块,再分别以可编辑模式安装 teleimagertelevuer 两个子模块。文档说代码在 Ubuntu 20.04 和 22.04 上测试过,具体版本要求以官方仓库当前文档为准。

conda create -n tv python=3.10 pinocchio=3.1.0 numpy=1.26.4 -c conda-forge
conda activate tv
git clone https://github.com/unitreerobotics/xr_teleoperate.git
cd xr_teleoperate
git submodule update --init --depth 1

然后就是那一长串 openssl。为什么非要证书?因为 XR 头显里的那个「机器人视角」画面,是跑在浏览器里的。你戴上 Vision Pro 或者 PICO,打开 Safari 或 PICO 浏览器,访问主机上的一个地址,页面里点「Virtual Reality」进入 VR 会话——这条路走的是 WebXR。而浏览器要开放摄像头透视、手部追踪、WebRTC 这类能力,必须在安全上下文里,也就是 HTTPS。局域网里的一台 Ubuntu 主机没有公网域名,拿不到 CA 签发的证书,只能自签。

自签又分两条路,README 分得很细:Pico / Quest 用一条 openssl req -x509 就够,浏览器里点「高级 → 继续访问」放行;Apple Vision Pro 则要生成一个根证书 rootCA.pem,用它去签服务器证书,还得在 server_ext.cnf 里把主机 IP 写进 subjectAltName,最后通过 AirDrop 把根证书传到头显上手动信任。

subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 192.168.123.164
IP.2 = 192.168.123.2

这两个 IP 也不是随便写的:一个是机器人身上的开发计算单元(文档里叫 PC2),一个是你的主机。遥操作是一个至少两台机器参与的分布式系统,证书里要把两边都写上,是因为浏览器要同时信任这两个地址上的服务。

证书路径的配置给了两种方式:放进 ~/.config/xr_teleoperate/,或者用 XR_TELEOP_CERT / XR_TELEOP_KEY 两个环境变量指过去。televuer 的 README 特意说明这份配置是和 teleimager 共享的——两个模块认同一套证书,少了一份重复配置。防火墙那边还要放行相应端口。

这一整段的信息量在于:它把「遥操作看起来只是戴个头显」这个错觉打掉了。真实情况是你要处理网络、证书、跨机部署,任何一环没通,头显里就是黑屏。

正向链路:一只手是怎么变成一串关节指令的

现在进入正题。README 第 4 章给了一份代码结构总览,我把和数据流相关的部分抽出来:

teleop/
├── teleimager                        [图像服务库]
├── televuer/src/televuer
│     ├── television.py               [用 Vuer 从 XR 设备采集头部、手腕、手/手柄数据]
│     └── tv_wrapper.py               [对采集到的数据做后处理]
├── robot_control
│     ├── src/dex-retargeting         [灵巧手重定向算法库]
│     ├── robot_arm_ik.py             [手臂逆运动学]
│     ├── robot_arm.py                [控制双臂关节并锁定其他部分]
│     ├── hand_retargeting.py         [灵巧手重定向库的封装]
│     ├── robot_hand_inspire.py       [控制 Inspire 灵巧手]
│     ├── robot_hand_unitree.py       [控制宇树灵巧手]
│     ├── robot_hand_brainco.py
│     └── dds_utils.py
├── utils
│     ├── episode_writer.py           [录制模仿学习数据]
│     ├── weighted_moving_filter.py   [关节数据滤波器]
│     ├── rerun_visualizer.py         [录制数据可视化]
│     ├── ipc.py                      [与代理程序的进程间通信]
│     ├── motion_switcher.py          [运动控制状态切换]
│     └── sim_state_topic.py          [仿真部署]
└── teleop_hand_and_arm.py            [遥操作启动脚本]

顺着这份清单,正向链路是这样一段接力:

第一棒,采集。 television.py 用 Vuer 从头显里拿数据。注意它的注释写的是「头部、手腕、手/手柄」——不只是手。头部姿态是必要的,因为操作员扭头看向哪里,直接决定了画面该怎么渲染,也决定了手臂动作的参考坐标系。v1.6 的更新说明里有一条:默认改为使用「head-yaw-relative」的手臂参考,即以头部偏航角为基准来解释手臂位姿。这个改动很实在——人转身的时候,手的绝对位置变了,但相对于身体的意图没变。

第二棒,后处理。 tv_wrapper.py 单独一层。televuer 的版本说明里能看出它经历过一轮整理:数据结构从嵌套的 TeleStateData 拍平成统一的 TeleData,取数函数从 get_motion_state_data 改名为 get_tele_data,还修了一处把 wrist(手腕)误写成 waist(腰)的命名错误。这种改名记录不好看,但对读代码的人是好事:它说明作者在有意识地收敛接口。

第三棒,也是最关键的一棒:重定向。

这里必须停下来讲清楚。人的手和机器人的手,结构不一样。你的手指有几段指骨、拇指能做对掌运动、每个关节的活动范围是多少,这些和 Dex3‑1 或者 Inspire 灵巧手完全对不上。有的机器手只有三指,有的手指少一个自由度,有的关节是耦合驱动的——你转不动其中一个而不带动另一个。

所以不能把人手的关节角直接抄给机器手。抄过去的结果要么是超出机械限位报错,要么是姿势对了但抓不住东西。

正确做法是重定向(retargeting):不匹配关节角,而是匹配任务空间里的关键特征——比如指尖之间的相对位置、手掌张开的程度、拇指与食指的捏合关系。然后反过来求解机器手应该摆成什么关节角,才能最接近这些特征。这本质上是一个优化问题:在机器手自己的关节限位内,找一组角度,让某个「像不像」的指标最小。

仓库里干这件事的是 src/dex-retargeting(作为子目录引入的第三方算法库,致谢里列了它的来源)加上 hand_retargeting.py 这层封装。到这儿,前面那个悬念也解开了:assets/ 里每种手配的那份 yml,就是喂给重定向算法的映射配置——告诉它这只手有哪些关键点、要匹配哪些量。换一款手,理论上就是换一份 URDF 加一份 yml,上层不用动。

手臂那边走的是另一条路。手腕在空间中的位姿是六维的(位置加姿态),要把它变成肩、肘、腕各关节的角度,这是标准的逆运动学问题,对应 robot_arm_ik.py。安装环境里明确装了 pinocchio,致谢清单里列了 casadi 和 meshcat——刚体动力学库、优化求解器、可视化,这三样凑在一起是做数值逆运动学的典型组合。我没有逐行读实现,这里只指出依赖与文件名之间的对应关系。

第四棒,滤波与下发。 weighted_moving_filter.py 是关节数据的加权移动滤波器。为什么要滤波?因为头显的手部追踪不可能完美稳定,遮挡、边缘、光照都会让姿态估计抖一下。这个抖动如果原样下发到关节,机器人手臂就会跟着高频颤动——轻则难看,重则伤减速器。滤波器是这条链路上的减震垫。

最后由 robot_arm.py 把指令发出去。它的注释里有半句很重要:「控制双臂关节并锁定其他部分」。遥操作时你只想动手臂,腿和腰必须保持它们原本的状态,不能因为你发了一帧上肢指令就把全身控制权抢过来。通信这一层走的是 dds_utils.pyunitree_sdk2_python——也就是我们拆过的那套 SDK 架构所依赖的同一条 DDS 通路。

把这一棒棒连起来:

XR 头显(手/手柄/头部姿态)
      │  WebXR over HTTPS/WebSocket
      ▼
televuer: television.py  →  tv_wrapper.py  →  TeleData
      │
      ├─ 手臂:逆运动学 robot_arm_ik.py ─┐
      │                                  ├─ 滤波 → robot_arm.py
      └─ 手/夹爪:dex-retargeting        │        robot_hand_*.py
                  hand_retargeting.py ───┘
                                          │  DDS (unitree_sdk2_python)
                                          ▼
                                    机器人关节

反向链路:操作员必须看见机器人看见的

只有正向链路,人是操作不了的。你得看见机器人视角的画面,才知道手要往哪儿伸、抓没抓住。这条反向链路在仓库里是独立的一套东西。

实机部署时,图像服务要手动装在机器人的开发计算单元 PC2 上,模块叫 teleimager。流程是:在 PC2 上配置 cam_config_server.yaml 并启动图像服务,主机侧再跑一个 image client 去订阅。前面生成的 key.pemcert.pem 还要 scp 一份到 PC2——因为 WebRTC 服务也需要它们。仿真模式下这套图像服务是自动启用的,不用手配。

传输有两条可选:zmq 或 WebRTC。televuer 的版本说明里记录了图像传输方式的演进——从走外部共享内存改成按引用传递,后来又加了 webrtc 开关接口,以及把发图的方法从 set_display_image 调整为 render_to_xr

显示模式那三个值,对应的是操作员眼前看到什么:

  • immersive:完全沉浸,头显里显示机器人的第一人称视角(需要启用 zmq 或 webrtc 之一)
  • pass-through:透视,头显摄像头把真实世界透进来,不显示机器人画面(即便启用了图传也不显示)
  • ego:中间一个小窗显示机器人视角,周围是真实世界

televuer 的 V4.0 说明里还提到调整了 immersive 和 ego 模式下图像平面的高度,理由写的是「提供更自然舒适的 VR 体验」。

为什么延迟是遥操作体验的命门? 这条反向链路上每多一道工序——采集、编码、网络、解码、渲染——画面就晚到一点。人操作时靠的是闭环:看到手快碰到杯子了,减速;看到抓稳了,开始提。如果画面比实际慢一拍,你的每一次修正都是在对着过去的状态下命令,手就会在目标附近来回过冲,越急越抓不住。更糟的是,视觉与前庭感觉不一致会直接引发眩晕,人根本撑不住长时间采集。这也是为什么图传这一环值得单独做一个库、还要在 zmq 和 WebRTC 之间给选择——它不是「顺便把画面传过去」,它和控制链路一样是一等公民

本文不讨论具体的延迟数值,仓库文档里也没有给出可引用的指标。

televuer 的角色:把「哪款头显」这件事挡在外面

televuer 是单独一个仓库,README 里的自我定位是「Vuer 库的特化版本」,专门为宇树机器人的 XR 遥操作做适配,作为 xr_teleoperate 的核心组件存在。它支持的设备就是前面那几款。

它的价值在于抽象。上层的 robot_control 完全不需要知道操作员戴的是 Vision Pro 还是 PICO——它只管从 get_tele_data 里拿一份统一的数据结构:头部在哪、手腕在哪、手指什么姿态、手柄按了哪个键。设备差异、浏览器差异、WebXR 的各种坑,全被挡在 televuer 里面。

这层抽象有多值钱,看它 README 的最后一节就知道了。那里有一张按上游 vuer 版本排列的兼容性记录表,逐版本写着哪个版本手部追踪正常、哪个版本拿不到手部数据、哪个版本只有平面 RGB 图没有立体视图、PICO 上用手势点「Virtual Reality」按钮会卡在黑屏但用手柄点就正常(反之在手柄模式下又要用手势点)。文档最后给出了一个明确的推荐版本,并附上了相关 issue 链接。具体版本号以官方仓库当前文档为准。

读到这一节我停了很久。这张表背后是一个一个版本试出来的,是别人替你踩过的坑。它也解释了为什么要把 XR 适配单独做成一层:上游依赖的行为在不同版本间会漂移,你必须有一个地方来集中吸收这种漂移,否则每次升级都要在业务代码里到处打补丁。

顺带一个读文件树时发现的小细节:xr_teleoperate 的目录说明里写的是 television.py,而 televuer 仓库快照的文件树里是 src/televuer/televuer.py。这类命名漂移在子模块拆分过程中很常见,以仓库当前状态为准,别照着文档硬找文件。

录制:这才是这套系统的落点

到这里前面所有铺垫才有意义。

按 README 的说明,带 --record 启动之后,操作流程是这样的:戴头显、连 Wi‑Fi、浏览器进入 VR 会话,看到机器人第一人称画面后,先把自己的手臂摆到与机器人初始位姿一致,在终端按 r 开始遥操作,按 s 开始录制,再按 s 停止并保存这一段(episode),可以反复重复。按 q 退出。

对应到代码里就是 utils/episode_writer.py,它的注释一句话说明了用途:用于录制模仿学习的数据。数据默认落在 teleop/utils/data/,README 里明确提示了要注意磁盘空间——一段一段的多路图像加同步的关节状态,体积增长很快。另外还有 rerun_visualizer.py 用来回看录下来的数据,这在数据清洗环节是刚需:哪一段操作失败了、哪一段手抖得厉害,得能看出来才能筛掉。

录完的数据怎么用?README 的注释指向了宇树自己的 unitree_IL_lerobot 仓库,说明了数据采集与格式转换的用法。IL 就是 imitation learning,模仿学习。

为什么遥操作是采集具身智能训练数据的主力手段?

想训练一个能干活的机器人策略,你需要成千上万条「在这种情况下应该这么动」的样本。这些样本从哪来?

  • 让人写规则?抓一个没见过的杯子就得重写。
  • 让机器人自己试错?在真机上跑强化学习,试错的代价是硬件损坏,而且探索效率极低。
  • 让人来演示——人戴上头显,把机器人当自己的身体用一遍,机器人从头到尾把「我看到了什么」和「我被指挥着怎么动了」同步记下来。这就是一条完整的训练样本:观测在,动作在,而且两者天然对齐。

这就是遥操作的位置。它是把人类的操作直觉灌进机器人的最短路径。人不需要懂逆运动学,不需要知道关节限位在哪,他只要把活干成,剩下的都由这条链路翻译。而那五个 --task-* 参数,正是在给每一段数据打上语义标签,让它以后能被检索、被筛选、被喂给需要语言条件的模型——关于这类「看图 + 听指令 + 出动作」的模型范式,可以看视觉-语言-动作模型那篇

那它和仿真采集是什么关系? 互补,不是替代。

仿真的长处是量:可以并行几千个环境,可以随机化物体位置、摩擦、光照,可以跑一整夜。短处是「真实感」——接触力学、柔性物体、光学畸变,仿真里都是近似的,学出来的策略搬到真机上会掉性能。

真机遥操作正好相反:数据是真的,接触是真的,相机噪声也是真的,但一条一条采,慢且贵,还费人。

有意思的是,这个仓库把两边接在同一套操作界面下了。--sim 参数一开,同一个 teleop_hand_and_arm.py、同一副头显、同一套按键流程,控制的对象换成了 Isaac Lab 里的仿真机器人(对应宇树的 unitree_sim_isaaclab 仓库,文档里给的示例任务是抓取放置类的场景)。这意味着你可以在仿真里先把流程练熟、把任务脚本调通,再原样搬到真机上采数据。对新手来说这更是必经之路——先在仿真里把手抖的毛病改掉,再去动真机

关于具身智能这条链路整体是怎么串起来的,具身智能那篇讲得更完整;数据落到模仿学习之后怎么训、怎么部署,可以接着看足式机器人的模仿学习实践

安全:README 用红色警告标出来的那些

拿真机做遥操作,风险和跑仿真完全不是一个量级。人形机器人的手臂有配重、有减速比,摆动起来动能不小,而操作它的人还戴着头显——视野被占用,看不见周围的真实空间

README 在实机部署章节用红色 Warning 标出的几条,我原样转述:

  1. 所有人必须与机器人保持安全距离,以防潜在危险。
  2. 运行程序前,务必至少完整读一遍官方文档。
  3. 要使用运动模式(--motion),需先通过遥控器让机器人进入控制模式。
  4. 运动模式下的操作约定:右手柄 A 键退出遥操作;两个摇杆同时按下 = 软急停(切换到阻尼模式);左摇杆控制行进方向,右摇杆控制转向;最大速度在代码里做了限制。

第 4 条里那个「两个摇杆同时按下」的软急停,是我认为整份文档里最该记住的一个设计。它不需要你摘下头显、不需要你去找物理急停按钮——在你手已经握着的东西上,用一个不可能误触的组合动作,就能让机器人立刻进入阻尼状态。遥操作时人的手是不能离开输入设备的,急停就必须做在输入设备上。

退出同样有讲究。README 建议按 q 之前先把机器人手臂摆到接近初始位姿,理由是避免损伤机器人。文档写的是:Debug 模式下按退出键后,双臂会在数秒内回到初始位姿再结束控制;运动模式下则回到运动控制位姿。也就是说退出不是「立刻断电撒手」,而是有一段自动归位过程——如果你退出时手臂正伸在一个奇怪的位置,那段归位轨迹可能会扫到东西。

还有一条藏在末端执行器章节里的说明很值得称道,它体现了这个仓库的诚实:对于内部走线的 Dex1 夹爪配置,文档说明这种模式通过 G1 底层指令中的特定电机索引来控制左右夹爪,不需要启动外部服务;紧接着写了一句——这种内部走线的夹爪能否通过 rt/arm_sdk 控制「尚未验证」,因此目前不支持同时使用 --motion

没验证就说没验证,不确定的组合就先不开放。这种写法比一张全是对勾的兼容性表格可信得多。

读完这个仓库,值得记住的几件事

第一,遥操作是两条链路,不是一条。 动作从头显流向关节,画面从相机流回眼睛。多数人只想到前一条,但后一条才决定操作员能不能干活。任何一条断了,系统就是废的。

第二,重定向是不可省略的一环。 只要人手和机器手的结构不同——它们永远不同——就必须在任务空间里做映射,而不是照抄关节角。这个仓库把它做成了可替换的一层:换手就换 URDF 加 yml。

第三,这套系统的产物是数据集,不是「操作体验」。--task-* 参数到 episode_writer.py 再到与模仿学习仓库的对接,整条工具链的终点是一批带标注的演示数据。理解了这一点,你才明白为什么它要费劲支持这么多种手、为什么要单独做数据可视化。

第四,抽象层的价值在依赖漂移时才显现。 televuer 那张上游版本兼容表,是这套系统最不起眼也最昂贵的资产。

这条链路的下游就是模仿学习:把采到的一批演示喂进模型,让它学会「看到这个画面,该输出这样的动作」。人演一遍,机器学着做——这是当下最直接的一条从人类技能到机器人策略的通路。至于这批数据具体怎么组织、怎么训练、训出来的策略又怎么放回真机,是这一卷后面几篇的事。

📄 来源 / 自校链接

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

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

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