4. RRT*路径规划

任务目标

在包含障碍物的二维地图中,实现RRT*(Rapidly-exploring Random Tree Star)路径规划算法,生成从起点到终点的无碰撞路径,并通过路径重连接与渐进优化策略持续缩短路径长度。最终输出优化后的路径点序列及其总长度。

环境依赖

Python 3.8+,仅需标准库(math, random, matplotlib用于可视化验证)。无需第三方路径规划库。

核心实现

import math
import random
import matplotlib.pyplot as plt

class RRTStar:
    def __init__(self, start, goal, obstacle_list, map_width=50, map_height=50,
                 expand_dist=3.0, goal_sample_rate=0.1, max_iter=500, connect_circle_dist=8.0):
        self.start = self.Node(start[0], start[1])
        self.goal = self.Node(goal[0], goal[1])
        self.obstacles = obstacle_list  # [(x, y, radius), ...]
        self.width, self.height = map_width, map_height
        self.expand_dist = expand_dist
        self.goal_sample_rate = goal_sample_rate
        self.max_iter = max_iter
        self.connect_circle_dist = connect_circle_dist
        self.node_list = [self.start]

    class Node:
        def __init__(self, x, y, cost=0.0, parent=None):
            self.x, self.y = x, y
            self.cost = cost
            self.parent = parent

    def plan(self):
        for _ in range(self.max_iter):
            rnd_node = self._get_random_node()
            nearest_idx = self._get_nearest_idx(rnd_node)
            new_node = self._steer(nearest_idx, rnd_node)
            if new_node and not self._collision(new_node):
                near_idxs = self._get_nearby_idxs(new_node)
                new_node = self._choose_parent(new_node, near_idxs)
                self.node_list.append(new_node)
                self._rewire(new_node, near_idxs)
        return self._extract_path()

    def _get_random_node(self):
        if random.random() < self.goal_sample_rate:
            return self.Node(self.goal.x, self.goal.y)
        return self.Node(random.uniform(0, self.width), random.uniform(0, self.height))

    def _get_nearest_idx(self, node):
        return min(range(len(self.node_list)),
                   key=lambda i: self._dist(node, self.node_list[i]))

    def _steer(self, from_idx, to_node):
        from_node = self.node_list[from_idx]
        dist = self._dist(from_node, to_node)
        if dist < self.expand_dist:
            return None
        theta = math.atan2(to_node.y - from_node.y, to_node.x - from_node.x)
        new_x = from_node.x + self.expand_dist * math.cos(theta)
        new_y = from_node.y + self.expand_dist * math.sin(theta)
        new_node = self.Node(new_x, new_y)
        new_node.cost = from_node.cost + self.expand_dist
        new_node.parent = from_idx
        return new_node

    def _collision(self, node):
        for (ox, oy, r) in self.obstacles:
            if math.hypot(node.x - ox, node.y - oy) <= r + 0.2:
                return True
        return False

    def _get_nearby_idxs(self, node):
        return [i for i, n in enumerate(self.node_list)
                if self._dist(n, node) < self.connect_circle_dist]

    def _choose_parent(self, new_node, near_idxs):
        for i in near_idxs:
            near_node = self.node_list[i]
            if not self._collision_path(near_node, new_node):
                new_cost = near_node.cost + self._dist(near_node, new_node)
                if new_cost < new_node.cost:
                    new_node.cost = new_cost
                    new_node.parent = i
        return new_node

    def _rewire(self, new_node, near_idxs):
        for i in near_idxs:
            near_node = self.node_list[i]
            if not self._collision_path(new_node, near_node):
                new_cost = new_node.cost + self._dist(new_node, near_node)
                if new_cost < near_node.cost:
                    near_node.cost = new_cost
                    near_node.parent = len(self.node_list) - 1

    def _collision_path(self, from_node, to_node):
        dist = self._dist(from_node, to_node)
        steps = max(1, int(dist / 0.5))
        for i in range(1, steps):
            t = i / steps
            x = from_node.x + t * (to_node.x - from_node.x)
            y = from_node.y + t * (to_node.y - from_node.y)
            if self._collision(self.Node(x, y)):
                return True
        return False

    def _extract_path(self):
        goal_idx = min(range(len(self.node_list)),
                       key=lambda i: self._dist(self.node_list[i], self.goal))
        if self._dist(self.node_list[goal_idx], self.goal) > self.expand_dist:
            return None
        path = [(self.goal.x, self.goal.y)]
        node = self.node_list[goal_idx]
        while node.parent is not None:
            path.append((node.x, node.y))
            node = self.node_list[node.parent]
        path.append((self.start.x, self.start.y))
        return path[::-1]

    @staticmethod
    def _dist(n1, n2):
        return math.hypot(n1.x - n2.x, n1.y - n2.y)

def main():
    obstacles = [(20, 20, 4), (30, 30, 5), (15, 35, 3), (35, 15, 4), (25, 10, 3)]
    rrt = RRTStar(start=(2, 2), goal=(45, 45), obstacle_list=obstacles)
    path = rrt.plan()

    if path is None:
        print("未找到可行路径")
        return

    total_len = sum(math.hypot(path[i][0]-path[i-1][0], path[i][1]-path[i-1][1])
                    for i in range(1, len(path)))
    print(f"路径点数量: {len(path)}")
    print(f"路径总长度: {total_len:.2f}")

    # 可视化
    fig, ax = plt.subplots(figsize=(8, 8))
    for (ox, oy, r) in obstacles:
        circle = plt.Circle((ox, oy), r, color='gray', alpha=0.7)
        ax.add_patch(circle)
    px, py = zip(*path)
    ax.plot(px, py, 'b-', linewidth=2, label='RRT* Path')
    ax.plot(2, 2, 'go', markersize=10, label='Start')
    ax.plot(45, 45, 'ro', markersize=10, label='Goal')
    ax.set_xlim(0, 50); ax.set_ylim(0, 50)
    ax.set_aspect('equal'); ax.legend(); ax.grid(True)
    plt.savefig('rrt_star_result.png', dpi=100)
    print("路径图已保存为 rrt_star_result.png")

if __name__ == '__main__':
    main()

运行与验证

  1. 运行方式:将代码保存为 rrt_star.py,在终端执行 python rrt_star.py。
  2. 预期输出:程序打印路径点数量和路径总长度,并生成 rrt_star_result.png 可视化文件。由于算法含随机采样,每次运行路径点数量与总长度会略有不同,但应满足:
  • 路径起点为 (2,2),终点为 (45,45);
  • 路径上所有点与障碍物圆心距离均大于对应半径+0.2(无碰撞);
  • 路径总长度应明显短于普通RRT的锯齿形路径(通常小于80,典型值在55~70之间),且路径较为平滑。

通过判定标准:打开生成的图片,确认蓝色路径从绿色起点出发、绕过所有灰色圆形障碍物、到达红色终点,且路径无穿越障碍物。若打印的路径总长度在多次运行中持续收敛于较短值(相比RRT有明显优化),则RRT*的路径优化功能验证通过。

关键说明

RRT*相比RRT的核心改进在于选择父节点(_choose_parent)和重连接(_rewire)两步:新节点会从邻近节点中选择代价最小的作为父节点,并在插入后尝试重连邻近节点以降低其累计代价。这两步使树在生长过程中不断局部优化,最终生成的路径代价趋近最优。若需调整路径平滑度或搜索效率,可修改expand_dist(步长)和max_iter(迭代次数)。