使用Pybullet实现UR5机器人RRT/RRT*避障仿真的Python实践
主要是为了记录一下Pybullet的使用过程,顺便实现一下RRT/RRT*的避障仿真。
本文希望能够将机器人的规划模块尽可能独立,即尽可能与环境和机器人结构解耦。因此将任务主要分为机器人类、环境类、规划类、可视类四个部分来编写。
1. 机器人类
机器人类中包含规划和可视化中可能用到的内容,如机器人的基座位姿、URDF文件路径、DOF、末端执行器关节序号、关节限位等。
def __init__(self):
self.type = "manipulator"
self.startPos = [0, 0, 0]
self.startOrientation = p.getQuaternionFromEuler([0, 0, 0])
self.robotID = p.loadURDF(
"ur5/ur5.urdf", self.startPos, self.startOrientation
)
self.DOF = 6
self.end_effector_index = 8
# limit可以用getJointInfo()里的信息
self.limit_lower = [-2, -2, -2, -2, -2, -2]
self.limit_upper = [2, 2, 2, 2, 2, 2]
为了方便后续使用,还实现了正运动学的功能。首先resetJointState根据索引和关节值改变机器人的姿态,利用getLinkState获取了机器人末端执行器的笛卡尔坐标。
def kinematics(self, configuration):
for i in range(self.DOF):
p.resetJointState(self.robotID, i, configuration[i])
return p.getLinkState(self.robotID, self.end_effector_index)[0]
在这里还通过setJointMotorControlArray函数实现了电机位置控制的功能,本意是希望机器人规划完路径后,以更符合真实物理规律的形式(电机控制)运行一遍。但对于不同的机器人,电机控制的PID以及其他的设置都不太一样,都需要额外设置。例如UR5中就设计P为1,如下所示。
def run_path(self, configurations):
for i in range(len(configurations)):
# 获取当前时间步的关节配置
target_positions = configurations[i]
# 设置关节电机控制 Pandas的有些问题
p.setJointMotorControlArray(
positionGains=[1] * self.DOF,
bodyUniqueId=self.robotID,
jointIndices=list(range(self.DOF)),
controlMode=p.POSITION_CONTROL,
targetPositions=target_positions,
)
# 进行一步模拟
p.stepSimulation()
time.sleep(0.5)
2. 环境类
在环境类中主要是定义了环境中的障碍物。例如本文使用了createCollisionShape构造了一个立方体碰撞形状,并使用createMultiBody创建了这个碰撞形状的刚体。
# 创建一个立方体的碰撞形状
collision_shape_id = p.createCollisionShape(
shapeType=p.GEOM_BOX, halfExtents=[0.05, 0.05, 0.1]
)
# 创建一个具有该碰撞形状的刚体
base_position = [0.2, 0.2, 0.8]
base_orientation = p.getQuaternionFromEuler([0, 0, 0])
body_id = p.createMultiBody(
baseMass=1,
baseCollisionShapeIndex=collision_shape_id,
basePosition=base_position,
baseOrientation=base_orientation,
)
self.obstacleID = [collision_shape_id]
3. 规划类
RRT按照下列的思路进行:
-
初始化机器人型号、初始配置、目标配置、关节限位
-
随机采样
-
树内检索最近点
-
最近点限位
-
取间隔点,逐点避障检测
-
添加点至RRT
-
找到路径回溯
为方便后续进行向量化操作,将rrt中每个节点的值、父节点索引分别存储在两个rrt树中,这样在后续寻找最近点时,可以对存有值的rrt树整体进行操作,而不用手动遍历。RRT规划的主函数如下:
def planning(self, plot=True):
for i in range(self.iter_max):
node_rand = self.generate_random_node()
node_near, index_near = self.nearest_node(node_rand)
index_near = index_near
node_new = self.new_node(node_near, node_rand)
if not self.is_collision(node_near, node_new):
self.rrt_append(node_new,index_near)
dist = np.linalg.norm(self.goal - node_new, 2)
# self.plot_points_and_lines()
if dist <= self.step_delta and not self.is_collision(
node_new, self.goal
):
if plot:
self.plot_points_and_lines()
# len(self.rrt_value) - 2为node_new的索引
self.rrt_append(self.goal,len(self.rrt_value) - 2)
# len(self.rrt_value) - 1为goal的索引
return self.extract_path(len(self.rrt_value) - 1)
RRT*加入了重布线的操作,按照下列的思路进行:
-
初始化机器人型号、初始配置、目标配置、关节限位
-
随机采样
-
找潜在父节点
-
计算代价
-
碰撞检测
-
重新布线
-
取间隔点,逐点避障检测
-
添加点至RRT
-
找到路径回溯
RRT*规划的主函数如下:
def planning(self, plot=True):
for i in range(self.iter_max):
node_rand = self.generate_random_node()
node_near, index_near = self.nearest_node(node_rand)
index_near = index_near
node_new = self.new_node(node_near, node_rand)
if not self.is_collision(node_near, node_new):
neighbor_index = self.find_neighbor(node_new)
self.rrt_append(node_new, index_near)
if neighbor_index is not None:
parent_index = self.choose_parent(node_new, neighbor_index)
if parent_index:
self.rewire(node_new, parent_index)
dist = np.linalg.norm(self.goal - node_new, 2)
if dist <= self.step_delta and not self.is_collision(
node_new, self.goal
):
if plot:
self.plot_points_and_lines()
# len(self.rrt_value) - 2为node_new的索引
self.rrt_append(self.goal,len(self.rrt_value) - 2)
# len(self.rrt_value) - 1为goal的索引
return self.extract_path(len(self.rrt_value) - 1)
4. 可视化类
可视化类中的函数使用staticmethod定义,利用addUserDebugPoints和addUserDebugLine在Pybullet中绘制点和线,具体如下:
import numpy as np
import pybullet as p
class Visualize:
@staticmethod
def plot_points(position, color, size):
if np.array(position).ndim == 1:
position = [position]
if np.array(color).ndim == 1:
color = [color]
if len(color) == 1:
num_points = len(position)
color = color * num_points
p.addUserDebugPoints(
pointPositions=position,
pointColorsRGB=color,
pointSize=size,
)
@staticmethod
def plot_lines(position1, position2, color, size):
if np.array(position1).ndim == 1:
position1 = [position1]
if np.array(position2).ndim == 1:
position2 = [position2]
assert len(position1) == len(position2)
# 循环绘制连线
for start, end in zip(position1, position2):
p.addUserDebugLine(
lineFromXYZ=start, lineToXYZ=end, lineColorRGB=color, lineWidth=size
)
@staticmethod
def plot_path(path, point_color, line_color, point_size, line_size):
Visualize.plot_points(path, point_color, point_size)
if np.array(path).ndim == 1 or len(path) == 1:
return
position1 = path[:-1]
position2 = path[1:]
Visualize.plot_lines(position1, position2, line_color, line_size)
5. 主函数
主函数调用rrt/rrt*进行仿真,代码如下:
import pybullet as p
import numpy as np
from env import Env
from rrtManipulator import RRT
from rrtstarManipulator import RRTstar
from visualize import Visualize
np.random.seed(123)
env = Env()
robot = env.robot
ur5_start = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
ur5_end = [0, 0, 0, 0, 0, 0]
rrt = RRT(
start=ur5_start,
goal=ur5_end,
step_delta=0.05,
iter_max=10000,
sample_rate=0.7,
robot=robot,
)
rrtstar = RRTstar(
start=ur5_start,
goal=ur5_end,
step_delta=0.05,
iter_max=10000,
sample_rate=0.7,
search_radius=0.5,
robot=robot,
)
configuration, path = rrtstar.planning(plot=False)
Visualize.plot_path(path, [0.1, 0.1, 0.1], [0.8, 0, 0.8], 4, 2)
robot.run_path(configuration)
while p.isConnected():
p.stepSimulation()
使用RRT规划结果如下:
使用RRT*规划结果如下:
可以看到,RRT*因为重布线轨迹比RRT更短更平滑些。
将planning函数中的plot设为True,可以将rrt中存储的所有的点都可视化出来,如下图所示,红点为所有的点,绿线为点之间的连接边,黑点为最终路径的点,紫线为最终路径的连接边。点和线的颜色和大小都能够更改。
如果需要使用其他的机械臂进行仿真,可以参考上传代码的PandasRobot()写法进行修改,但可能需要调整positionGains等参数以实现电机控制显示最终路径的功能。
作者:世界尽头的青豆