""" Path planning Sample Code with Randomized Rapidly-Exploring Random Trees (RRT) author: AtsushiSakai(@Atsushi_twi) """ import math import random import matplotlib.pyplot as plt import numpy as np from pathsmoothing import get_path_length, get_target_point show_animation = True class RRT: """ Class for RRT planning """ class Node: """ RRT Node """ def __init__(self, x=0.0, y=0.0): self.x = x self.y = y self.path_x = [] # Node 到 parent 间的路径链 self.path_y = [] self.parent = None class AreaBounds: def __init__(self, area): self.xmin = float(area[0]) self.xmax = float(area[1]) self.ymin = float(area[2]) self.ymax = float(area[3]) def __init__(self, rand_area, # 横纵坐标的采样范围 [minx,maxx,miny,maxy] map, # 离散化地图 scale_ratio=5, # 地图缩放率 expand_dis=200, # 最大扩展距离 path_resolution=10.0, # 路径链分辨率 goal_sample_rate=5, # 5%的几率采样到目标点 max_iter=500, robot_radius=12.0, # robot身位半径 ): self.map = map self.scale_ratio = scale_ratio self.start = self.Node(0, 0) self.goal = self.Node(0, 0) (self.min_x, self.max_x, self.min_y, self.max_y) = rand_area # self.play_area = self.AreaBounds(rand_area) # 采样区域(边界框启发式) self.play_area = None self.expand_dis = expand_dis self.path_resolution = path_resolution self.goal_sample_rate = goal_sample_rate self.max_iter = max_iter self.node_list = [] self.robot_radius = robot_radius def reset(self): ''' 重置rrt的变量和数据结构 ''' self.node_list.clear() self.start = self.Node(0, 0) self.goal = self.Node(0, 0) def update_play_area(self): ''' 更新采样区域 ''' if self.goal.x > self.start.x: self.play_area.xmin = self.start.x self.play_area.xmax = self.max_x else: self.play_area.xmin = self.min_x self.play_area.xmax = self.start.x if self.goal.y > self.start.y: self.play_area.ymin = self.start.y self.play_area.ymax = self.max_y else: self.play_area.ymin = self.min_y self.play_area.ymax = self.start.y def planning(self, start, goal, path_smoothing=True, animation=False): """ rrt path planning, return a path list animation: flag for animation on or off """ (self.start.x, self.start.y) = start (self.goal.x, self.goal.y) = goal # self.update_play_area() self.node_list = [self.start] # 已经采样的节点列表 for i in range(self.max_iter): rnd_node = self.get_random_node() # 随机采样节点 nearest_ind = self.get_nearest_node_index(self.node_list, rnd_node) nearest_node = self.node_list[nearest_ind] # node_list中距离新采样点最近的节点 new_node = self.steer(nearest_node, rnd_node, self.expand_dis) # 确保新采样节点在范围内且不是障碍物 if self.check_if_outside_play_area(new_node, self.play_area) and \ self.check_collision(new_node): self.node_list.append(new_node) if animation and i % 5 == 0: self.draw_graph(rnd_node) if self.calc_dist_to_goal(self.node_list[-1].x, self.node_list[-1].y) <= self.expand_dis: final_node = self.steer(self.node_list[-1], self.goal, self.expand_dis) if self.check_collision(final_node): path = self.generate_final_course(len(self.node_list) - 1) if path_smoothing: return self.path_smoothing(path) else: return path if animation and i % 5: self.draw_graph(rnd_node) return None # cannot find path def steer(self, from_node, to_node, extend_length=float("inf")): ''' 返回from_node 到 to_node 距离限制内最远的节点,并设置其父节点为from_node 并计算路径链 Args: entend_length: 距离限制 return: new_node: from_node 到 to_node 距离限制内最远的节点 ( 如果 to_node 在距离限制内则返回 to_node ) ''' new_node = self.Node(from_node.x, from_node.y) d, theta = self.calc_distance_and_angle(new_node, to_node) # from 和 to 之间的距离和方向角 new_node.path_x = [new_node.x] new_node.path_y = [new_node.y] if extend_length > d: extend_length = d n_expand = math.floor(extend_length / self.path_resolution) # from 和 to 之间的采样点数 for _ in range(n_expand): new_node.x += self.path_resolution * math.cos(theta) new_node.y += self.path_resolution * math.sin(theta) new_node.path_x.append(new_node.x) new_node.path_y.append(new_node.y) d, _ = self.calc_distance_and_angle(new_node, to_node) if d <= self.path_resolution: new_node.path_x.append(to_node.x) new_node.path_y.append(to_node.y) new_node.x = to_node.x new_node.y = to_node.y new_node.parent = from_node return new_node def generate_final_course(self, goal_ind) -> [(float, float), ]: ''' 得到路径(起点->终点) ''' path = [(self.goal.x, self.goal.y)] node = self.node_list[goal_ind] while node.parent is not None: path.append((node.x, node.y)) node = node.parent path.append((node.x, node.y)) path.reverse() return path def calc_dist_to_goal(self, x, y): ''' 计算节点到终点距离 ''' dx = x - self.goal.x dy = y - self.goal.y return math.hypot(dx, dy) def get_random_node(self): ''' 在规定范围内随机采样节点(一定概率采样到目标节点) ''' if random.randint(0, 100) > self.goal_sample_rate: rnd = self.Node( random.uniform(self.min_x, self.max_x), random.uniform(self.min_y, self.max_y)) else: # goal point sampling rnd = self.Node(self.goal.x, self.goal.y) # 返回goal return rnd def map2real(self, x, y): ''' 地图坐标->实际坐标 ''' x = x * self.scale_ratio + self.min_x y = y * self.scale_ratio + self.min_y return x, y def real2map(self, x, y): ''' 实际坐标->地图坐标 (向下取整) ''' # x = round((x - self.min_x) / self.scale_ratio) # y = round((y - self.min_y) / self.scale_ratio) x = math.floor((x - self.min_x) / self.scale_ratio) y = math.floor((y - self.min_y) / self.scale_ratio) return x, y def check_collision(self, node): ''' 判断 node 到 父节点之间 是否有障碍物 ''' if node is None: return False for (x, y) in zip(node.path_x, node.path_y): map_x, map_y = self.real2map(x, y) # TODO: 碰撞检测考虑robot_radius和scale_ratio if self.map[map_x, map_y] or self.map[map_x + 1, map_y] or \ self.map[map_x, map_y + 1] or self.map[map_x + 1, map_y + 1]: return False return True # safe def draw_graph(self, new=None, final_path=None): # 清空当前figure内容,保留figure对象 plt.clf() # for stopping simulation with the esc key. 按esc结束 plt.gcf().canvas.mpl_connect( 'key_release_event', lambda event: [exit(0) if event.key == 'escape' else None]) # 画地图: X行Y列,第一行在下面 # 范围: 横向Y[-70,120] 纵向X[-80,290] plt.imshow(self.map, cmap='binary', alpha=0.5, origin='lower', extent=(self.min_y / self.scale_ratio, self.max_y / self.scale_ratio, self.min_x / self.scale_ratio, self.max_x / self.scale_ratio)) # 画新采样点 if new is not None: (scale_x, scale_y) = (new.x / self.scale_ratio, new.y / self.scale_ratio) plt.plot(scale_y, scale_x, "^k") if self.robot_radius > 0.0: self.plot_circle(scale_y, scale_x, self.robot_radius / self.scale_ratio, '-r') # 画新边 if new.parent: plt.plot([y / self.scale_ratio for y in new.path_y], [x / self.scale_ratio for x in new.path_x], "-g") # 画整个搜索树 for node in self.node_list: if node.parent: plt.plot([y / self.scale_ratio for y in node.path_y], [x / self.scale_ratio for x in node.path_x], "-g") # 画起点和目标 plt.plot(self.start.y / self.scale_ratio, self.start.x / self.scale_ratio, "xr") plt.plot(self.goal.y / self.scale_ratio, self.goal.x / self.scale_ratio, "xr") plt.xlabel('y', loc='right') plt.ylabel('x', loc='top') plt.grid(color='black', linestyle='-', linewidth=0.5) plt.pause(0.2) # 画最终路径 if final_path: plt.plot([y / self.scale_ratio for (x, y) in final_path], [x / self.scale_ratio for (x, y) in final_path], '-r') # 画出最终路径 plt.pause(2) @staticmethod def plot_circle(x, y, size, color="-b"): # pragma: no cover ''' 以(x,y)为圆心,size为半径 画圆 ''' deg = list(range(0, 360, 5)) deg.append(0) xl = [x + size * math.cos(np.deg2rad(d)) for d in deg] yl = [y + size * math.sin(np.deg2rad(d)) for d in deg] plt.plot(xl, yl, color) @staticmethod def get_nearest_node_index(node_list, rnd_node): ''' 得到距离rnd_node最近的节点编号 ''' dlist = [(node.x - rnd_node.x) ** 2 + (node.y - rnd_node.y) ** 2 for node in node_list] minind = dlist.index(min(dlist)) return minind @staticmethod def check_if_outside_play_area(node, play_area): if play_area is None: return True # no play_area was defined, every pos should be ok if node.x < play_area.xmin or node.x > play_area.xmax or \ node.y < play_area.ymin or node.y > play_area.ymax: return False # outside - bad else: return True # inside - ok @staticmethod def calc_distance_and_angle(from_node, to_node): ''' 计算2点间的距离和角度( 以from_node为原点 ) ''' dx = to_node.x - from_node.x dy = to_node.y - from_node.y d = math.hypot(dx, dy) # 欧式距离 theta = math.atan2(dy, dx) return d, theta # **************************Path Smoothing**************************************************** # # **************************Path Smoothing**************************************************** # # **************************Path Smoothing**************************************************** # def line_collision_check(self, first, second): ''' 检查first-second的直线是否穿过障碍物 Args: path_resolution: 采样点分辨率 ''' x1 = first[0] y1 = first[1] x2 = second[0] y2 = second[1] path_x = [x1] path_y = [y1] # 计算距离和夹角 dx = x2 - x1 dy = y2 - y1 d = math.hypot(dx, dy) # 欧式距离 theta = math.atan2(dy, dx) # 夹角 n_expand = math.floor(d / self.path_resolution) # first 和 second 之间的采样点数 for _ in range(n_expand): x1 += self.path_resolution * math.cos(theta) y1 += self.path_resolution * math.sin(theta) path_x.append(x1) path_y.append(y1) if d <= self.path_resolution: path_x.append(x2) path_y.append(y2) for (x, y) in zip(path_x, path_y): map_x, map_y = self.real2map(x, y) # 向下取整 # TODO: 碰撞检测考虑robot_radius和scale_ratio if self.map[map_x, map_y] or self.map[map_x + 1, map_y] or \ self.map[map_x, map_y + 1] or self.map[map_x + 1, map_y + 1]: return False return True def path_smoothing(self, path, max_iter=1000): ''' 输入原路径,输出平滑后的路径 Args: path: [(x,y),...] ''' le = get_path_length(path) for i in range(max_iter): # Sample two points pickPoints = [random.uniform(0, le), random.uniform(0, le)] # 总路径长度中采样2个点 pickPoints.sort() first = get_target_point(path, pickPoints[0]) second = get_target_point(path, pickPoints[1]) if first[2] <= 0 or second[2] <= 0: continue if (second[2] + 1) > len(path): continue if second[2] == first[2]: continue # collision check if not self.line_collision_check(first, second): continue # Create New path newPath = [] newPath.extend(path[:first[2] + 1]) newPath.append([first[0], first[1]]) newPath.append([second[0], second[1]]) newPath.extend(path[second[2] + 1:]) path = newPath le = get_path_length(path) return path def main(gx=6.0, gy=10.0): print("start " + __file__) # ====Search Path with RRT==== obstacleList = [(5, 5, 1), (3, 6, 2), (3, 8, 2), (3, 10, 2), (7, 5, 2), (9, 5, 2), (8, 10, 1)] # [x, y, radius] # Set Initial parameters rrt = RRT( start=[0, 0], goal=[gx, gy], rand_area=[-2, 15], obstacle_list=obstacleList, # play_area=[0, 10, 0, 14] robot_radius=0.8 ) path = rrt.planning(animation=show_animation) if path is None: print("Cannot find path") else: print("found path!!") # Draw final path if show_animation: rrt.draw_graph() # 画出探索过程 plt.plot([x for (x, y) in path], [y for (x, y) in path], '-r') # 画出最终路径 plt.grid(True) plt.pause(0.01) plt.show() if __name__ == '__main__': main()