MuJoCo 全流程实战教程:从零搭建一个仿真实验

该文章已生成可运行项目,

前言:

本教程基于MuJoCo官方文档中提供的python接口(对应的MuJoCo版本为3.2.7),并结合论文《Goal-Driven Robotic Pushing Using Tactile and Proprioceptive Feedback》中的物体推动实验(如下图),让读者通过代码实操快速上手MuJoCo。

通过在MuJoCo中复现论文中的物体推动实验,读者可以学习到:

  1. 如何获取并导入机械臂模型
  2. 如何修改MJCF文件(MuJoCo中定义模型的xml文件)
  3. 如何通过IKPy库简单控制机械臂
  4. 如何获取仿真过程中的各种数据(如力传感器的数据)

注意事项:

1、在开始本教程前,建议读者先去官方文档中简单运行下Tutorial notebook提供的入门教程。

2、MuJoCo的官方文档写的不够通俗,但是它确实是将所有的API都清晰列了出来,在自学时可以借助AI工具快速了解各个函数的作用。(本教程是编者自己摸索出来的其中一种实现方式,水平有限,写法可能不是最优解)

3、目前在具身智能的强化学习中主要用dm_control库,这是DeepMind公司在MuJoCo的基础上,补充了强化学习的框架等别的功能,里面是包含了完整的MuJoCo的。本教程不涉及强化学习相关概念,读者可以按需要直接去学习dm_control。

4、获取本教程的代码请点击:https://github.com/VincentXun/tutorial_for_mujoco

此代码对应着第五章物体推动实验。

一、安装MuJoCo

我这里选择新建一个Anaconda虚拟环境:

conda create -n tutorial_for_mujoco python=3.9

接着在虚拟环境中执行:

pip install mujoco

下载时记得开个VPN,不然下载速度很慢。

然后打开PyCharm,或是其他python编辑器,选择好对应的装有MuJoCo的环境。

二、获取并导入UR5e机械臂模型

论文中使用的常见的UR5e机械臂,我们可以在MuJoCo官方模型库Model Gallery中找到对应的模型。

打开后可以看到UR5e的MuJoCo模型文件"universal_robots_ur5e":

我们将模型下载,并放到工程目录里:

接下来创建一个main.py, 并运行如下代码:

import mujoco.viewer

def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)

    with mujoco.viewer.launch_passive(model, data) as viewer:
        while viewer.is_running():
            mujoco.mj_step(model, data)
            viewer.sync()

if __name__ == "__main__":
    main()

运行后可以看到MuJoCo的官方图形用户界面:

点击左侧Simulation下的"Load key",可以看到机械臂的姿态变成了上图所示的样子。Load key指加载UR5e模型默认的keyframe,即机械臂的默认姿态对应的六个关节值。同时读者可以左右滑动改变右侧Control的关节电机设定值来观察机械臂的运动。

接下来修改下代码:

import mujoco.viewer
import time

def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)
    data.ctrl[:6] = [-1.57, -1.34, 2.65, -1.3, 1.55, 0]

    with mujoco.viewer.launch_passive(model, data) as viewer:
        while viewer.is_running():
            mujoco.mj_step(model, data)
            viewer.sync()
            time.sleep(0.002) # 让动画速度变慢,不然更新太快看不清机械臂的运动过程

if __name__ == "__main__":
    main()

运行后可以看到UR5e慢慢地运动到如下位姿:

其中,

data.ctrl[:6] = [-1.57, -1.34, 2.65, -1.3, 1.55, 0]

表示MJCF文件中的前6个驱动器actuator的控制值,此时对应着机械臂的6个关节电机的控制值。具体可以在ur5e.xml中看到:

  <actuator>
    <general class="size3" name="shoulder_pan" joint="shoulder_pan_joint"/>
    <general class="size3" name="shoulder_lift" joint="shoulder_lift_joint"/>
    <general class="size3_limited" name="elbow" joint="elbow_joint"/>
    <general class="size1" name="wrist_1" joint="wrist_1_joint"/>
    <general class="size1" name="wrist_2" joint="wrist_2_joint"/>
    <general class="size1" name="wrist_3" joint="wrist_3_joint"/>
  </actuator>

这里对应着6个PD控制的关节电机,data.ctrl设置的就是这6个电机的目标值。

至此,我们已经掌握了如何导入模型、如何开始仿真、如何将仿真过程可视化这几个重要步骤了。接下来将详细讲解下每一行代码。

首先看到这两行:

    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)

可以看到读取了scene.xml模型文件。这是MuJoCo的特点之一,模型model与仿真过程中产生的各种动态数据data分开储存。

下面打开scene.xml文件,这是一个用来定义仿真场景的文件(例如里面定义了光照与地板),其中可以看到里面包含了UR5e的机械臂模型:

<include file="ur5e.xml"/>

之后我们需要对scene.xml与ur5e.xml这两个MJCF文件进行修改,添加所需的力传感器与被推动物体的模型。

接着往下看代码,打开图形用户界面后,便进入了循环,不断运行

mujoco.mj_step(model, data)

这是更新仿真状态的函数,非常重要,每执行一次mj_step,仿真便会前进0.002s(默认值)。简单而言,比如我定义了一个初始位置在半空中的小球,那么每执行一次mj_step,MuJoCo就会根据重力加速度的值计算出0.002s后小球会下降多少的距离,同时更新一次data中小球的xyz坐标信息。

下一行代码:

viewer.sync()

是用来更新图形用户界面中的动画的。

三、通过IKPy库求解逆运动学,控制机械臂运动

3.1 前言

逆运动学Inverse Kinematics,即给定机械臂末端执行器的目标位置与姿态,然后求出机械臂对应的各个关节值。

在本教程中我们将通过IKPy这个库来实现这一点,并将得到的关节值通过线性插值的方法传给MuJoCo中ur5e机械臂的各个关节电机,以此实现对机械臂的控制。

读者可以先去IKPy的官方GitHub简单了解下,为了方便阅读,我把官方提供的教程截图了。

简而言之,IKPy库需要我们提供机械臂的URDF文件。下面我们先在环境中安装所需要的库:

pip install ikpy matplotlib transforms3d

接下来去获取ur5e的URDF文件,可以看到在MuJoCo模型库中也是很贴心地给出了ur5e的官方URDF文件,

点进去后,将整个ur_e_description文件下载,

打开urdf文件,可以看到官方提供的.xacro格式的文件,这需要我们通过ROS将其转换为.urdf文件。

这是获取ur5e的urdf文件的官方途径,方便起见,读者可以直接在教程代码https://github.com/VincentXun/tutorial_for_mujoco中获取转换好的ur5e_orig.urdf。

下面我将原始的ur5e_orig.urdf文件重命名为ur5e.urdf,并将ur5e.urdf文件放在model文件夹目录下。

3.2 IK计算

如下图,我们的目标是让ur5e从初始姿态运动到准备推动物块的姿态:

我们先利用IKPy提供的可视化工具来简单验证下,运行如下代码:

import ikpy.chain
import ikpy.utils.plot as plot_utils
import matplotlib.pyplot as plt
import transforms3d as tf

def main():
    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf")
    ee_pos = [-0.13, 0.5, 0.1] # 末端执行器在世界坐标系下的目标位置的xyz 
    ee_euler = [3.14, 0, 1.57] # 末端执行器的目标欧拉角姿态(弧度)
    ee_orientation = tf.euler.euler2mat(*ee_euler) # 将欧拉角姿态用旋转矩阵表示
    ref_pos = [0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0] # 提供初始关节角猜测,用于优化求解
    
    fig, ax = plot_utils.init_3d_figure()
    my_chain.plot(my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos), ax)
    plt.show()

if __name__ == "__main__":
    main()

运行后可以看到IKPy计算出的逆运动学符合期望位置与姿态:

下面解释inverse_kinematics()函数:

my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos)

前两个参数是目标位置与目标姿态(这里参考了深蓝课堂的课程,发现姿态这里要用旋转矩阵来表示,用欧拉角得到的结果会有偏差);第三个参数"all"表示要绕着xyz三个轴进行旋转;

第四个参数initial_position为IK的初始猜测解。一次逆运动学可能有很多种不同的关节角结果,猜测解就是尽可能让它收敛到我希望的那个结果,因此猜测解需要尽可能的准确。如下图,我们可以通过MuJoCo的图形用户界面来手动调整机械臂的姿态,当此时的姿态大致符合我们的要求时,记录下对应的关节角,并以这一组关节角作为初始猜测解。在本次实验中,由于机械臂只需要进行简单的向前推动的动作,所以不需要频繁地修改这个猜测值。

至于为什么除了ur5e的6个关节角外还多了两个数值0,那是因为URDF中,"base_link""ee_fixed_joint"即使不参与IK计算,也必须赋0,否则IKPy会报错。

ok,下面把IK结果传给MuJoCo,看看仿真效果如何。

3.3 在MuJoCo中验证IK

在开始前,先在scene.xml文件中可视化MuJoCo世界坐标系原点的坐标轴,以及可视化目标点,方便我们直观地判断IK结果是否准确。

打开scene.xml,在其中添加两个body,一个名字为"world_frame",一个名字为"target":

    <worldbody>
        <light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
        <geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>

        <!--你需要添加的-->
        <body name="world_frame" pos="0 0 0">
            <geom name="X" type="cylinder" size="0.005" fromto="0 0 0 0.3 0 0" rgba="1 0 0 1" contype="0"
                  conaffinity="0"/>
            <geom name="Y" type="cylinder" size="0.005" fromto="0 0 0 0 0.3 0" rgba="0 1 0 1" contype="0"
                  conaffinity="0"/>
            <geom name="Z" type="cylinder" size="0.005" fromto="0 0 0 0 0 0.3" rgba="0 0 1 1" contype="0"
                  conaffinity="0"/>
        </body>
        
        <body name="target" pos="-0.13 0.5 0.1">
            <geom size="0.02" rgba="0 1 0 0.5" contype="0" conaffinity="0"/>
        </body>

    </worldbody>

这是我们第一次对MJCF文件进行修改,读者可以先看下MJCF的body标签。这里我们在<worldbody>这个标签下添加了一个新的<body>,名字叫"world_frame",坐标在世界坐标系的原点处。其中,这个"world_frame"的body还拥有3个子标签<geom>,分别用来描述指向X、Y、Z坐标轴正方向的圆柱形几何体。

接下来将IKPy算出的关节值传给MuJoCo,运行如下代码:

import mujoco.viewer
import time
import ikpy.chain
import transforms3d as tf


def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)
    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf")

    ee_pos = [-0.13, 0.5, 0.1]
    ee_euler = [3.14, 0, 1.57]
    ref_pos = [0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0]
    ee_orientation = tf.euler.euler2mat(*ee_euler)

    joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos)
    ctrl = joint_angles[1:-1]
    data.ctrl[:6] = ctrl

    with mujoco.viewer.launch_passive(model, data) as viewer:
        while viewer.is_running():
            mujoco.mj_step(model, data)
            viewer.sync()
            time.sleep(0.002)

if __name__ == "__main__":
    main()

可得到的仿真结果并不符合预期,机械臂的末端执行器并没有抵达绿色球处的目标点[-0.13,0.5,0.1]:

这说明URDF文件的坐标系与MuJoCo的坐标系不一致。从结果上看,感觉URDF的坐标系的XY轴与MuJoCo的XY轴相反。为了确保IKPy计算的坐标与MuJoCo保持一致,我们需要知道此时的误差是什么,才能准确地调整坐标系。

下面先打印出此时的末端执行器的xyz坐标看一下,运行如下代码:

import mujoco.viewer
import time
import ikpy.chain
import transforms3d as tf

def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)
    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf")

    ee_pos = [-0.13, 0.5, 0.1]
    ee_euler = [3.14, 0, 1.57]
    ref_pos = [0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0]
    ee_orientation = tf.euler.euler2mat(*ee_euler)
    ee_id = model.site("attachment_site").id # 获取末端执行器对应的id

    joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos)
    ctrl = joint_angles[1:-1]
    data.ctrl[:6] = ctrl

    with mujoco.viewer.launch_passive(model, data) as viewer:
        while viewer.is_running():
            mujoco.mj_step(model, data)
            viewer.sync()
            time.sleep(0.002)

    ee_pos = data.site_xpos[ee_id] # 获取此时末端执行器对应的xyz坐标信息
    print(ee_pos)

if __name__ == "__main__":
    main()

此处的"attachment_site"对应着ur5e.xml中的机械臂最后一个关节处,用于连接工具的位置点,此时我们直接认为这个<site>标签对应的标记点就是ur5e的末端执行器。值得注意的是,用户在MJCF中创建的所有实体<body>与标记点<site>,在加载xml文件后,MuJoCo都会按照定义的顺序给每个元素进行标号,在仿真过程中我们可以实时通过id来获取对象当前的各种仿真数据。

打印出来的结果是:[ 0.12974408 -0.49788067  0.0925184 ],与给定的目标[-0.13,0.5,0.1]相比,可以确定URDF文件的XY轴与MJCF文件的相反了。我的解决办法是:将URDF文件的坐标系绕着Z轴旋转180°。具体实现方式是在base_link处增加一个虚拟关节,此虚拟关节会绕着base_link旋转180°,那么后面的所有关节都相当于是跟着一起旋转了(这个办法有点复杂,应该存在更优的解决办法)。

对ur5e.urdf文件的修改如下,添加一个"global_transform"关节,同时记得修改"shoulder_pan_joint"的父关节。

...  
<link name="base_link">
    <visual>
      <geometry>
        <mesh filename="package://test_and_learn/meshe/ur5e/visual/base.dae"/>
      </geometry>
      <material name="LightGrey">
        <color rgba="0.7 0.7 0.7 1.0"/>
      </material>
    </visual>
    <collision>
      <geometry>
        <mesh filename="package://test_and_learn/meshe/ur5e/collision/base.stl"/>
      </geometry>
    </collision>
    <inertial>
      <mass value="4.0"/>
      <origin rpy="0 0 0" xyz="0.0 0.0 0.0"/>
      <inertia ixx="0.00443333156" ixy="0.0" ixz="0.0" iyy="0.00443333156" iyz="0.0" izz="0.0072"/>
    </inertial>
  </link>
  
  <!--你需要添加的:-->
  <!-- Global coordinate transformation (rotate 180 degrees around Z axis) -->
  <joint name="global_transform" type="fixed">
    <origin xyz="0 0 0" rpy="0 0 3.141592653589793"/>
    <parent link="base_link"/>
    <child link="transformed_base_link"/>
  </joint>
  <link name="transformed_base_link"/>

  <joint name="shoulder_pan_joint" type="revolute">
    <parent link="transformed_base_link"/>  <!--记得修改parent_link-->
    <child link="shoulder_link"/>
    <origin rpy="0.0 0.0 0.0" xyz="0.0 0.0 0.163"/>
    <axis xyz="0 0 1"/>
    <limit effort="150.0" lower="-3.141592653589793" upper="3.141592653589793" velocity="3.14"/>
    <dynamics damping="0.0" friction="0.0"/>
  </joint>
  <link name="shoulder_link">
...

修改完URDF文件后,不要忘了代码中也要进行相应修改:

...
ref_pos = [0, 0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0]
...
ctrl = joint_angles[2:-1]
...

 最后,运行代码,打印出的末端执行器的坐标为[-0.13002427  0.49752776  0.0925558 ],成功!

至于在运行时IKPy会有一些warning:

想要消除,只需要在读取URDF时将不参与计算的关节显式设置为False:

    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf",
                                               active_links_mask=[False, False] + [True] * 6 + [False])

至此,我们已经实现了通过IKPy进行逆运动学求解,在MuJoCo中对机械臂进行控制了。 

3.4 对关节空间下的轨迹进行线性插值

本小节的目标是:让机械臂末端执行器沿着Y轴正方向近似匀速地运动一段距离。这里设置起始点为[-0.13,0.3,1],终点为[-0.13,0.6,1]。为了实现"近似匀速"这一效果,一种简单的办法是:对两点之间的关节空间坐标系下的轨迹进行线性插值处理。具体代码如下,读者也可以获取末端执行器"wrist_3_link"的线速度来简单验证:

import mujoco.viewer
import ikpy.chain
import transforms3d as tf
import numpy as np


def viewer_init(viewer):
    """渲染器的摄像头视角初始化"""
    viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE
    viewer.cam.lookat[:] = [0, 0.5, 0.5]
    viewer.cam.distance = 2.5
    viewer.cam.azimuth = 180
    viewer.cam.elevation = -30


class JointSpaceTrajectory:
    """关节空间坐标系下的线性插值轨迹"""
    def __init__(self, start_joints, end_joints, steps):
        self.start_joints = np.array(start_joints)
        self.end_joints = np.array(end_joints)
        self.steps = steps
        self.step = (self.end_joints - self.start_joints) / self.steps
        self.trajectory = self._generate_trajectory()
        self.waypoint = self.start_joints

    def _generate_trajectory(self):
        for i in range(self.steps + 1):
            yield self.start_joints + self.step * i
        # 确保最后精确到达目标关节值
        yield self.end_joints

    def get_next_waypoint(self, qpos):
        # 检查当前的关节值是否已经接近目标路径点。若是,则更新下一个目标路径点;若否,则保持当前目标路径点不变。
        if np.allclose(qpos, self.waypoint, atol=0.02):
            try:
                self.waypoint = next(self.trajectory)
                return self.waypoint
            except StopIteration:
                pass
        return self.waypoint


def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)
    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf",
                                               active_links_mask=[False, False] + [True] * 6 + [False])

    start_joints = np.array([-1.57, -1.34, 2.65, -1.3, 1.55, 0])  # 对应机械臂初始位姿[-0.14, 0.3, 0.1, 3.14, 0, 1.57]
    data.qpos[:6] = start_joints # 确保渲染一开始机械臂便处于起始位置,而非MJCF中的默认位置

    # 设置目标点
    ee_pos = [-0.13, 0.6, 0.1]
    ee_euler = [3.14, 0, 1.57]
    ref_pos = [0, 0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0]
    ee_orientation = tf.euler.euler2mat(*ee_euler)

    joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos)
    end_joints = joint_angles[2:-1]

    joint_trajectory = JointSpaceTrajectory(start_joints, end_joints, steps=100)

    with mujoco.viewer.launch_passive(model, data) as viewer:
        viewer_init(viewer)
        while viewer.is_running():
            waypoint = joint_trajectory.get_next_waypoint(data.qpos[:6])
            data.ctrl[:6] = waypoint

            mujoco.mj_step(model, data)
            viewer.sync()


if __name__ == "__main__":
    main()

viewer_init()函数是用来初始化MuJoCo渲染器的初始视角的,方便用户观察仿真动画。

值得注意的是,代码在一开始通过直接设置机械臂6个关节值data.qpos[:6],来实现初始化机械臂的位姿。qpos指储存在data中的所有<joint>,这也是按照MJCF文件中的定义顺序来一一存储的。由于在我们的MJCF中,最先读取的MJCF就是ur5e.xml,所以最先定义的<joint>,即前6位qpos,就是对应着ur5e的关节值。另外,<joint>不单指机械臂的关节值。比如一个进行自由落体的物体,描述它的位姿需要3轴位置与3轴位姿信息,那么这里也应该给这个物体添加上拥有6轴自由度的<joint>标签,这在下一章添加物体模型的时候会提到。

还有一点,设置data.qpos[:6]data.ctrl[:6]的目标关节值来调节机械臂位姿,这两种控制方式是不一样的。前者是直接修改机械臂的关节值,后者是修改机械臂的关节电机的设定值。

四、添加触觉传感器与被推动物体的模型

论文中使用的触觉传感器为TacTip,外观如下图。TacTip就像人的手指尖,由半球形橡胶表面与内置摄像头组成,当传感器接触物体时,橡胶表面发生形变,同时带动内表面上的标记点发生移动,内置摄像头记录下标记点的位移,对图像进行分析后便可以判断出接触的位置、方向等信息。论文中是通过CNN去学习这种标记点位移图像到力的映射关系的。

下面我们通过MuJoCo的<sensor>标签中提供的"force"传感器去实现一个TacTip传感器的大致建模。这个"force"传感器可以求出指定物体所受到的合外力矢量。

首先修改ur5e.xml文件,添加一个名叫"force_sensor"的body用来设置传感器的物理外形,让其长得像TacTip传感器。

                                ...
                                <body name="wrist_3_link" pos="0 0 0.1">
                                    <inertial mass="0.1889" pos="0 0.0771683 0" quat="1 0 0 1"
                                              diaginertia="0.000132134 9.90863e-05 9.90863e-05"/>
                                    <joint name="wrist_3_joint" class="size1"/>
                                    <geom material="linkgray" mesh="wrist3" class="visual"/>
                                    <geom class="eef_collision" pos="0 0.08 0" quat="1 1 0 0" size="0.04 0.02"/>
                                    <site name="attachment_site" pos="0 0.1 0" quat="-1 1 0 0"/>

                                    <!--你需要添加的-->
                                    <body name="force_sensor" pos="0 0.1 0" euler="3.14 0 3.14">
                                        <geom name='force_sensor_geom' type="sphere" mass="0.0" size="0.04"
                                              rgba="0 1 0 0.3"/>
                                        <site name="force_sensor_site" pos="0 0 0" quat="1 0 0 0"/>
                                    </body>
                                </body>
                                ...

接下来,在与<worldbody>同级的标签下添加一个<sensor>标签,并将"force"传感器“固定”在"force_sensor_site"这个点上。那么这个"force"传感器便会求出所属的<body>,即"force_sensor"所受到的合外力,包括重力与接触力。因此这里会将mass设置为0,避免重力的干扰。

    <worldbody>
    ...
    </worldbody>

    <!--你需要添加的-->
    <sensor>
        <force site="force_sensor_site"/>
    </sensor>

此时运行main.py,可以看到我们建模出来的TacTip传感器:

至此我们已经添加了一个半球形的"force"传感器了,下面在scene.xml文件中添加被推动物体的模型,在<worldbody>标签里面添加一个拥有6自由度关节的正方体:

        <body name="red_box" pos="-0.1 0.5 0.1" euler="0 0 45">
            <joint name="red_box_joint" type="free"/>
            <geom name="red_box_geom" type="box" size="0.1 0.1 0.1" rgba="1 0 0 1" mass="1.0"/>
        </body>

同时要将ur5e.xml中的<keyframe>部分注释掉:

    <!--    The number of keyframe qpos must match the total number of joints specified in the XML!-->
    <!--    <keyframe>-->
    <!--        <key name="home" qpos="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0"-->
    <!--             ctrl="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0"/>-->
    <!--    </keyframe>-->

这是因为由于"red_box"引入了一个新的<joint>,而原先的<keyframe>只给出了UR5e的六个关节值,若不进行修改则会报错。

五、物体推动实验

将main.py修改为如下代码并运行:

import mujoco.viewer
import ikpy.chain
import transforms3d as tf
import numpy as np
from collections import deque
import matplotlib.pyplot as plt


def viewer_init(viewer):
    """渲染器的摄像头视角初始化"""
    viewer.cam.type = mujoco.mjtCamera.mjCAMERA_FREE
    viewer.cam.lookat[:] = [0, 0.5, 0.5]
    viewer.cam.distance = 2.5
    viewer.cam.azimuth = 180
    viewer.cam.elevation = -30


class ForcePlotter:
    """实时可视化接触力"""

    def __init__(self, update_interval=20):
        plt.ion()
        self.fig = plt.figure()
        self.ax = self.fig.add_subplot(111, projection='3d')
        self.update_interval = update_interval  # 更新间隔帧数
        self.frame_count = 0  # 帧计数器

    def plot_force_vector(self, force_vector):
        self.frame_count += 1
        if self.frame_count % self.update_interval != 0:
            return  # 跳过本次渲染

        self.ax.clear()

        origin = np.array([0, 0, 0])
        force_magnitude = np.linalg.norm(force_vector)
        force_direction = force_vector / force_magnitude if force_magnitude > 1e-6 else np.zeros(3)

        # 主箭头
        arrow_tip = force_direction * 1.5
        self.ax.quiver(*origin, *arrow_tip, color='r', arrow_length_ratio=0)

        # 蓝色箭头
        self.ax.quiver(*arrow_tip, *(0.5 * force_direction), color='b', arrow_length_ratio=0.5)

        # XY平面投影
        self.ax.plot([0, arrow_tip[0]], [0, arrow_tip[1]], [-2, -2], 'g--')

        # XZ平面投影
        self.ax.plot([0, 0], [2, 2], [0, arrow_tip[2]], 'm--')

        # 力大小指示条
        scaled_force = min(max(force_magnitude / 50, 0), 2)
        self.ax.plot([-2, -2], [2, 2], [0, scaled_force], 'c-')
        self.ax.text(-2, 2, scaled_force, f'Force: {force_magnitude:.1f}', color='c')

        # 坐标系设置
        self.ax.scatter(0, 0, 0, color='k', s=10)
        self.ax.set_xlim([-2, 2])
        self.ax.set_ylim([-2, 2])
        self.ax.set_zlim([-2, 2])
        self.ax.set_title(f'Force Direction')

        plt.draw()
        plt.pause(0.001)
        self.frame_count = 0  # 重置计数器


class ForceSensor:
    def __init__(self, model, data, window_size=100):
        self.model = model
        self.data = data
        self.window_size = window_size
        self.force_history = deque(maxlen=window_size)

    def filter(self):
        """获取并滑动平均滤波力传感器数据(传感器坐标系下)"""
        # 获取MjData中的传感器数据
        force_local_raw = self.data.sensordata[:3].copy() * -1

        # 添加新数据到滑动窗口
        self.force_history.append(force_local_raw)

        # 计算滑动平均
        filtered_force = np.mean(self.force_history, axis=0)

        return filtered_force


class JointSpaceTrajectory:
    """关节空间坐标系下的线性插值轨迹"""

    def __init__(self, start_joints, end_joints, steps):
        self.start_joints = np.array(start_joints)
        self.end_joints = np.array(end_joints)
        self.steps = steps
        self.step = (self.end_joints - self.start_joints) / self.steps
        self.trajectory = self._generate_trajectory()
        self.waypoint = self.start_joints

    def _generate_trajectory(self):
        for i in range(self.steps + 1):
            yield self.start_joints + self.step * i
        # 确保最后精确到达目标关节值
        yield self.end_joints

    def get_next_waypoint(self, qpos):
        # 检查当前的关节值是否已经接近目标路径点。若是,则更新下一个目标路径点;若否,则保持当前目标路径点不变。
        if np.allclose(qpos, self.waypoint, atol=0.02):
            try:
                self.waypoint = next(self.trajectory)
                return self.waypoint
            except StopIteration:
                pass
        return self.waypoint


def main():
    model = mujoco.MjModel.from_xml_path('model/universal_robots_ur5e/scene.xml')
    data = mujoco.MjData(model)
    my_chain = ikpy.chain.Chain.from_urdf_file("model/ur5e.urdf",
                                               active_links_mask=[False, False] + [True] * 6 + [False])

    start_joints = np.array([-1.57, -1.34, 2.65, -1.3, 1.55, 0])  # 对应机械臂初始位姿[-0.13, 0.3, 0.1, 3.14, 0, 1.57]
    data.qpos[:6] = start_joints  # 确保渲染一开始机械臂便处于起始位置,而非MJCF中的默认位置

    # 设置目标点
    ee_pos = [-0.13, 0.6, 0.1]
    ee_euler = [3.14, 0, 1.57]
    ref_pos = [0, 0, -1.57, -1.34, 2.65, -1.3, 1.55, 0, 0]
    ee_orientation = tf.euler.euler2mat(*ee_euler)

    joint_angles = my_chain.inverse_kinematics(ee_pos, ee_orientation, "all", initial_position=ref_pos)
    end_joints = joint_angles[2:-1]

    joint_trajectory = JointSpaceTrajectory(start_joints, end_joints, steps=100)

    force_sensor = ForceSensor(model, data)
    force_plotter = ForcePlotter()

    with mujoco.viewer.launch_passive(model, data) as viewer:
        viewer_init(viewer)
        while viewer.is_running():
            waypoint = joint_trajectory.get_next_waypoint(data.qpos[:6])
            data.ctrl[:6] = waypoint

            filtered_force = force_sensor.filter()
            force_plotter.plot_force_vector(filtered_force)

            mujoco.mj_step(model, data)
            viewer.sync()


if __name__ == "__main__":
    main()

运行后可以看到UR5e在推动正方体,同时左侧会实时显示出“TacTip”传感器受到的接触力的大小与方向(传感器坐标系下)。

回到代码中,相较之前的多出了两个class,分别是ForceSensorForcePlotter。前者是获取MuJoCo仿真中的"force"传感器的原始数据,即XYZ力矢量,并对其进行滑动平均滤波后输出滤波后的力矢量;后者是通过matplotlab库将传入的XYZ力矢量实时可视化出来。

那么通过得到的接触力数据,我们就可以按照论文的思路设计出PID控制器,来实现“将正方体推动到目标位置”这一目的。根据论文,一共需要2个PID控制器:

第1个PID控制器 :用来确保机械臂末端执行器始终垂直推动正方体。如下图,输入误差为θ,通过调节末端执行器的偏航角Yaw,使得θ误差趋向于0。

第2个PID控制器:将正方体的朝向逐渐推向目标位置。下图截取自论文,即调节末端执行器坐标系的Y轴朝向始终指向目标位置。

这部分内容读者可以自行尝试去复现。

关于本仿真实验的PS:

1、本实验没有设置正方体与地面的摩擦系数,采用的是默认数值1。

2、在实际当中TacTip传感器是带有一定弹性的,而在本实验中,创建的"force_sensor"的几何体是刚性的,这里与实际存在偏差。编者不清楚如何在MuJoCo中设置“弹性”这个属性。

3、编者并没有在真机上部署实验,但是通过其他的仿真实验验证了MuJoCo的"force"传感器的数据在仿真环境下是准确的。具体实验过程是:还是通过上面的UR5e去推动物体,根据物体的加速度与接触力大小,结合去线性拟合出物体的质量,得到的估计质量与在xml中设定的质量相同。

本文章已经生成可运行项目
10-09
### MuJoCo 的安装、配置与使用方法 MuJoCo(Multi-Joint dynamics with Contact)是一个高性能的物理仿真引擎,广泛应用于机器人控制、强化学习和生物力学研究领域。其高精度的动力学模拟能力和对 GPU 加速的支持使其成为许多先进项目的首选工具。 #### 安装准备 在开始安装之前,推荐使用 Linux 系统(如 Ubuntu 20.04),因为在 Windows 上可能存在兼容性和性能瓶颈,尤其是在涉及 GPU 调用时[^5]。为了隔离依赖并防止污染全局 Python 环境,应优先使用虚拟环境: ```bash virtualenv -p python3 venv_mujoco && source venv_mujoco/bin/activate ``` 或者使用 Conda 创建独立环境: ```bash conda create -n mujoco_env python=3.10 conda activate mujoco_env ``` 这有助于管理不同项目之间的依赖冲突[^4]。 #### 下载与安装 MuJoCo 自 DeepMind 将 MuJoCo 开源以来,该引擎已可免费用于学术和个人用途。当前最新稳定版本为 MuJoCo 2.1 或更高版本(如 mjx 和 MuJoCo 2.3.x)。对于传统 `mujoco-py` 支持的场景,仍可能需要特定结构布局。 下载适用于系统的二进制包后,将其解压缩至用户主目录下的 `.mujoco` 文件夹中: ```bash mkdir ~/.mujoco tar -xf mujoco-2.1-linux-x86_64.tar.gz -C ~/.mujoco/ mv ~/.mujoco/mujoco-2.1-linux-x86_64 ~/.mujoco/mjpro210 ``` 注意:某些旧版库(如早期 `mujoco-py`)期望路径为 `~/.mujoco/mjpro210` 或 `mjproj150`[^1]。如果使用较新接口 MJX,则目标可能是 `mjx` 架构分支[^3]。 #### 许可证设置 尽管 MuJoCo 已开源,部分功能或历史版本仍然要求有效的许可证文件。将获得的 `mjkey.txt` 复制到指定位置: ```bash cp /path/to/mjkey.txt ~/.mujoco/mjkey.txt ``` 此步骤是激活高级渲染和商业用途的关键环节[^1]。 #### 安装绑定库(Python 接口) 最常用的 Python 绑定是 `mujoco-py` 和新的 `mujoco`(即原生 PyPI 包)。针对现代工作流,建议采用官方维护的新 `mujoco` 包: ```bash pip install mujoco ``` 而对于需要向后兼容的老项目,可通过如下方式安装带扩展支持的 gym 变体: ```bash pip install 'gymnasium[mujoco]' ``` 或从本地克隆仓库安装增强版本: ```bash git clone https://github.com/openai/gym.git cd gym pip install -e ".[mujoco]" ``` 这一过程会自动解析所有底层依赖项,包括 OpenGL 渲染器所需的共享库[^1][^4]。 #### 验证安装 编写简单脚本验证是否成功加载模型并执行前向动力学计算: ```python import mujoco import glfw model = mujoco.MjModel.from_xml_path('path_to_your_model.xml') data = mujoco.MjData(model) # 执行单步模拟 mujoco.mj_step(model, data) print(f'Simulation time: {data.time}') ``` 若无异常抛出且能正常推进时间步,则表明核心组件运作良好。 #### 使用场景与生态集成 MuJoCo 广泛被集成于主流 RL 框架之中。例如 Dopamine 框架利用 Gin 配置系统实现了对其环境的标准接入流程,便于开展大规模实验对比分析[^2]。此外,新兴项目 **MuJoCo Playground** 基于 MJX 实现了全 GPU 加速的批量模拟能力,特别适合四足行走、灵巧手操控等复杂任务的研究,并支持 Madrona 提供的即时视觉观测生成机制[^3]。 这类现代化封装显著提升了采样效率,使得端到端训练更具可行性。 #### 故障排查提示 常见问题包括缺失 GLIBCXX 版本、无法找到 libcudart.so 或 X-server 错误。解决方案通常包括更新 GCC 编译器套件、正确配置 LD_LIBRARY_PATH,以及启用无头模式渲染(headless rendering)以绕过图形界面限制。 ---
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值