
9.3 实战案例:基于Gazebo仿真的路径规划系统
本项目是一个基于Python实现的路径规划系统,利用RRT(Rapidly-Exploring Random Tree)和RRT*(Rapidly-Exploring Random Tree Star)等算法在给定地图中找到起点和终点之间的最优路径。通过读取SDF文件获取地图信息,并利用Matplotlib进行可视化展示。项目提供了RRT、RRT和RRT-FN算法的实现,用户可以根据需要选择不同的路径规划策略,并在可视化界面中观察算法生成的路径。
实例9-5 :使用RRT* 算法寻找路径 (codes/9/RRT-Algorithm )
9.3.1 项目介绍
路径规划在自动驾驶和机器人领域具有关键作用,它能够帮助车辆和机器人在复杂的环境中找到最佳的行驶或移动路径,从而实现自主导航和避障功能,提高安全性、效率和舒适性。本项目旨在探索并实现基于路径规划的自动导航系统,适用于自动驾驶和机器人导航领域。
在自动驾驶汽车方面,路径规划算法可以帮助车辆规划从起点到目的地的最佳行驶路径,并在行驶过程中动态调整路线,考虑实时的交通情况、道路条件和周围环境的变化。通过合理规划路径,车辆可以避免交通拥堵、减少能源消耗,并且确保行驶安全。
在机器人导航方面,路径规划算法同样具有重要意义。各种类型的机器人,如无人机、无人车、服务机器人等,需要能够在各种复杂的室内和室外环境中自主导航,完成各种任务。路径规划算法可以帮助机器人规划安全、高效的路径,避开障碍物和危险区域,以完成任务并达到预期目标。
本项目旨在开发和实现一套智能的自动导航系统,为自动驾驶汽车和机器人提供高效、安全的导航解决方案,促进智能交通系统和智能机器人的发展,推动人工智能技术在自动驾驶和机器人领域的应用和创新。
本项目包含的技术栈如下所示。
- 路径规划算法:采用了多种路径规划算法,如基本的RRT(Rapidly-exploring Random Tree)、RRT*(Rapidly-exploring Random Tree Star)以及RRT*-FN(Rapidly-exploring Random Tree Star with Fixed Nodes)等。这些算法能够在复杂的环境中快速生成可行路径,并通过不同的优化策略提高路径的质量和效率。
- Gazebo仿真环境:Gazebo是一个功能强大的机器人仿真平台,可以模拟各种机器人和环境,为自动驾驶和机器人导航系统提供实验和测试环境。本项目可能会使用Gazebo来模拟汽车、机器人和不同类型的环境场景,验证路径规划算法在实际场景中的性能和可靠性。
- Matplotlib可视化库:Matplotlib是一个Python绘图库,用于绘制图表和可视化数据,可以用于可视化路径规划结果、仿真场景以及实验数据分析。
总之,本项目的技术栈涵盖了路径规划算法、Python编程语言、Gazebo仿真环境、可视化库和XML解析等关键技术,旨在开发实现智能的自动导航系统,并在仿真环境中验证和评估其性能和可靠性。
9.3.2 RRT*-FN算法介绍
RRT*-FN算法是改进的Rapidly-exploring Random Tree (RRT*)路径规划算法的一种变体,它结合了RRT*算法和弗洛伊德-内曼算法(Floyd-Newton algorithm)。该算法旨在解决在含有大量障碍物的环境中的路径规划问题,尤其是在遇到局部最小值或者路径围绕障碍物的情况下。RRT*-FN算法的主要特点如下所示。
- 基于树结构的探索:RRT*-FN算法通过在自由空间中构建一棵树来表示路径搜索过程。每个节点代表了一种可能的机器人状态,并通过边连接起来,形成一条路径。
- 采样与延伸:算法通过随机采样生成新的节点,并通过延伸现有节点来探索状态空间。这样可以确保算法在有限的时间内能够覆盖大部分自由空间。
- 路径优化:与传统的RRT算法不同,RRT*-FN算法不仅考虑到路径的长度,还考虑到路径上每个点到起点的代价。它使用弗洛伊德-内曼算法来优化路径,以找到具有最小总代价的最优路径。
- 节点重新连接:为了确保搜索的全局性,算法会定期对树中的节点进行重新连接,以确保所有部分都有机会被探索。
- 自适应性参数:RRT*-FN算法具有一些参数,如步长、搜索半径等,这些参数可以根据问题的复杂程度和障碍物密度进行调整,以获得更好的路径规划结果。
总的来说,RRT*-FN算法通过结合随机探索和路径优化的方法,能够在复杂环境中高效地找到机器人的路径,并且具有一定的自适应性,适用于各种不同的路径规划场景。
9.3.3 构建地图
在路径规划中,地图的构建在路径规划中扮演着关键的角色,它直接影响着路径规划算法的性能和效果。一个准确、完整的地图可以为路径规划算法提供准确的环境信息,从而使路径规划算法能够更好地执行任务。
(1)文件line.py定义了一个名为Line的类,用于表示直线,它具有如下所示的属性和方法。
- 方法__init__:初始化直线对象,接受两个点作为参数,并计算直线的方向、常数项和长度。
- 方法calculate_y:给定一个 x 坐标,计算直线上对应的 y 坐标。
- calculate_x 方法:给定一个 y 坐标,计算直线上对应的 x 坐标。
python
import numpy as np
class Line:
def __init__(self, p1: tuple, p2: tuple):
self.p1 = np.array(p1)
self.p2 = np.array(p2)
self.norm = np.linalg.norm(self.p2 - self.p1)
if p2[0] == p1[0]:
self.dir = float("inf") # 如果 p1 和 p2 的 x 坐标相同,则直线与 y 轴平行
self.x_pos = p1[0] # 直线上的任意一个点的 x 坐标
self.const_term = None
return
else:
self.dir = (p2[1] - p1[1]) / (p2[0] - p1[0]) # 计算直线的斜率
self.const_term = self.p1[1] - self.dir * self.p1[0] # 计算直线的常数项
def calculate_y(self, x0: float) -> float:
"""
根据直线方程和给定的 x0,获取 y 坐标值
:param x0: 需要计算 y 坐标的点
:return: y 的值
"""
return self.dir * x0 + self.const_term
def calculate_x(self, y0: float) -> float:
"""
根据直线方程和给定的 y0,获取 x 坐标值
:param y0: 需要计算 x 坐标的点
:return: x 的值
"""
return (y0 - self.const_term) / self.dir
上述代码使用了 NumPy 库来处理向量和矩阵运算,具体来说,使用了 numpy.array 和 numpy.linalg.norm 函数。
注意:直线对象的方向(斜率)和常数项的计算方式是基于两个点之间的差异来确定的。如果两点的 x 坐标相同,说明这条直线与 y 轴平行,此时斜率被设定为无穷大,且常数项为直线上的任意一个点的 x 坐标;否则,斜率被计算为两点间的 y 坐标差除以 x 坐标差,而常数项则是通过将斜率代入直线方程计算得到的。
(2)文件graph.py实现了一个图类Graph,用于构建和管理图结构。可以在类Graph中添加顶点、移除顶点、添加边,计算节点到起始节点的成本,并且可以生成随机点。此外,代码中还定义了一个自定义异常类(OutOfBoundsException),用于处理起始点或目标点超出地图范围的情况。
python
import random
class OutOfBoundsException(Exception):
print("Start or goal point out of bounds.")
class Graph:
def __init__(self, start: tuple, goal: tuple, width: int, height: int, xy_range: list):
"""
初始化图的类
:param start: 起始点
:param goal: 目标点
:param width: 地图宽度
:param height: 地图高度
:param xy_range: 地图范围
"""
self.start = start # 起始位置
self.goal = goal # 目标位置
self.width = xy_range[0][1] - xy_range[0][0]
self.height = xy_range[1][1] - xy_range[1][0]
self.xy_range = xy_range
self.parent = {0: None}
self.children = {0: []}
self.vertices = {0: self.start}
self.cost = {0: 0.0} # 到父节点的成本
self.id_vertex = {self.start: 0}
self.last_id = 0
def get_cost(self, from_node: int) -> float:
"""
获取到起始节点的成本
:param from_node: 起始计算成本的节点的 ID
:return: 成本
"""
cost_list = []
node = from_node
while self.parent[node] is not None:
cost_list.append(self.cost[node])
node = self.parent[node]
return sum(cost_list)
def add_vertex(self, pos: tuple) -> int:
"""
添加新的顶点到图中
:param pos: 新顶点的位置
:return: 新顶点的 ID
"""
try:
id_vertex = self.id_vertex[pos]
except KeyError:
self.last_id += 1
id_vertex = self.last_id # ID 等于当前顶点数量
self.vertices[id_vertex] = pos # 添加新顶点到顶点列表
self.id_vertex[pos] = id_vertex # 添加新顶点的 ID
self.children[id_vertex] = [] #为新顶点创建一个空的子节点列表(用于存储邻接关系)
return id_vertex
def remove_vertex(self, id: int):
"""
从图中移除顶点
:param id: 要移除的节点的 ID
"""
parent = self.parent[id]
pos = self.vertices[id]
if self.parent[id] is not None:
del self.children[parent][self.children[parent].index(id)]
del self.parent[id]
del self.children[id]
del self.vertices[id]
del self.cost[id]
del self.id_vertex[pos]
def add_edge(self, id_node: int, id_parent: int, cost: float):
"""
添加新的边到图中
:param id_node: 子节点的 ID
:param id_parent: 父节点的 ID
:param cost: 两个节点之间的边的成本
"""
self.parent[id_node] = id_parent
self.children[id_parent].append(id_node)
self.cost[id_node] = cost
def random_node(self, bias: float = 0) -> tuple:
"""
在地图上生成一个随机节点:以 bias 的概率直接返回目标点
:param bias: 目标点作为新节点的概率,取值范围 [0, 1)
:return: 节点的位置
"""
if random.random() < bias:
return self.goal
return random.uniform(self.xy_range[0][0], self.xy_range[0][1]), random.uniform(self.xy_range[1][0], self.xy_range[1][1])
(3)文件map.py定义了地图类Map,用于表示和操作地图。可以在类Map中添加障碍物,生成随机障碍物,检查是否有圆形障碍物与起点或终点发生碰撞,以及检查给定点是否被圆形障碍物占据。此外,该类还提供了一个方法来显示地图,将障碍物以黑色圆形的形式绘制在 matplotlib 中。
python
import numpy as np
import random
import matplotlib.pyplot as plt
class Map:
def __init__(self, size: tuple, start: tuple, goal: tuple, node_radius: int):
"""
地图类的初始化方法
:param size: 地图的宽度和高度
:param start: 起始节点的位置
:param goal: 目标节点的位置
"""
self.obstacles_c = [] # 圆形障碍物的位置和半径列表
self.obstacles_r = [] # 矩形障碍物的左上角和右下角位置列表
self.width = size[0]
self.height = size[1]
self.start = start # 起始节点
self.goal = goal # 目标节点
self.node_radius = node_radius # 节点的半径
def collides_w_start_goal_c(self, obs) -> bool:
""" 检查圆形障碍物是否与起点或目标点相碰
:param obs: 障碍物位置和半径的列表
:return: 如果检测到碰撞返回True,否则返回False
"""
distance_start = np.sqrt((obs[0][0] - self.start[0]) ** 2 + (obs[0][1] - self.start[1]) ** 2)
distance_goal = np.sqrt((obs[0][0] - self.goal[0]) ** 2 + (obs[0][1] - self.goal[1]) ** 2)
if distance_start < self.node_radius + obs[1] or distance_goal < self.node_radius + obs[1]:
return True
return False
def is_occupied_c(self, p: tuple) -> bool:
""" 检查空间是否被圆形障碍物占据
:param p: 要检查的点的位置
:return: 如果被占用返回True,否则返回False
"""
for obs in self.obstacles_c:
distance = np.sqrt((p[0] - obs[0][0])**2 + (p[1] - obs[0][1])**2)
if distance < self.node_radius + obs[1]:
return True
return False
def add_obstacles(self, obstacles):
for obstacle in obstacles:
self.obstacles_c.append(obstacle)
def generate_obstacles(self, obstacle_count: int = 10, size: int = 1):
""" 生成随机障碍物并存储在 self.obstacles 中。矩形障碍物尚未实现
:param obstacle_count: 要生成的障碍物数量
:param size: 障碍物的半径
"""
for _ in range(obstacle_count):
new_obstacle = (random.randint(0 + size, self.width - size),
random.randint(0 + size, self.height - size))
if self.collides_w_start_goal_c((new_obstacle, size)):
self.generate_obstacles(obstacle_count=1, size=size)
else:
self.obstacles_c.append((new_obstacle, size))
def show(self):
""" 显示地图 """
plt.gca().set_aspect('equal', adjustable='box')
for obstacle in self.obstacles_c:
circle = plt.Circle(obstacle[0], obstacle[1], color='black')
plt.gca().add_patch(circle)
plt.show()