ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pico手柄+MuJoCo遥操作实战:从坐标转换到逆运动学全解析

Pico手柄+MuJoCo遥操作实战:从坐标转换到逆运动学全解析 1. 为什么选择Pico手柄来做Mujoco遥操作先说项目背景。我最近在做一套基于Mujoco的机器人仿真遥操作系统目标很直接戴上Pico VR头显双手各拿一支手柄在现实空间中自然移动手臂仿真环境里的机械臂末端就跟着我的手走。听起来像是一个标准玩法但真正跑起来之后我才意识到整个链路里最花时间的不是环境搭建、不是手柄协议而是坐标系转换。先聊聊方案选型。目前做仿真遥操作主流输入设备无非这么几类输入设备自由度成本上手难度适用场景键盘 鼠标低极低低快速调试、离散点位控制SpaceMouse 3D鼠标中中等中连续轨迹拖拽、示教数据手套高很高高灵巧手操作、精细抓取VR手柄Pico/Quest高较低中全身位姿映射、沉浸式遥操作我最终选了Pico手柄理由其实很朴素它自带完整的6DoF六自由度追踪位置三自由度、姿态三自由度一套拿到手就能用内部有IMU和光学追踪融合几十毫秒内能给出稳定的手柄位姿不需要额外搭动捕系统。对比SpaceMouse那种摇杆式的输入VR手柄最大的优势是你不需要学习映射逻辑——手怎么动机械臂末端就怎么动操作直觉几乎为零门槛。对比数据手套成本又友好太多Pico Neo 3或Pico 4的一对手柄在闲鱼或促销时几百块就能收到而一套商用数据手套动辄上万。实测下来对于末端位姿连续控制这个需求VR手柄是性价比最高的方案。顺带提一句我为什么没有用WebXR或者浏览器方案。虽然Pico浏览器支持WebXR串流延迟和帧率稳定性在复杂场景下不够可靠遥操作最怕的就是手柄位姿抖动或者延迟突刺一旦出现机械臂在仿真里就是一顿乱甩。所以我选择了更稳的PC端链路Pico串流 - OpenVR/OpenXR运行时 - Python - Mujoco。整套架构从硬件到仿真每一环都能拿到确定的位姿数据和控制频率排错也方便。整个项目的技术链路如下Pico手柄物理位姿 ↓ 无线串流 PC端OpenVR运行时SteamVR ↓ Python openvr库 6DoF位姿矩阵OpenVR世界坐标系Y-up ↓ 坐标转换Y-up → Z-up缩放偏置 Mujoco世界坐标系下的目标末端位姿 ↓ 逆运动学求解 机械臂各关节角目标值 ↓ Mujoco物理步进 仿真机械臂末端跟随手柄运动这篇文章的重点放在坐标转换上因为这是我实际花费时间最多、也最容易被后续项目复用的一段经验。但为了让整个流程完整可跑我会把环境搭建、手柄数据读取、控制环代码一并讲清楚。2. Mujoco仿真环境搭建从空环境到可动机械臂2.1 Windows 11上安装Mujoco的完整步骤Mujoco目前已经更新到3.x版本Python包安装非常简单本质就是一条pip命令pip install mujoco它会自动安装预编译的wheel包里面已经带了物理引擎本体和离屏渲染器不需要自己编译C代码。但很多人在这一步就卡住了因为安装完跑一个简单demo会报glfw相关错误或者窗口闪退。原因通常是Windows 11缺少Visual C运行库或者显卡驱动太老。建议先把这两件套补上安装Microsoft Visual C Redistributablex64版本这是Mujoco在Windows上跑起来的硬性依赖。去NVIDIA或AMD官网更新显卡驱动Mujoco 3.x的渲染后端对OpenGL版本有要求老驱动会直接报错。装完后跑一下官方自带的demo验证环境import mujoco import mujoco.viewer xml mujoco modeltest_scene worldbody light pos0 0 2 directionaltrue/ geom typeplane size1 1 0.1/ body pos0 0 0.1 freejoint/ geom typesphere size0.1 rgba1 0 0 1/ /body /worldbody /mujoco model mujoco.MjModel.from_xml_string(xml) data mujoco.MjData(model) with mujoco.viewer.launch_passive(model, data) as viewer: for _ in range(1000): mujoco.mj_step(model, data) viewer.sync()如果能看到一个红色小球自由落体并停在平面上环境就算通了。2.2 几个高频安装问题的排查链路结合热搜词里大家都在搜的Mujoco安装常见问题我把几个真实出现过的坑和排查步骤整理出来第一个坑ImportError: DLL load failed while importing mujoco这个问题几乎都出在mujoco.dll找不到依赖库上。排查顺序打开cmd输入where python确认当前Python环境。确认pip list里有mujoco且版本在2.3以上。运行python -c import mujoco看具体报错。如果依赖缺失重装VC运行库重启终端再试。第二个坑渲染窗口初始化失败报GLFW error这个大概率是环境变量问题。Mujoco在Windows下会尝试加载GLFW和OpenGL如果系统里装了多个OpenGL驱动或者缺少MESA环境就会初始化失败。我在Windows 11上遇到过一次最后是卸载了机器上残留的旧版OpenGL驱动再重装显卡驱动解决的。如果你用的是虚拟机或远程桌面建议直接放弃GUI viewer改用mujoco.Renderer做离屏渲染再把图像传回来显示。第三个坑加载自己训练的MJCF模型时报Unknown function in ...这通常是Mujoco版本升级后XML里的旧字段不再被支持。比如某些网页上的教程还在用default class...而新版语法已经变了。遇到这种问题不要慌直接去官方mujoco_menagerie仓库找对应机器人的最新MJCF文件比自己手写XML省事得多。我用的是Franka Panda机械臂直接从官方模型库里拉的模型。2.3 MJCF模型的加载与基本控制接口我这边用的机械臂模型是Franka PandaMujoco官方mujoco_menagerie里就有。加载方式import mujoco model mujoco.MjModel.from_xml_path(path/to/franka_emika_panda/scene.xml) data mujoco.MjData(model) # 查看关节列表 for i in range(model.nu): print(model.actuator_names[i])Mujoco里的控制逻辑很简单data.ctrl是一个数组每个元素对应一个执行器关节电机的目标值。你设置了ctrl然后调用mujoco.mj_step(model, data)仿真就推进一个物理步。后面做遥操作本质上就是不断把手柄解算出的关节目标灌进data.ctrl然后步进仿真。需要记住的一个点Mujoco里运动学计算依赖data.qpos广义坐标和data.qvel广义速度而控制接口是data.ctrl。你做逆运动学时通常是给一个期望末端位姿通过IK求出期望关节角然后设置到ctrl上。如果机械臂没有PD控制器直接设置ctrl会导致关节位置是开环的机械臂不会乖乖停在目标位置。所以MJCF模型里自带的motor执行器通常需要配合一个简单的关节PD控制器或者用官方模型里已经配好的position执行器。3. Pico手柄数据读取从硬件到Python的完整链路3.1 通信方案为什么走OpenVR而不是走Pico原生SDKPico手柄要拿到PC上现在主流有几条路Pico自家SDK、OpenXR、OpenVRSteamVR。我最后用的是Pico串流助手 SteamVR Python openvr库的组合。原因有几个Pico串流助手是官方工具稳定性和压缩延迟控制得不错。手柄位姿的精度很大程度依赖追踪算法Pico的inside-out追踪在手部快速移动时偶尔会有抖动但串流链路本身不会丢数据。OpenVR是PC端VR生态的事实标准。不管手柄是Pico还是Quest插上SteamVR都能被统一抽象为左右控制器设备接口固定Python有现成的openvr绑定不用自己写底层USB协议。OpenXR虽然更现代但Python绑定少调试工具也少。如果只是做研究验证OpenVR这条路更省事。3.2 通过openvr读取手柄6DoF位姿安装依赖pip install openvr读取手柄位姿的完整逻辑如下import openvr import numpy as np # 初始化OpenVR openvr.VR_Init() poses None def get_controller_pose(): # 获取所有设备的位姿 poses, _ openvr.VRCompositor().waitGetPoses() left_pose None right_pose None for device_index in range(openvr.k_unMaxTrackedDeviceCount): device_class openvr.VRSystem().getTrackedDeviceClass(device_index) if device_class openvr.TrackedDeviceClass_Controller: # 判断左右手 role openvr.VRSystem().getControllerRoleForTrackedDeviceIndex(device_index) pose poses[device_index] # 位姿矩阵是3x4行主序 mat np.array(pose.mDeviceToAbsoluteTracking).reshape(3, 4) if role openvr.TrackedControllerRole_LeftHand: left_pose mat elif role openvr.TrackedControllerRole_RightHand: right_pose mat return left_pose, right_pose这里拿到的mat是3x4矩阵前三列是旋转矩阵相对于OpenVR的世界坐标系最后一列是位置。注意OpenVR的世界坐标系是Y-up的右手坐标系这一点非常关键后面坐标转换全靠它。3.3 数据形态与频率说明OpenVR的数据刷新率通常跟头显的追踪频率一致Pico在串流SteamVR模式下一般是60Hz或者90Hz。对于遥操作来说这个频率足够了。但要注意waitGetPoses是阻塞式的如果串流画面掉帧这个函数也会跟着掉帧。所以我在代码里用的是getDeviceToAbsoluteTrackingPose配合一个独立线程来读数据避免画面掉帧影响控制。还有一个容易忽略的点刚拿到手柄位姿时不要直接当世界坐标用。双手拿手柄站在不同位置手柄位姿差异很大。遥操作时通常要定义一个初始零点——比如按下扳机键的瞬间记录当前手柄位姿作为参考基准之后的手柄相对运动都相对于这个零点来计算。这样操作者不管站在哪里都能以一个舒服的姿势开始控制。reference_pose None def on_trigger_pressed(): global reference_pose left, right get_controller_pose() reference_pose left # 比如以左手为基准这个相对位姿的思路后面会反复用到也是避免操作者手酸的关键。4. 坐标转换这个项目真正的拦路虎4.1 为什么非转不可回到标题里的坐标转换。很多人觉得Mujoco环境搭好、手柄数据能读到了接下来不就是把手柄位置赋给机械臂末端吗其实根本不是。原因有三层坐标系轴方向不同。OpenVR世界坐标系是Y-upY轴朝上而Mujoco里常见的机械臂模型约定Z轴朝上机器人学惯例也贴合重力方向。如果直接把OpenVR的位置塞给Mujoco你会发现手柄往上抬机械臂往屏幕里走完全错乱。物理尺寸映射需要缩放。现实中你手臂的移动范围可能只有0.5米左右但仿真里的机械臂工作空间可能是1米、2米。你需要一个比例因子把真实操作空间映射到仿真工作空间。初始对齐问题。手柄在世界坐标系里的原点和Mujoco世界坐标系里的机械臂基座原点并不重合。必须定义一个偏移让手柄移动到某个区域时机械臂末端正好在初始位置。4.2 Y-up到Z-up的旋转映射这是坐标转换的第一步。我需要一个旋转矩阵把OpenVR的Y-up坐标变成Mujoco的Z-up坐标。绕X轴旋转-90度即可实现Y-up到Z-up的映射。旋转矩阵R [1 0 0] [0 0 1] [0 -1 0]验证一下OpenVR坐标系中的Y轴单位向量(0, 1, 0)左乘R得到(0, 0, -1)。这表示OpenVR中的向上在Mujoco中变成了-Z方向等等不对应该验证实际上OpenVR的向上是YMujoco的向上是Z。绕X轴旋转-90度Y轴单位向量 (0,1,0) → 旋转-90°绕X轴Y → -Z还是 Z绕X轴旋转角度θ标准矩阵[1 0 0] [0 cosθ -sinθ] [0 sinθ cosθ]θ -90°时cos 0, sin -1[1 0 0] [0 0 1] [0 -1 0](0,1,0) → (0, 0, -1)。所以OpenVR的Y映射到Mujoco的-Z。这不对我想要的应该是Y→Z。那应该用绕X轴旋转90度[1 0 0] [0 0 -1] [0 1 0](0,1,0) → (0, 0, 1)即Y → Z。好这个才对。所以正确的旋转矩阵应该是绕X轴旋转**90度**R [1 0 0] [0 0 -1] [0 1 0]同时-Z轴会映射到Y。通常OpenVR的-Z是朝向屏幕内侧或者说用户前方映射到Mujoco的Y这通常没什么问题因为机器人正面朝向可以人为约定。四元数的转换也可以用同样的思路但更稳妥的做法是把OpenVR给的旋转矩阵左乘R得到新的旋转矩阵再转成四元数。不要试图在四元数层面直接变换容易在wxyz/xyzw的顺序上翻车。def yup_to_zup(mat_3x4): R_yup mat_3x4[:, :3] # 3x3旋转矩阵 t_yup mat_3x4[:, 3] # 位置向量 R_convert np.array([ [1, 0, 0], [0, 0, -1], [0, 1, 0] ]) R_zup R_convert R_yup t_zup R_convert t_yup mat_zup np.hstack([R_zup, t_zup.reshape(-1, 1)]) return mat_zup这一步做完手柄的手势方向已经和Mujoco对齐了但位置还是OpenVR原点下的绝对位置需要继续平移和缩放。4.3 位置偏移与操作空间缩放接下来处理位置。假设操作者在初始化时按下扳机记录一个参考位姿mat_ref_zup。之后每一帧的手柄位姿mat_cur_zup计算相对位移delta_pos mat_cur_zup[:, 3] - mat_ref_zup[:, 3]这个delta_pos就是操作者手部相对于初始位置的空间位移。然后乘一个缩放因子scale 0.6 # 根据实际工作空间调整 target_pos base_pos scale * delta_pos其中base_pos是Mujoco中机械臂末端的期望初始位置。你可以根据机械臂的零位姿态算出来比如Franka Panda的末端默认在基座前方约0.5米处那么base_pos就设成(0.3, 0.0, 0.5)之类的值。缩放因子的选择有讲究。值太大手柄微动一下机械臂就飞出工作空间值太小手部大幅移动机械臂才挪一点点操作很迟钝。我这边实测的经验是对于Franka Panda这种臂长1米左右的操作臂缩放0.5到0.8比较合适。对于小型的桌面机械臂工作空间20厘米量级缩放0.1到0.2。如果希望精细操作可以在手柄上某个触摸板上加一个变速档按住触摸板时缩放系数降到原来的1/5做微调。4.4 四元数顺序的坑Mujoco的wxyz与常见库的xyzw这是整个坐标转换过程中最容易阴沟翻船的地方。Mujoco内部使用四元数表示姿态时顺序是w, x, y, z也就是实部在前。而很多库比如scipy的Rotation.from_quat默认接受x, y, z, w。如果你从OpenVR拿到的旋转矩阵转四元数默认转了xyzw顺序直接塞给Mujoco的qpos机械臂的姿态会完全乱掉。我在写代码时统一封装了一个转换函数from scipy.spatial.transform import Rotation as R def mat_to_mujoco_quat(mat_3x4): # 输入: 经过yup_to_zup转换后的3x4矩阵 rot_matrix mat_3x4[:, :3] quat_xyzw R.from_matrix(rot_matrix).as_quat() # 拿到xyzw quat_wxyz np.roll(quat_xyzw, 1) # 移到wxyz return quat_wxyz这个坑我必须强调凡是涉及四元数和Mujoco交互的统一走这一个函数别在调用处再自己转一次。我刚开始时就是没注意在IK解算里用scipy转了一次又在赋值给qpos前用Mujoco的内部函数转了一次两次转换互相抵消姿态稳定地偏了180度排查了整整一个晚上。5. 从手柄末端位姿到机械臂关节角完整控制环5.1 控制频率与数据流设计手柄数据是60~90HzMujoco物理步进可以跑得非常快我这里设定的是200Hz步进。两者频率不一致不能每读一次手柄就步进一步也不能每个控制周期都去读手柄否则控制会抖。我的做法是开两个线程读取线程以OpenVR的频率不断更新当前目标末端位姿这个共享变量。控制线程以200Hz频率运行从共享变量里拿最新的目标位姿做IK设置关节目标步进仿真。这两个线程之间用threading.Lock保护共享变量避免读到半写状态。5.2 末端位姿平滑低通滤波是必须的手柄追踪在快速移动时会有微小的抖动直接用于控制会让机械臂末端出现高频颤振。解决方案是一个简单的一阶低通滤波alpha 0.3 smoothed_pos alpha * target_pos (1 - alpha) * smoothed_pos姿态也可以用球面线性插值slerp做平滑但我在实际项目中只对位置做了低通姿态平滑用了更简单的nlerp归一化线性插值效果够用计算还便宜。注意alpha值不要太小否则滞后感明显操作者会觉得跟不上手。我实测0.2到0.4是比较舒服的区间。5.3 逆运动学求解阻尼最小二乘法从末端位姿求关节角最常用的方法是基于雅可比矩阵的数值迭代。Mujoco自带mj_jac接口可以拿到雅可比矩阵所以我直接在控制循环里做阻尼最小二乘IKimport mujoco def ik_solve(model, data, target_pos, target_quat_wxyz, init_q, max_iter30): # 把当前关节角作为初始猜测 data.qpos[:7] init_q mujoco.mj_forward(model, data) # 找到末端body的id ee_body_id mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, ee_link) for _ in range(max_iter): # 当前末端位姿 cur_pos data.body(ee_body_id).xpos cur_quat data.body(ee_body_id).xquat # 位置误差 pos_err target_pos - cur_pos # 姿态误差四元数转旋转向量 err_quat quat_error(cur_quat, target_quat_wxyz) rot_err quat_to_rotvec(err_quat) err np.hstack([pos_err, rot_err]) # 6维误差向量 if np.linalg.norm(err) 1e-4: break # 雅可比矩阵3xN位置 3xN姿态 jacp np.zeros((3, model.nv)) jacr np.zeros((3, model.nv)) mujoco.mj_jac(model, data, jacp, jacr, target_pos, ee_body_id) J np.vstack([jacp[:3, :7], jacr[:3, :7]]) # 阻尼最小二乘 lambda_reg 0.05 dq J.T np.linalg.solve(J J.T lambda_reg**2 * np.eye(6), err) data.qpos[:7] dq # 关节限位 data.qpos[:7] np.clip(data.qpos[:7], model.jnt_range[:, 0], model.jnt_range[:, 1]) mujoco.mj_forward(model, data) return data.qpos[:7].copy()这个IK不是全局最优解但对连续控制场景足够。每次迭代时以上一帧的关节角为初始值收敛非常快通常2到5次迭代就到达目标精度。阻尼系数lambda_reg不能去掉否则在奇异位型附近雅可比矩阵奇异解会爆炸。5.4 完整控制循环的代码骨架把所有环节串起来主控循环如下def control_loop(): while running: with lock: target_pos smoothed_target_pos target_quat smoothed_target_quat # IK求解 q_target ik_solve(model, data, target_pos, target_quat, current_q) # 或者直接用关节PD设置目标位置 data.ctrl[:7] q_target # 步进仿真 mujoco.mj_step(model, data) # 更新平滑后的目标 update_smoothed_target()有一点需要提醒data.ctrl设置的是关节电机的目标值如果你的MJCF模型里是纯力矩电机这种开环控制会有静态误差。最省事的做法是在MJCF里给每个关节配一个position执行器Mujoco内置的position actuator会自动做关节PD控制量直接就是期望关节角省去自己调PD参数的麻烦。6. 实测效果与高频踩坑记录6.1 坐标转换没做好时的典型病征这部分是我最想分享的因为踩坑时的现象和最终原因之间往往隔着一层窗户纸。我把几个典型表现列出来如果你也在调同类系统可以对照排查病征一手柄往右动机械臂往左动。这说明坐标系发生了镜像通常是绕某个轴的旋转方向反了。检查你自己的转换矩阵看看是不是把绕X轴的90度和-90度搞反了。病征二手柄往上抬机械臂沿着水平方向乱飞。这是典型的Y-up/Z-up没转换OpenVR的Y轴位移被当成了Mujoco的X或Z轴位移。我之前第一次跑起来就是这个现象整个机械臂像喝醉了一样在水平面上乱窜。病征三机械臂末端位置对但姿态完全是拧的。姿态问题优先检查四元数顺序。Mujoco要wxyzscipy给xyzw差一个np.roll就天翻地覆。另外检查旋转矩阵左乘的顺序是R_convert R_hand还是R_hand R_convert这个顺序错了姿态同样会拧。病征四一切正常但机械臂运动有可感知的延迟和拖尾感。这种多半是平滑系数设得太小低通滤波滞后严重。把alpha调大到0.3以上再看看是不是IK迭代次数太少导致每帧只能走一部分。还有一种可能是控制线程频率太低试着手柄读取线程频率对齐。6.2 几个容易被忽略的细节手柄丢失追踪的容错处理。Pico手柄快速甩动或者被身体挡住时追踪会瞬间丢失OpenVR会返回上一次有效的位姿或者一个无效的pose。如果不做处理机械臂会突然停在原地然后等手柄恢复后猛跳一下。我的做法是拿到一帧位姿后检查旋转矩阵是否包含NaN以及位置是否发生突变位移超过5厘米就认为是异常帧异常帧直接丢弃用上一帧值顶替。初始参考位姿的坐标系基准。记录初始化基准时建议让操作者把双手放在一个舒适的自然位置然后按下扳机。如果操作者身高不同、站位不同都要重新校准。我写了个简单逻辑每次按下手柄的A键都重新记录参考位姿方便随时重新对齐。Mujoco的mj_forward和mj_step混用问题。在IK循环里必须调用mj_forward而不是mj_step因为mj_step会推进动力学并修改速度不适合作为纯运动学校准。在主控制循环里才用mj_step。如果你把IK里的mj_forward换成了mj_step会看到机械臂抖得非常厉害因为每帧都在改变速度而不是位置。渲染线程和控制线程的同步。如果用了mujoco.viewerviewer的sync()频率必须配合渲染帧率不要每个控制循环都sync一次否则窗口会变成PPT。在实际项目中我让viewer在独立线程里以30Hz左右同步控制线程不关心viewer是否卡顿。6.3 性能与实际操作体验整个链路跑通后我测试了匀速拖拽和快速抓取两种典型操作。平稳拖拽时机械臂末端和手柄目标位置的位置误差大概在毫米级主要来自IK迭代精度姿态误差在1度以内操作手感是指哪打哪。快速甩动时会有轻微的超调和回落这是低通滤波本身的特性但整体可控。一个意外的收获是这套方案不仅可以控制单臂我后来扩展成了双臂遥操作——左手手柄控制左臂右手手柄控制右臂坐标转换逻辑完全复用。只要把两个手柄的位置分别映射到两条机械臂的期望末端位姿即可。如果你也需要做双臂协作类的仿真验证这个扩展路径几乎没有额外成本。6.4 再分享一个调试小技巧最后聊一个很实用的调试方法。当你的机械臂行为完全不符合预期时不要先在Mujoco里看结果而是把目标末端位姿和IK输出的关节角打印出来再用Mujoco的官方查看器单独回放。这样可以快速定位问题出在坐标转换还是IK。我写了一个简单的数据记录模块把每一帧的目标位置、目标四元数、IK结果关节角都写入CSV之后用脚本离线分析。很多时候机械臂发疯的原因是手柄数据本身就包含了异常跳变而不是你的控制代码出了问题。这套仿真遥操作 坐标转换的方案从实际效果来看已经很成熟了。如果你要复现这个项目我建议把重点放在坐标转换和IK的调试上这两个环节一旦打通整个系统就像打通了任督二脉剩下的就是按需求调整参数。
返回列表