RoboWaiter/robowaiter/algos/explore/rrt.py

441 lines
15 KiB
Python
Raw Normal View History

2023-11-09 08:47:57 +08:00
"""
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()