用Python和PyBullet开启机器人仿真之旅
机器人是能够执行任务并复制或替代人类动作的机器。机器人学是专注于机器人设计和构建的科学领域。它是一个多学科的组合,包括用于传感器和执行器的电气工程、用于物理结构设计的机械工程,以及用于人工智能软件和算法的计算机科学[citation:1]。
目前,ROS(机器人操作系统)是处理机器人所有不同部分(传感器、电机、控制器、摄像头……)的主要框架,其中所有组件以模块化方式交换数据。ROS框架适用于真实的机器人原型,可以与Python和C++一起使用。由于其普及性,有许多建立在ROS之上的库,例如最先进的3D模拟器Gazebo[citation:1]。
由于Gazebo相当复杂,我们仍然可以在ROS生态系统之外学习机器人学的基础知识并构建用户友好的模拟。主要的Python库有:
- PyBullet(初学者) — 非常适合尝试URDF(统一机器人描述格式),这是一种描述机器人身体、零件和几何形状的XML模式。
- Webots(中级) — 物理更准确,因此可以构建更复杂的模拟。
- MuJoCo(高级) — 现实世界模拟器,用于研究级实验。一个研究机构的RoboGYM库就是建立在MuJoCo之上的[citation:4]。
由于这是一个面向初学者的教程,将展示如何使用PyBullet构建简单的3D模拟。将提供一些有用的Python代码,这些代码可以轻松应用于其他类似情况(只需复制、粘贴、运行),并通过注释逐步讲解每一行代码,以便你能复现这个例子。
准备工作
PyBullet是一个适用于游戏、视觉效果、机器人学和强化学习的最易用的物理模拟器[citation:3]。你可以使用以下命令之一轻松安装它(如果pip失败,conda应该绝对有效):
pip install pybullet
conda install -c conda-forge pybullet
PyBullet可以在两种主要模式下运行:
p.GUI→ 打开一个窗口并实时显示模拟。p.DIRECT→ 无图形模式,用于脚本。
import pybullet as p
p.connect(p.GUI) # 或者 p.connect(p.DIRECT)
由于这是一个物理模拟器,第一件事就是设置重力:
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
设置模拟的全局重力矢量。Z轴上的“-9.8”意味着向下9.8 m/s²的加速度,就像在地球上一样。没有这个设置,你的机器人和平面将在零重力下漂浮,就像在太空中一样。
URDF文件
如果机器人是人体,那么URDF文件就是定义其物理结构的骨骼。它是用XML编写的,基本上是机器的蓝图,定义了机器人的外观和运动方式[citation:3]。
我将展示如何创建一个简单的立方体,这是相当于print("hello world")的3D版本。
urdf_string = """"
<robot name="cube_urdf">
<link name="cube_link">
<visual>
<geometry>
<box size="0.5 0.5 0.5"/> <!-- 50 cm 立方体 -->
</geometry>
<material name="blue">
<color rgba="0 0 1 1"/>
</material>
</visual>
<collision>
<geometry>
<box size="0.5 0.5 0.5"/>
</geometry>
</collision>
<inertial>
<mass value="1.0"/>
<inertia ixx="0.0001" iyy="0.0001" izz="0.0001"
ixy="0.0" ixz="0.0" iyz="0.0"/>
</inertial>
</link>
</robot>
"""
你可以将XLM代码保存为URDF文件,或将其作为字符串使用。
import pybullet as p
import tempfile
## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
## 在内存中创建一个临时的URDF文件
with tempfile.NamedTemporaryFile(suffix=".urdf", mode="w", delete=False) as f:
f.write(urdf_string)
urdf_file = f.name
## 加载URDF
p.loadURDF(fileName=urdf_file, basePosition=[0,0,1])
## 渲染模拟
for _ in range(240):
p.stepSimulation()
请注意,循环运行了240步。为什么是240?这是视频游戏开发中常用的固定时间步长,旨在提供流畅和响应迅速的体验,而不会过度消耗CPU。60 FPS(每秒帧数)意味着每1/60秒显示一帧,而1/240秒的物理步长则提供每渲染帧4次物理更新。
在之前的代码中,我使用p.stepSimulation()渲染了立方体。这意味着模拟不是实时的,由你控制下一个物理步长何时发生。或者,你可以使用time.sleep将其绑定到现实世界的时钟。
import time
## 渲染模拟
for _ in range(240):
p.stepSimulation() # 添加一个物理步长(CPU速度 = 0.1秒)
time.sleep(1/240) # 减慢到实时(240步 × 1/240秒睡眠 = 1秒)
展望未来,机器人的XML代码会变得更加复杂。幸运的是,PyBullet自带了一组预设的URDF文件。你可以轻松加载默认的立方体,而无需为其创建XML。
import pybullet_data
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
p.loadURDF(fileName="cube.urdf", basePosition=[0,0,1])
URDF文件的核心定义了两样东西:连杆和关节。连杆是机器人的固体部分(就像我们的骨骼),而关节是两个刚性连杆之间的连接(就像我们的肌肉)。没有关节,你的机器人只是一个雕像;有了关节,它就变成了具有运动和目的的机器[citation:6]。
简而言之,每个机器人都是一个由关节连接的连杆树。每个关节可以是固定的(不能移动)或旋转的(“旋转关节”)和滑动的(“棱柱关节”)。因此,你需要了解你正在使用的机器人。
让我们以《星球大战》中著名的机器人R2D2为例。
关节
这次,我们还必须导入一个平面,以便为机器人创建地面。如果没有平面,物体将没有表面可碰撞,它们只会无限期地下落。
## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## 加载URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## 渲染模拟
for _ in range(240):
p.stepSimulation()
time.sleep(1/240)
p.disconnect()
在使用机器人之前,我们必须了解其组件。PyBullet已经标准化了信息结构,因此你导入的每个机器人始终由相同的17个通用属性定义。由于我只想运行脚本,我将连接物理服务器而不使用图形界面(p.DIRECT)。分析关节的主要函数是p.getJointInfo()。
p.connect(p.DIRECT)
dic_info = {
0:"joint Index", # 从0开始
1:"joint Name",
2:"joint Type", # 0=旋转,1=棱柱(滑动),4=固定
3:"state vectorIndex",
4:"velocity vectorIndex",
5:"flags", # 总是0
6:"joint Damping",
7:"joint Friction", # 系数
8:"joint lowerLimit", # 最小角度
9:"joint upperLimit", # 最大角度
10:"joint maxForce", # 允许的最大力
11:"joint maxVelocity", # 最大速度
12:"link Name", # 连接到此关节的子连杆
13:"joint Axis",
14:"parent FramePos", # 位置
15:"parent FrameOrn", # 方向
16:"parent Index" # −1 = 基座
}
for i in range(p.getNumJoints(bodyUniqueId=robo)):
print(f"--- JOINT {i} ---")
joint_info = p.getJointInfo(bodyUniqueId=robo, jointIndex=i)
print({dic_info[k]:joint_info[k] for k in dic_info.keys()})
每个关节,无论是轮子、肘部还是夹爪,都必须暴露这些相同的特征。上面的代码显示R2D2有15个关节。让我们分析第一个,名为“base to right-leg”:
- 关节类型是4,意味着它不能移动。
- 父连杆是-1,意味着它连接到基座,即机器人的根部分(就像我们的脊柱)。对于R2D2,基座是主要的圆柱体,那个大的蓝白色桶。
- 连杆名称是“right leg”,所以我们理解这个关节将机器人的基座与右腿连接起来,但它不是电机驱动的。这被关节轴、关节阻尼和关节摩擦均为零所证实。
- 父框架位置和方向定义了右腿连接到基座的位置。
连杆
另一方面,为了分析连杆(物理身体段),必须循环遍历关节以提取连杆名称。然后,你可以使用两个主要函数:p.getDynamicsInfo()来了解连杆的物理属性,以及p.getLinkState()来了解其空间状态。
p.connect(p.DIRECT)
for i in range(p.getNumJoints(robo)):
link_name = p.getJointInfo(robo, i)[12].decode('utf-8') # 字段12="link Name"
dyn = p.getDynamicsInfo(robo, i)
pos, orn, *_ = p.getLinkState(robo, i)
dic_info = {"Mass":dyn[0], "Friction":dyn[1], "Position":pos, "Orientation":orn}
print(f"--- LINK {i}: {link_name} ---")
print(dic_info)
让我们分析第一个,“right-leg”。10公斤的质量影响重力和惯性,而摩擦力影响其接触地面时的滑动程度。方向(0.707 = sin(45°)/cos(45°))和位置表明右腿连杆是一个固体部件,略微向前(5厘米),相对于基座倾斜90°。
运动
让我们看一个实际可以移动的关节。
例如,关节2是右前轮。它是一个类型=0的关节,所以可以旋转。这个关节将右腿(连杆索引1)连接到右前轮:base_link → right_leg → right_front_wheel。关节轴是(0,0,1),所以我们知道轮子绕Z轴旋转。限制(lower=0, upper=-1)表明轮子可以无限旋转,这对于转子来说是正常的。
我们可以轻松移动这个关节。控制机器人上执行器的主要命令是p.setJointMotorControl2(),它允许控制关节的力、速度和位置。在这种情况下,我想让轮子旋转,所以将施加力以逐步将速度从零增加到目标速度,然后通过平衡施加的力和阻力(即摩擦、阻尼、重力)来维持它。
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=2,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=10, force=5)
现在,如果我们对所有4个轮子施加相同的力,我们将让我们的小机器人向前移动。根据之前的分析,我们知道轮子的关节是2号和3号(右侧),以及6号和7号(左侧)。
考虑到这一点,我将首先让R2D2通过只旋转一侧来转弯,然后同时对所有轮子施加力使其向前移动。
import pybullet as p
import pybullet_data
import time
## 设置
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## 加载URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## 稳定下来
for _ in range(240):
p.stepSimulation()
right_wheels = [2,3]
left_wheels = [6,7]
### 先转弯
for _ in range(240):
for j in right_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=50)
for j in left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=100, force=50)
p.stepSimulation()
time.sleep(1/240)
### 然后向前移动
for _ in range(500):
for j in right_wheels + left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=10)
p.stepSimulation()
time.sleep(1/240)
p.disconnect()
请注意,每个机器人都有不同的质量(重量),因此根据模拟物理(例如重力)的不同,运动可能会有所不同。你可以尝试不同的力和速度,直到找到想要的结果。
结论
本文是一个教程,旨在介绍PyBullet以及如何为机器人学创建非常基础的3D模拟。我们学习了如何导入URDF对象以及如何分析关节和连杆。我们还尝试了著名的机器人R2D2。未来将有更多关于更先进机器人的新教程。
本文的完整代码可在GitHub上找到。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码


浙公网安备 33010602011771号