不戴头显也能遥操作:体感相机做全身动作捕捉
- 读懂一套无接触遥操作程序的完整链路:深度相机 → 人体骨骼 → 关节重定向 → 机器人/仿真
- 理解重定向(retargeting)为什么不能省,它到底在处理人和机器人之间的哪几类不匹配
- 分清穿戴式与无接触式两条遥操作路线各自的代价和适用场景,不靠感觉选型
假设你手上有一台人形机器人,想让它做一段动作。写脚本?关节太多,一帧一帧编太慢。用强化学习训一个策略?周期长,而且你现在只是想快速试个想法。
最直接的办法其实是:你自己做一遍,让机器人跟着做。这就是遥操作(teleoperation)。
遥操作在具身智能这条线上的地位这两年变了。它一开始只是「远程操控」,现在更常见的用法是采数据——人示范一遍,把人的动作和机器人的关节轨迹一起录下来,喂给模仿学习。我们在具身智能是什么里聊过这条数据链的价值,遥操作就是这条链最上游的水龙头。
而遥操作本身又分两条路:
- 穿戴式:你戴上头显、握着手柄或者让设备追踪你的手,设备主动告诉程序你的手在哪。
- 无接触式:你什么都不穿,一台相机看着你,程序自己从画面里把你的骨架推出来。
这篇讲第二条路。样本是宇树的 kinect_teleoperate 仓库——用 Azure Kinect DK 相机遥操作人形机器人。
先把话说在前面:本文完全基于这个公开仓库的 README、目录结构和文件清单,没有真机验证,也没有跑过程序。所有关于「怎么跑」的描述都来自仓库文档的说法,不是我的实测结论。
先逐项核对:这个仓库到底给了你什么
读一个陌生仓库,第一件事是把「它做什么、依赖什么、怎么跑」核清楚,不要凭印象。
做什么:仓库描述写的是用 Azure Kinect DK 相机实现宇树人形机器人的遥操作。README 正文里点名的是 H1;而 src/ 目录下同时放着 unitree_h1 和 unitree_g1 两个模型目录,unitree_h1 里有 urdf/h1.urdf、urdf/h1_with_hand.urdf 和 mjcf/h1.xml、mjcf/scene.xml,unitree_g1 里有 g1.xml 和 scene.xml。
依赖什么:这是这个仓库最有意思的地方——它的依赖不是一层,是两层。
第一层是相机侧。README 说得很明确,Azure Kinect DK 的驱动由两部分组成:camera SDK 和 body tracking SDK,要分别装。装完之后有两个可视化工具可以自检:
k4aviewer # 看相机图像
k4abt_simple_3d_viewer # 看人体骨骼追踪结果
这两个命令的存在很关键。它把「相机通没通」和「骨骼追踪出没出结果」拆成了两个独立的验证点。真到调不通的时候,你至少知道该从哪一环开始查。README 还提到一个容易踩的坑:要在非 root 用户下用这套 SDK,得先放一份 udev 规则文件(仓库 lib/ 目录里就备份了一份 99-k4a.rules),另外相机要接 USB 3.0 接口。
第二层是机器人与仿真侧。README 说这个仓库用 MuJoCo 物理引擎做仿真测试,而且把 MuJoCo 的文件直接放进了 src/mujoco-3.1.5 目录,开发者不用自己下。构建工具是 CMake 加 ninja,数学库用 Eigen3。
mkdir build && cd build
cmake .. -GNinja
ninja
编译完在 build 目录下得到一个可执行文件 kinect_teleoperate。环境方面 README 写的是在 Ubuntu 20.04 上测试的,其他系统配置可能不同(这类环境要求以官方仓库当前文档为准)。
怎么跑:直接执行 ./kinect_teleoperate。按 README 里给出的输出,程序起来之后会打印一段导航和快捷键说明(ESC 退出、h 帮助、b 切换人体可视化模式、k 切换 3D 窗口布局),然后是三行:
control loop start...
MujocoRender loop start...
KinectRender loop start...
Please use the wake-up action to start or stop the TeleOperation...
三个循环并列启动,这句输出信息量很大——控制、MuJoCo 渲染、Kinect 渲染是三条独立跑的线。桌面上会弹出两个可视化窗口。
核心链路:从深度图到电机指令
把整条链拆开,是这么四段:
深度相机 → 人体骨骼关节点 → 重定向 → 机器人关节角
(硬件) (body tracking SDK) (本仓库) (模型 / 真机)
第一段和第二段属于 Azure Kinect 的两个 SDK,第三段才是这个仓库自己写的东西。看 src/kinect_teleoperate_robot/ 就一目了然:
src/kinect_teleoperate_robot/
├── include
│ ├── jointRetargeting.hpp [把骨骼关节角度重定向到电机关节]
│ ├── math_tool.hpp [滤波与四元数转换工具]
│ └── StartEndPoseDetector.hpp [唤醒动作检测]
└── main.cpp [主程序]
三个头文件加一个 main。整个仓库其余的体积——extern/glfw、sample_helper_libs/window_controller_3d、mujoco-3.1.5——全是渲染和仿真的支撑。真正的业务逻辑就这三个文件,而且中间那个 jointRetargeting.hpp 的注释直接把这一环的名字写出来了:retargeting,重定向。
深度相机是怎么「看出」骨架的
这一段值得单独说清楚,因为它是整条链的入口,也是它所有局限的来源。
一台深度相机给你的原始输出不是骨架,是深度图——画面里每个像素对应一个「这个方向上最近的物体离我多远」的数值。它和普通彩色图的区别在于,普通图的一个像素是颜色,深度图的一个像素是距离。把整张深度图还原到三维空间,就得到一片点云。
深度怎么来的?主流的几条路子分别是:结构光(打出已知图案,看图案在物体表面怎么变形)、飞行时间(发一束调制光,测它往返的相位差)、双目视觉(两个摄像头看同一个场景,算视差)。这几种传感原理各自的取舍我们在传感器卷里展开过,这里只需要知道结论:它们都能给你一张带距离的图,但都不直接给你「这是人的左肘」这种语义。
从深度图(通常还配合彩色图)跳到「关节点在哪」,靠的是另一层——学习模型。这正是 Azure Kinect 把驱动拆成 camera SDK 和 body tracking SDK 两个包的原因:前者管取图,后者管从图里推人。body tracking 这一层的输出是一组带三维位置的关节点,人体被表示成一副骨架。仓库里有个 src/sample_helper_includes/BodyTrackingHelpers.h,还有 sample_helper_libs/window_controller_3d/SkeletonRenderer.cpp——骨架渲染器,程序里那个 Kinect 可视化窗口画的就是它。
所以「无接触动捕」的本质是:相机负责测距离,模型负责认人。两层都可能出错,而且错法完全不同。
为什么中间必须插一道重定向
拿到人的骨骼关节角,能不能直接发给机器人的电机?
不能。这是本文最想说清楚的一点。
你和机器人之间至少有三类不匹配,每一类都会让「照搬角度」翻车:
第一类,比例不一样。 你的上臂多长、肩宽多少、躯干多高,和机器人的连杆尺寸不是一回事。如果你的控制目标是「手到达空间中某个点」,那么直接照抄关节角度得到的落点必然是错的——同样的肩肘角度,长手臂和短手臂够到的位置差得远。这一类问题在机械臂运动学那篇的正逆解框架下会更好理解:角度到位置的映射跟连杆参数强绑定,换个身体就换一套解。
第二类,关节数量和自由度构型不一样。 人的肩关节是个球窝关节,本身就能三向转动,肩胛还会跟着滑动;机器人的肩通常是几个单自由度电机串起来近似出来的。仓库里 unitree_h1 和 unitree_g1 的 mesh 文件名把这套结构写得很直白:shoulder_pitch_link、shoulder_roll_link、shoulder_yaw_link、elbow_link——一个人类意义上的「肩」,在这里是三个正交的转动关节依次串联。人的一个动作要拆解成这几个轴上分别转多少,这本身就是一次求解,不是一次赋值。
第三类,活动范围不一样。 每个电机关节都有物理限位,而人的柔韧性和机器人的限位不重合。你能做到的姿势,机器人可能到不了;就算算出来的目标角度在数学上成立,超出限位之后要么被驱动器截断、要么硬顶造成损伤。所以重定向必须做限位裁剪——把目标角度夹到合法区间里。
还有一类问题排在限位后面:自碰撞。人的手臂贴着身体侧面是自然姿势,机器人照做可能就是小臂撞躯干。人自己有皮肤触觉和本体感觉在实时兜底,机器人没有——你在相机前的动作再自然,也不代表映射过去的那套关节角是安全的。
把这三类加起来,就是 jointRetargeting.hpp 存在的全部理由:重定向不是「格式转换」,是一次带约束的再规划。输入是人的姿态,输出是机器人能执行、且不自伤的关节指令。
math_tool.hpp 里的两样东西也各有分工。四元数转换负责姿态表示——从相机里出来的关节朝向用四元数表达最省事,转成机器人关节需要的角度得换表示。滤波则是对付抖动:视觉估计出来的关节点是逐帧算的,帧与帧之间必然有跳变,不滤一下直接送进控制环,机器人会跟着一起抖。任何以视觉为输入的控制链路,滤波都不是可选项。
唤醒动作:安全边界写进了程序结构里
StartEndPoseDetector.hpp 这个文件名,第一眼容易当成一个小工具略过。但 README 专门给它开了一节,而且给出的理由很实在:如果程序一起来就立刻开始遥操作,可能造成事故和危险。
所以设计是这样的:程序启动之后遥操作默认不生效,你要做一个约定的唤醒动作才开始,再做一次才停止。README 描述的这个动作是——面对相机自然站立,上臂自然下垂,前臂向前抬起与上臂大致成直角,保持几秒。识别成功后终端打印 START.,再触发一次打印 END.。
这个设计有几层意思值得琢磨。
一是它解决了无接触方案特有的一个问题:相机一直在看,程序无法从「有没有人」判断你是不是想操作。穿戴式方案里,你手里握着控制器、头上戴着头显,本身就是明确的意图信号;无接触方案没有这个信号,就得靠一个不会被日常动作误触发的特定姿势来当开关。
二是这个动作被选成「上臂下垂、前臂前抬」不是随手挑的。它得同时满足几个条件:不容易在日常活动中意外做出来、在相机视角下左右手都不互相遮挡、以及各个关节都在骨骼追踪比较可靠的位置上。
三是它顺带解决了「初始姿态对齐」。遥操作最危险的一瞬间往往是刚接管的那一刻——如果人的当前姿势和机器人当前姿势差很远,接管瞬间机器人会猛地扑过去。用一个固定的唤醒姿势当起点,等于强制人和机器人从同一个已知状态开始。
顺带说,这个思路在另一条路线里也存在。xr_teleoperate 的 README 里同样要求:开始之前先把你的手臂对齐到机器人的初始姿态,避免启动瞬间的突然动作。两套输入设备完全不同,但这个安全动作的必要性是共通的。
无接触式换来什么、付出什么
先说付出的。这些不是这个仓库的缺陷,是这条技术路线的固有代价,换任何一台深度相机都躲不掉。
遮挡。 这是最硬的一条。相机只有一个视角,你的手挡住身体、身体挡住手、双手在胸前交叠——被挡住的那部分关节点,模型只能靠先验去猜。你在做一个精细的双手协同动作,恰恰是最容易互相遮挡的时候。
视野边界。 相机有固定的视场角和工作距离,你走出去就没了。穿戴式设备是跟着你走的,无接触方案要求你待在相机看得见的那块空间里。这也解释了为什么这类方案天然更适合「站在原地做上半身动作」而不是「满场跑」。
精度不如穿戴式。 这不需要实测数据也能从原理上推出来:穿戴式设备是直接测量自己的位姿,无接触方案是从图像里估计你的关节位置。一个是测量,一个是推断,中间隔着一层模型。
没有力反馈。 这条最容易被忽略,但对操作任务影响最大。机器人的手碰到东西了、抓紧了没有、用力过头了——这些信息在无接触方案里回不到你身上,你只能靠眼睛看。而人做精细操作时,触觉和力觉的作用远比我们以为的大。
再说换来的。
不用穿戴任何设备。 人站过去就能用,不需要戴、不需要校准穿戴位置、不需要考虑设备电量和佩戴舒适度。
上手快。 装两个 SDK、编译、跑起来、做个唤醒动作,链路就通了。对比一下另一条路要处理的东西——生成自签名证书、把根证书装到头显上、开防火墙端口、在头显浏览器里点过安全警告页——工程量不在一个量级。
适合演示和快速采集。 会议室里临时演示一段、或者短时间内让不同的人各录几组动作,无接触方案的换人成本几乎为零:下一个人走到相机前就行。
成本可能更低。 一台深度相机对比一整套 XR 头显加外设,账通常是这么算的。不过这一条依赖具体选型,别当成定论。
所以两条路线的分工其实挺清楚,而且不是谁比谁强的关系:
- 需要精细手部操作——抓取、装配、灵巧手做手指级动作——用穿戴式。手部关节多、互相遮挡严重,正好是相机最不擅长的场景。
- 需要全身大幅动作、快速演示、低门槛采集——用无接触式。这类动作幅度大、关节遮挡少,相机的短板不致命,而它的省事是实打实的。
和 xr_teleoperate 对照着看
同一个目标,宇树给了两个仓库。放在一起看,能读出不少东西。
先看同的部分,也就是这条链路里跟输入设备无关的那些环节:
- 两边都必须有重定向。
kinect_teleoperate是jointRetargeting.hpp;xr_teleoperate的teleop/robot_control/下有robot_arm_ik.py(手臂逆运动学)、hand_retargeting.py(灵巧手重定向的封装)以及一个dex-retargeting子模块。名字不同,干的是同一类事。 - 两边都必须滤波。这边是
math_tool.hpp里的滤波器,那边是teleop/utils/weighted_moving_filter.py。 - 两边都要带一套机器人模型文件。这边是
src/unitree_h1、src/unitree_g1下的 URDF 与 MJCF;那边是assets/下按型号分的一堆 URDF 和 mesh。 - 两边都有启动前的姿态对齐/安全确认,前面已经说过。
- 两边都提供仿真里先跑通的路径。这边直接把 MuJoCo 打进了仓库;那边通过参数接到独立的仿真仓库上。
再看不同的部分,这里要格外小心,只按事实源说:
两个仓库是两套独立实现,不是共用一个下游内核。kinect_teleoperate 是 C++ 工程,CMake 加 ninja 构建,产出一个可执行文件;xr_teleoperate 是 Python 工程,靠 conda 环境和 pip 安装,README 里明确要装 unitree_sdk2_python 来跟机器人通信。从两边的文件树看不到任何共享的代码模块——共享的是问题的形状,不是代码。
覆盖面上也有差别。xr_teleoperate 的 README 用一张表列出了它支持的机器人本体和末端执行器,并且有 --arm、--ee、--input-mode、--display-mode 这样一串启动参数来切换组合;它还带 --record 数据录制模式和 episode_writer.py,明确面向模仿学习的数据采集。kinect_teleoperate 的 README 没有对应的参数表和录制模块,仓库里目前只有那三个头文件加一个 main。
这里必须严格约束一句:这些差异只说明「这个仓库的公开代码暴露了什么」,不能反推「另一条路做不到什么」。kinect_teleoperate 的 README 里没有录制功能,只能得出「README 里没写」这个结论;它有没有别的用法、后续会不会加,仓库文档没说,我就不猜。同理,真机通信那一段在 kinect_teleoperate 的 README 里没有展开,我也不去推断它是怎么下发的。
关于头显那条路更完整的展开,可以看XR 设备遥操作那一篇。
读完这个仓库应该记住的三件事
第一,遥操作的难点不在采集,在映射。 拿到人的骨骼数据只是把门推开,真正的工程量在 retargeting 那一环——比例缩放、限位裁剪、自碰撞规避,一样都不能少。任何跳过这一环、把人的角度直接灌给电机的做法,都会在某个姿势上出事。
第二,输入设备决定了这套系统的能力边界,也决定了它的坑。 相机看不见的地方就是这套方案的盲区,这不是调参能解决的;同样,头显方案的证书配置和佩戴负担也不是换个算法能省掉的。选路线本质上是在选你愿意承受哪一类麻烦。
第三,安全动作要写进程序结构里,而不是写进使用说明里。 StartEndPoseDetector.hpp 是一个独立头文件,唤醒动作是程序状态机里的一道闸门,而不是文档里的一句「请注意安全」。这个区别,就是能给人用的系统和只能自己演示的 demo 之间的区别。
想继续往下走的话,卷 U 的其他文章里还有仿真、SDK、模仿学习这几条线;如果你还没有动手做过机器人本体,先从机器人与具身智能卷把关节、驱动、传感这几块补起来,再回头看遥操作,会顺很多。
上游仓库地址:https://github.com/unitreerobotics/kinect_teleoperate 与 https://github.com/unitreerobotics/xr_teleoperate。具体操作步骤以官方仓库和 https://support.unitree.com 的当前文档为准。