【具身笔记】【π0.5 微调与仿真】五、在MuJoCo中实现可视化仿真
概述与QA
本系列为我在vla上的第一个实践:基于lerobot做π0.5的推理 、微调 、仿真。本文主要介绍在在MuJoCo中实现可视化仿真。
下面列出几个QA来引入:
Q:什么是MuJoCo?
A:MuJoCo 是一个能精准模拟多刚体动力学、复杂碰撞和摩擦的物理引擎,是强化学习和机器人控制最常用的“虚拟训练场”。
1. 常见仿真环境的简单对比
下面给出MuJoCo、Isaac、Gazebo、PyBullet这四款主流仿真引擎的对比:
| 特性维度 | MuJoCo | NVIDIA Isaac Sim | Gazebo | PyBullet |
|---|---|---|---|---|
| 核心定位 | 学术界RL/控制研究的黄金标准 | 工业级大规模AI训练与高保真Sim-to-Real | ROS生态的系统级集成测试场 | 轻量级入门与快速原型验证 |
| 物理引擎 | 自研MuJoCo引擎 | NVIDIA PhysX 5 (GPU加速) | DART/Bullet/ODE | Bullet |
| 物理精度/特点 | 接触动力学极其精准、稳定,速度快 | 大规模并行能力极强,物理精度高 | 满足多数系统集成需求,精度中等 | 接触稳定性稍逊于顶级引擎,但能满足多数验证 |
| 渲染能力 | 一般 (OpenGL),非光线追踪,画质类似游戏 | 顶级,RTX光线追踪,照片级真实感 | 一般 (OGRE) | 较差,复古画质 |
| GPU并行 | 有限(部分变体如MJX支持) | 极强,可在GPU上并行模拟数千个环境,训练速度极快 | 有限 | 支持GPU加速,但并非核心设计 |
| ROS 2 集成 | 社区维护的wrapper,生态在增长 | 官方桥接,成熟 | 深度原生集成,事实上的ROS标准仿真器 | 社区维护的桥接 |
| 核心优势 | 接触仿真极快且准,是验证RL算法逻辑的理想选择 | GPU并行+光追渲染,为视觉Sim-to-Real和大规模训练而生 | 与ROS生态无缝集成,适合系统级测试和导航任务 | 极致轻量,pip install即可,学习曲线平缓 |
| 主要短板 | 视觉渲染弱,不直接支持光线追踪 | 硬件要求极高 (需RTX显卡),闭源,学习曲线陡峭 | 物理引擎选项多导致行为可能不一致,视觉一般 | 物理精度和视觉效果相对入门 |
| 典型硬件 | CPU即可 | 必须 NVIDIA RTX GPU | CPU | CPU |
本章选择MuJoCo作为仿真引擎,主要是考虑到其硬件要求不高、且LeRobot封装了适配接口,方便入门。
2. 环境准备
通过下面的指令进行MuJoCo的安装及环境激活:
# ===== 一次性的系统依赖(在 Ubuntu 本机终端) =====
sudo apt install -y python3.10-venv mesa-utils
# python3.10-venv:允许 Python 创建隔离虚拟环境。
# mesa-utils:提供 glxinfo,用来检查本机 OpenGL 渲染信息。
# ===== 创建 MuJoCo 虚拟环境 =====
mkdir -p ~/venvs
python3 -m venv ~/venvs/mujoco
# 在 ~/venvs/mujoco 创建独立 Python 环境,不修改 Ubuntu 系统 Python。
# ===== 安装 MuJoCo =====
~/venvs/mujoco/bin/python -m pip install --upgrade pip
~/venvs/mujoco/bin/python -m pip install mujoco
# 第一条更新该虚拟环境中的 pip。
# 第二条安装 MuJoCo Python 绑定及其必要依赖。
# ===== 日常进入 MuJoCo 环境 =====
source ~/venvs/mujoco/bin/activate
# 激活后,python 和 pip 都指向 ~/venvs/mujoco 内的版本。
python -c "import mujoco; print(mujoco.__version__)"
# 验证 MuJoCo 能被导入;当前应输出 3.15.0。
deactivate
# 退出虚拟环境,恢复 Ubuntu 系统 Python。
3. 本地ubuntu的可视化实践
由于我自己有一台ubuntu的电脑,所以刚好可以用来做一下MuJoCo的本地可视化实践。大致流程可分为构建MJCF和写代码让其运动并可视化。
3.1. 构建MJCF
通过下面的代码构建一个MJCF(MuJoCo 的 XML 模型格式) xml,说简单点就是构造一个适配MuJoCo的机器人模型:
<mujoco model="a1_pendulum">
<!-- 全局配置选项,步长为0.01s,即100hz -->
<option timestep="0.01" gravity="0 0 -9.81"/>
<worldbody>
<!-- 定义光源 -->
<light pos="0 0 2"/>
<!-- 定义几何体:地面 -->
<geom name="floor" type="plane" size="2 2 0.1" rgba="0.8 0.8 0.8 1"/>
<!-- 定义刚体pendulum,钟摆 -->
<body name="pendulum" pos="0 0 1">
<!-- 定义这个body相对于父级怎么运动,一个铰链joint,旋转轴axis绕y轴转,阻尼damping -->
<joint name="hinge" type="hinge" axis="0 1 0" damping="0.1"/>
<!-- 定义这个body的形状,capsule胶囊体,
fromto="0 0 0 0 0 -0.5":从 body 原点 (0,0,0) 延伸到 (0,0,-0.5),长度 0.5 米,方向竖直向下。
密度density 500 -->
<geom
name="rod"
type="capsule"
fromto="0 0 0 0 0 -0.5"
size="0.03"
density="500"/>
<!-- 末端标记 -->
<site name="tip" pos="0 0 -0.5" size="0.04" rgba="1 0 0 1"/>
</body>
</worldbody>
<!-- 执行器,给hinge加执行器,传动比/缩放系数gear为1 -->
<actuator>
<motor name="hinge_motor" joint="hinge" gear="1" />
</actuator>
<sensor>
<!-- 关节位置传感器 -->
<jointpos name="hinge_position" joint="hinge" />
<!-- 关节速度传感器 -->
<jointvel name="hinge_velocity" joint="hinge" />
</sensor>
</mujoco>
这里简单构造了一个倒立摆,给其加上了运动执行器和相关传感器。
3.2. 运动及可视化
通过下面的代码引入MJCF模型,并让其动起来,在MuJoCo中可视化:
from pathlib import Path
import time
import mujoco
import mujoco.viewer
# 加载MJCF模型
model_path = Path(__file__).with_name("A1_1_pendulum.xml")
model = mujoco.MjModel.from_xml_path(str(model_path))
# 把目标引到data上
data = mujoco.MjData(model)
# 初始化状态,用mj_forward让状态立即生效
data.qpos[0] = 0.8
mujoco.mj_forward(model, data)
# 被动模式打开 viewer,物理推进仍然由代码控制;与之相对的是 launch(主动模式),它会自动以实时速度跑仿真,只能观察
with mujoco.viewer.launch_passive(model, data) as viewer:
while viewer.is_running() and data.time < 8.0:
# 取整秒
phase = int(data.time // 1.0)
data.ctrl[0] = 0.2 if phase % 2 == 0 else -0.2
# 单步执行
mujoco.mj_step(model, data)
# 同步
viewer.sync()
time.sleep(model.opt.timestep)
结果:


可以看到倒立摆在画面中左右摆动了起来。
4. 云端的MuJoCo实践
由于本地的显卡不足以支持我的模型实验,所以我需要在云端也打通MuJoCo相关的链路,以便后续云端训练好模型后能方便的进行可视化。
4.1. 自己构建action让机械臂在MuJoCo中动起来
通过下面的代码手动让机械臂在MuJoCo中动一点,然后读取其状态:
import os
import numpy as np
os.environ["MUJOCO_GL"] = "egl"
os.environ["PYOPENGL_PLATFORM"] = "egl"
os.environ["LIBERO_CONFIG_PATH"] = "/root/autodl-tmp/libero-config"
from libero.libero import benchmark
from libero.libero.envs import OffScreenRenderEnv
# 获取任务
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
bddl_file_name=bddl_path,
robots=["Panda"],
controller="OSC_POSE",
camera_names=["agentview", "robot0_eye_in_hand"],
camera_heights=256,
camera_widths=256,
render_gpu_device_id=0,
)
# reset,记录初始状态
obs_before = env.reset()
time_before = env.sim.data.time
eef_before = obs_before["robot0_eef_pos"].copy()
joint_before = obs_before["robot0_joint_pos"].copy()
print("=== 动作前 ===")
print("MuJoCo time:", time_before)
print("末端位置:", eef_before)
print("关节位置:", joint_before)
# 手动构建action,前三维为末端位置增量xyz,中三维末端姿态增量(旋转量),最后一维夹爪控制
# 这个7维动作定义是由controller="OSC_POSE"决定的
action= np.zeros(7, dtype=np.float32)
# 尝试x轴上动0.2
action[0] = 0.2
print(f"\naction输入:{action}")
obs_after, reward, done, info = env.step(action)
time_after = env.sim.data.time
eef_after = obs_after["robot0_eef_pos"].copy()
joint_after = obs_after["robot0_joint_pos"].copy()
print("\n=== 动作后 ===")
print("MuJoCo time:", time_after)
print("末端位置:", eef_after)
print("关节位置:", joint_after)
print("\n=== 变化 ===")
print("物理时间增量:", time_after - time_before)
print("末端位置变化:", eef_after - eef_before)
print("关节位置变化:", joint_after - joint_before)
print("底层 ctrl:", env.sim.data.ctrl)
print("reward:", reward)
print("done:", done)
print("是否成功:", env.check_success())
print("接触对数量 ncon:", env.sim.data.ncon)
env.close()
print("\nA1_5_ENV_STEP_OK")
其中:
# 获取任务
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
bddl_file_name=bddl_path,
robots=["Panda"],
controller="OSC_POSE",
camera_names=["agentview", "robot0_eye_in_hand"],
camera_heights=256,
camera_widths=256,
render_gpu_device_id=0,
)
suite拿到的是libero数据集“libero_spatial”这一类别下的第一个任务,该任务有其对应的bddl文件,即Behavior Domain Definition Language(行为领域定义语言),MuJoCo拿到了这个bddl,便可以由此构建该任务的初始场景。说的更直白些,这个任务具体来说是把碗夹到盘子上,那么MuJoCo加载对应的bddl后,环境中便会出现对应的碗、盘子等物体。
# reset,记录初始状态
obs_before = env.reset()
time_before = env.sim.data.time
eef_before = obs_before["robot0_eef_pos"].copy()
joint_before = obs_before["robot0_joint_pos"].copy()
接着通过env.reset()拿到机械臂的初始观察结构体,得到其初始的状态信息,方便与运动后做对比。
# 手动构建action,前三维为末端位置增量xyz,中三维末端姿态增量(旋转量),最后一维夹爪控制
# 这个7维动作定义是由controller="OSC_POSE"决定的
action= np.zeros(7, dtype=np.float32)
# 尝试x轴上动0.2
action[0] = 0.2
print(f"\naction输入:{action}")
obs_after, reward, done, info = env.step(action)
手动构建了一个action,通过env.step(action)让机械臂以此action为参数做运动,然后拿到其运动之后的观测信息。最后得到运动前后的状态对比:
[robosuite WARNING] No private macro file found! (__init__.py:7)
[robosuite WARNING] It is recommended to use a private macro file (__init__.py:8)
[robosuite WARNING] To setup, run: python /root/miniconda3/envs/lerobot/lib/python3.12/site-packages/robosuite/scripts/setup_macros.py (__init__.py:9)
[info] using task orders [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Local assets not found. Downloading from HuggingFace Hub...
Assets already downloaded at /root/.cache/libero/assets
=== 动作前 ===
MuJoCo time: 0.0
末端位置: [-0.20846466 0. 1.17327948]
关节位置: [ 0. -0.16103739 0. -2.44459747 0. 2.2267522
0.78539816]
action输入:[0.2 0. 0. 0. 0. 0. 0. ]
=== 动作后 ===
MuJoCo time: 0.05000000000000003
末端位置: [-2.07448173e-01 1.25641091e-10 1.17312582e+00]
关节位置: [ 1.23965311e-10 -1.58773374e-01 4.88064534e-10 -2.44314655e+00
-1.30078402e-09 2.22994845e+00 7.85398163e-01]
=== 变化 ===
物理时间增量: 0.05000000000000003
末端位置变化: [ 1.01648798e-03 1.25641091e-10 -1.53660407e-04]
关节位置变化: [ 1.23965311e-10 2.26401465e-03 4.88064534e-10 1.45091611e-03
-1.30078402e-09 3.19624830e-03 2.11285434e-11]
底层 ctrl: [ 1.14412382e-05 -1.65487243e+01 1.64244251e-05 1.85914614e+01
-2.42811908e-05 2.15043672e+00 2.60274124e-07 2.00000000e-02
-2.00000000e-02]
reward: 0.0
done: False
是否成功: False
接触对数量 ncon: 0
A1_5_ENV_STEP_OK
细究可以发现,action的第一位应代表末端x方向上的移动距离,这里给的是0.2,但实际的变化量却是1.01648798e-03,这是因为action输入之后实际还会进行一定比例的缩放,所以机械臂的真实位移跟输入并不相等。
4.2. 加入可视化,让机械臂连续运动,并录制视频
通过下面的代码让机械臂连续运动并基于相机画面回放制作视频:
import os
import numpy as np
import imageio.v2 as imageio
os.environ["MUJOCO_GL"] = "egl"
os.environ["PYOPENGL_PLATFORM"] = "egl"
os.environ["LIBERO_CONFIG_PATH"] = "/root/autodl-tmp/libero-config"
from libero.libero import benchmark
from libero.libero.envs import OffScreenRenderEnv
output_path = "/root/autodl-tmp/workspace/my_test/week3/a1_5_libero_agentview.mp4"
suite_class = benchmark.get_benchmark("libero_spatial")
suite = suite_class()
task_id = 0
task = suite.get_task(task_id)
# 搭完整环境
bddl_path = suite.get_task_bddl_file_path(task_id)
env = OffScreenRenderEnv(
bddl_file_name=bddl_path,
robots=["Panda"],
controller="OSC_POSE",
camera_names=["agentview", "robot0_eye_in_hand"],
camera_heights=256,
camera_widths=256,
render_gpu_device_id=0,
)
obs = env.reset()
eef_start = obs["robot0_eef_pos"].copy()
# 创建视频写入器,每秒20帧
writer = imageio.get_writer(output_path, fps=20)
try:
for step in range(100):
action= np.zeros(7, dtype=np.float32)
action[0] = 0.4 if step < 50 else -0.4
# 记录执行动作前的外部相机画面
writer.append_data(obs["agentview_image"])
obs, reward, done, info = env.step(action)
if step % 20 == 0:
print(
f"step={step:3d}, "
f"time={env.sim.data.time:.2f}s, "
f"eef_pos={obs['robot0_eef_pos']}"
)
finally:
writer.close()
eef_end = obs["robot0_eef_pos"].copy()
env.close()
print("\n起点末端位置:", eef_start)
print("终点末端位置:", eef_end)
print("视频已保存:", output_path)
print("A1_5_RENDER_VIDEO_OK")
大体流程跟4.1中的类似,只是视频生成的方式是基于相机的画面一帧一帧的拼成的。
输出:
step= 0, time=0.05s, eef_pos=[-2.06419356e-01 2.22131802e-10 1.17297472e+00]
step= 20, time=1.05s, eef_pos=[-1.11076906e-01 -4.63826865e-09 1.16437695e+00]
step= 40, time=2.05s, eef_pos=[-9.97307539e-03 -1.39111087e-08 1.15975094e+00]
step= 60, time=3.05s, eef_pos=[ 1.10731097e-02 -1.44967077e-08 1.17882865e+00]
step= 80, time=4.05s, eef_pos=[-9.45860408e-02 -4.35499803e-09 1.17959858e+00]
起点末端位置: [-0.20846466 0. 1.17327948]
终点末端位置: [-1.94633736e-01 3.18126055e-09 1.18002965e+00]
视频已保存: /root/autodl-tmp/workspace/my_test/week3/a1_5_libero_agentview.mp4
A1_5_RENDER_VIDEO_OK

最后生成了视频,并观测到机械臂顺x轴来回运动。
总结
本文介绍了MuJoCo并将其与主流仿真引擎做了对比、然后介绍了在本地和云端进行的MuJoCo实践。
文章仅做学习分享,如有谬误,欢迎指正。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)