4. 全局路径规划

任务目标

实现RRT(Rapidly-exploring Random Tree)算法,在包含障碍物的二维平面环境中规划一条从起点到终点的可行路径,并可视化随机树的生长过程与最终路径。

环境与依赖

本案例使用Python 3.8+,仅依赖numpy和matplotlib。安装命令:

pip install numpy matplotlib

核心实现

以下代码完整实现了RRT算法:定义障碍物环境、随机采样扩展树、碰撞检测、路径回溯,并输出可视化结果。


import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Circle

class RRT:
    def __init__(self, start, goal, obstacle_list, bounds, step_size=0.5, max_iter=2000):
        self.start = np.array(start, dtype=float)
        self.goal = np.array(goal, dtype=float)
        self.obstacles = obstacle_list  # 每个障碍: (x, y, w, h) 或 (x, y, r)
        self.bounds = bounds            # [xmin, xmax, ymin, ymax]
        self.step = step_size
        self.max_iter = max_iter
        self.nodes = [self.start]
        self.parent = [-1]
        self.path = []

    def _collision_free(self, p1, p2):
        """检查线段p1-p2是否与任何障碍物碰撞"""
        for obs in self.obstacles:
            if len(obs) == 3:  # 圆形障碍
                cx, cy, r = obs
                # 计算线段到圆心的最短距离
                d = np.cross(p2 - p1, np.array([cx, cy]) - p1) / np.linalg.norm(p2 - p1)
                if abs(d) < r:
                    # 检查垂足是否在线段上
                    t = np.dot(np.array([cx, cy]) - p1, p2 - p1) / np.linalg.norm(p2 - p1)**2
                    if 0 <= t <= 1:
                        return False
            else:  # 矩形障碍 (x, y, w, h)
                x, y, w, h = obs
                # 线段与矩形四条边分别检测
                rect_pts = [(x, y), (x+w, y), (x+w, y+h), (x, y+h)]
                for i in range(4):
                    a = np.array(rect_pts[i])
                    b = np.array(rect_pts[(i+1) % 4])
                    if self._segments_intersect(p1, p2, a, b):
                        return False
        return True

    def _segments_intersect(self, p1, p2, p3, p4):
        """判断两条线段是否相交"""
        def ccw(A, B, C):
            return np.cross(B - A, C - A) > 0
        return (ccw(p1, p3, p4) != ccw(p2, p3, p4)) and (ccw(p1, p2, p3) != ccw(p1, p2, p4))

    def _nearest_node(self, sample):
        """找到树中距离采样点最近的节点索引"""
        dists = np.linalg.norm(np.array(self.nodes) - sample, axis=1)
        return int(np.argmin(dists))

    def _steer(self, from_node, to_sample):
        """从from_node向to_sample方向扩展step距离"""
        direction = to_sample - from_node
        dist = np.linalg.norm(direction)
        if dist < self.step:
            return to_sample
        return from_node + direction / dist * self.step

    def plan(self):
        """执行RRT搜索,返回路径点列表"""
        for _ in range(self.max_iter):
            # 以10%概率直接采样目标点,引导树向终点生长
            if np.random.rand() < 0.1:
                sample = self.goal
            else:
                sample = np.array([np.random.uniform(self.bounds[0], self.bounds[1]),
                                   np.random.uniform(self.bounds[2], self.bounds[3])])

            nearest_idx = self._nearest_node(sample)
            nearest_node = self.nodes[nearest_idx]
            new_node = self._steer(nearest_node, sample)

            # 检查新节点是否在边界内且路径无碰撞
            if (self.bounds[0] <= new_node[0] <= self.bounds[1] and
                self.bounds[2] <= new_node[1] <= self.bounds[3] and
                self._collision_free(nearest_node, new_node)):

                self.nodes.append(new_node)
                self.parent.append(nearest_idx)

                # 到达目标附近则回溯路径
                if np.linalg.norm(new_node - self.goal) < self.step:
                    self.nodes.append(self.goal)
                    self.parent.append(len(self.nodes) - 2)
                    self._extract_path()
                    return self.path
        return None  # 达到最大迭代次数仍未找到路径

    def _extract_path(self):
        """从目标节点回溯到起点"""
        idx = len(self.nodes) - 1
        while idx != -1:
            self.path.append(self.nodes[idx])
            idx = self.parent[idx]
        self.path.reverse()

    def visualize(self):
        """绘制障碍物、随机树和最终路径"""
        fig, ax = plt.subplots(figsize=(8, 8))
        # 绘制障碍物
        for obs in self.obstacles:
            if len(obs) == 3:
                ax.add_patch(Circle((obs[0], obs[1]), obs[2], color='gray', alpha=0.7))
            else:
                ax.add_patch(Rectangle((obs[0], obs[1]), obs[2], obs[3],
                                       color='gray', alpha=0.7))
        # 绘制树结构
        for i, node in enumerate(self.nodes):
            if self.parent[i] != -1:
                px, py = self.nodes[self.parent[i]]
                ax.plot([px, node[0]], [py, node[1]], 'lightblue', linewidth=0.5)
        # 绘制最终路径
        if self.path:
            path_arr = np.array(self.path)
            ax.plot(path_arr[:, 0], path_arr[:, 1], 'r-', linewidth=2.5, label='Path')
        # 绘制起点和终点
        ax.plot(*self.start, 'go', markersize=10, label='Start')
        ax.plot(*self.goal, 'ro', markersize=10, label='Goal')
        ax.set_xlim(self.bounds[0], self.bounds[1])
        ax.set_ylim(self.bounds[2], self.bounds[3])
        ax.set_aspect('equal')
        ax.legend()
        ax.set_title('RRT Path Planning')
        plt.show()

# 使用示例
if __name__ == "__main__":
    # 定义环境:起点、终点、障碍物列表、边界
    start = (0.5, 0.5)
    goal = (9.0, 9.0)
    obstacles = [(3, 3, 1.5, 1.5), (6, 2, 1.0, 2.0), (2, 6, 2.0, 1.0), (7, 7, 1.0, 1.0)]
    bounds = [0, 10, 0, 10]

    rrt = RRT(start, goal, obstacles, bounds, step_size=0.6, max_iter=3000)
    path = rrt.plan()

    if path:
        print(f"路径找到,共 {len(path)} 个路径点")
        print("路径点序列:")
        for p in path:
            print(f"  ({p[0]:.2f}, {p[1]:.2f})")
    else:
        print("未找到可行路径")

    rrt.visualize()

运行与验证

运行方式:将上述代码保存为rrt_planning.py,在终端执行python rrt_planning.py。

验证方法:

  • 程序输出“路径找到”并打印路径点序列,说明RRT成功规划出可行路径;
  • 弹出的可视化窗口应显示:灰色障碍物、浅蓝色随机树、红色最终路径、绿色起点和红色终点;
  • 红色路径应连续且不穿过任何灰色障碍物,从起点连通至终点。

判定标准:若输出“未找到可行路径”,可增大max_iter或step_size后重新运行;若路径穿过障碍物,说明碰撞检测有误,需检查障碍物参数定义。

关键说明

算法采用概率完备的RRT策略:每次迭代随机采样,扩展树向采样点生长,并以10%概率直接采样目标点以加速收敛。碰撞检测支持矩形和圆形两种障碍物类型,通过线段相交和点到线段距离判断。本实现为教学演示版本,未包含路径平滑处理,实际应用中可在RRT基础上增加剪枝或B样条平滑优化路径质量。