Webots——RRT算法
RRT路径规划的实验报告
仓库地址:github
实验目标
- 实现
RRT
路径规划算法,并让小车依照规划的路线进行行走,从起点到终点。
实验环境
-
开发环境:macOS Monterey 12.0.1
-
开发工具:Webots 2021b, OpenCV 4, clang-1300.0.29.3
实验内容
本实验内容主要是两个:
-
运用
RRT
算法规划出一条从起点到终点的最短路线 -
让小车依照此路线从起点走到终点
限于系统原因,OpenCV
安装时所编译的动态链接库为Arm
架构,而Webots
暂未对MacOS
的M1
类型适配,其动态链接库均为x86_64
架构。这将导致Webots
在生成项目时,在链接动态链接库阶段出现找不到OpenCV
相关成员变量的x86_64
的动态库,而在手动用命令行编译时,亦出现找不到Webots
相关成员变量的Arm
的动态库。因此这里将这两个代码分开,即路径规划和巡线代码为不同文件,其中路径规划代码引用了OpenCV
库,在命令行编译下能够正常编译成功运行,巡线代码在Webots
下亦能正常编译成功运行。而理论上它们在Linux
或Windows
应能融洽在同一个代码中。
本次实验提供了一个已经制作好的迷宫的世界。
RRT路径规划
首先是进行RRT
路径规划的实现。
本次实验的平面图如图下图所示。
右上角绿色圈圈为起点,左下角红色圈圈为终点。
通过OpenCV
的cv::imread(s.c_str(), cv::IMREAD_COLOR);
函数,以彩色方式读取该图像,输出其长宽发现是一张800\times 600
的图像,将其输入的三通道像素数输出到文件qwq
进行分析,可以得到起点的绿色圈圈的色素值为,终点的红色圈圈的色素值为,对全图遍历找到可以找到起点和终点像素位置并保存。
maze = cv::imread(s.c_str(), cv::IMREAD_COLOR); |
同时将一切像素值不是255
的点都视为障碍物。虽然这不太精确,但是考虑到小车是有实际体积而非质点,而规划路径时力求最短路,会很大可能造成规划出来的路径贴墙,因此将非255
的视为障碍物能够一定程度地将墙边缘的像素视为障碍物,让规划出来的路径尽可能不贴着墙。
这里相比上次实验代码加上了优化,在判障碍物时将障碍物方圆REC
的切比雪夫距离的像素均视为障碍物,这虽然增加了些许图像处理时间,但对下面运用RRT
算法求解路径时带来了倍的速度提升,意义重大。
这里将障碍物的二维坐标转换成一维坐标,并保存在set
中。
inline int two2one(int x, int y){ |
OpenCV的运行结果如下图所示。其输出了起点和终点坐标。
RRT
算法一共三个步骤:
-
图上随机采样点
a
-
找到与该点距离最近的点
b
,让点b
往点a
方向步长距离step
,得到新点c
,建立有向边 -
重复以上操作,直到走到终点
值得注意的是,RRT
算法生成出来的是一棵树。众所周知,树上两点路径唯一。
由于朴素的RRT
算法存在诸多问题,比如收敛速度慢,因此加上了概率选择目标点作为采样点的方案;但也有难以求解最优的可行路的问题,因此这里实现的是RRT*
算法。
RRT*
算法相比朴素的RRT
算法,多了两个步骤:重新选择父节点过程和重布线随机树过程,总的过程如下:
-
图上随机采样点
a
-
找到与该点距离最近的点
b
,让点b
往点a
方向步长距离step
,得到新点c
-
在点
c
方圆dis1
距离找到点d
,使得从的距离最小(重新选择父节点),建立有向边(重新选择父节点,即不一定选点b
作为父节点),其中st
表示起点 -
在点
c
方圆dis2
距离找到符合条件的点e
,使得距离小于原的距离,更改点e
的父亲为点
c$
即从点c
走到点e
),删去原先的有向边,新建有向边,同时更新点e
的子树的所有点的距离。其中fa[i]
表示点i
的父亲。(重布线随机数) -
重复以上操作,直到走到终点
新增的两个步骤都是优化最短路。以下是一个简单的例子。
在上图里,这里通过采样后从右边的点拓展了一个点,此时在方圆距离寻找一个距离起点最近的点。
如蓝色线所示,事实上左边的点距离起点最近,因此重新选择父节点为左边的点。如上图所示。
重新选择父节点后,进行重新布线操作。同样以采样点方圆距离寻找是否有点通过该点能以更近的距离到达起点,如上图所示。
发现蓝色线回到起点跟红色线回到起点近,于是删除原先父边,建立新边,如上图所示。这就是重布线操作。当然如果其点还有后继点,需要沿路更新最短距离到叶子。
以下是代码讲解。
void findPath(){ |
核心函数findPath
,前面均为初始化。然后开始扩展。
while(true){ |
步骤也如上述所说的,先采样点,然后从点集中找到距离最近的点,往采样点方向进行扩展,然后重新选择父节点和进行重布线操作。
随机采样点函数很简单。
// 随机生成点 |
其中two2one
函数是将点二维坐标转换成一维。
寻找距离最近的点的函数也很简单。
// 寻找pointSet中距离point最近的点 |
步长扩展点函数稍有点复杂。
pair<bool, int> goStep(int curPoint, int dirPoint){ |
主要是判断扩展的点是否合法,除了判断是否在障碍物,出界,以及连线无障碍之外,还要判断是否在点集里,在的话也视为非法,因为已经扩展的点就没必要在此扩展了,在早期版本中,pointSet
还是vector
类型,就发生了栈溢出的现象,主要是重复点加进来太多爆内存了。
valid
函数如下。
inline bool valid(int x, int y){ // 判断当前点是否合法,即是否越界或者是否在障碍物边缘 |
上个实验代码里对点判越界写错了,原先写的
是与关系,这里已修正为或关系。以及正如前文所说,原先这里需要
次判断来判断是否在障碍物附近,由于在处理图像时已经进行了预处理,故此处优化成只需次了。实测运行速度大幅提升。
checkline
函数如上次一致。
bool checkLine(int x1, int y1, int x2, int y2){ // 判断一条直线上是否有障碍物 |
重新选择父亲的函数如下。
// 重新选择父亲 |
这跟寻找最近点的距离很类似,但判断的条件不同。距离最近的就单纯地看距离最近,而最好的父亲节点是到起点最近的节点。
重布线函数如下。
void rewrite(int point, set<int>& pointSet, vector<double>& pointDistance){ |
距离更新就通过edge
储存的父亲到儿子到有向边进行递归更新。
// 计算到终点距离,判断是否在终点附近 |
判断是否到达终点距离的函数就比较简单了。一旦到达终点附近,求解路径就算结束了。
for(int point = ed; point != st; point = fa[point]) |
最后根据fa
数组关系记录最短路径即可。
以下是一些跑出来的效果图。
如第一次结果所示,尽管有众多优化,RRT
算法能有可能会找到较远路径,尽管概率很小,此为稀有路径了。
这三张图是无重布线操作RRT
。
这三张图是有重布线操作RRT
。
从图中可以很好的看出,不带重布线操作的连边情况犹如一棵正常的树一样,且得到的路径稍有摆动。而带有重布线操作的连边情况像是放射性一般,且得到的路径非常平整。
小车巡线
此部分与上一次PRM
实验报告相一致,本次实验未改动,直接沿用上次代码。在此就不重复赘述,具体详情参见PRM算法
实验结果
演示视频:bilibili
实验总结
通过本次实验,我了解到了RRT
算法实现的具体流程,明白了RRT
算法的重要意义:连续变离散化。由于世界是连续的,RRT
算法通过对世界采样将其离散化,往采样点方向前进。这非常类似于人对现实环境的探索。而且通过对世界随机采样点,再以采样点的方向行走,这在一定程度上能保证探索到的地方尽可能平均尽可能多。因为比如当前位置在世界的最左边,通过采样点的方向前进,则有非常大的概率会往右边走,因为会往右边走的采样点远多于会往左边走的点,避免一头撞到墙然后不动了。但这其实也有点不太好的地方就是,如果终点在某个角落,则可能会难以走到终点,因为每次接近终点时,采样点方向远离终点的概率就很大,从而不往终点那边走。因此才有会以一定概率选择终点为采样点,加快收敛速度的方法。为了解决RRT
探索出来的路径一般不会是最优路径以及无意义的路径摆动,又增加了重新选择父节点和重新布线的方法,而从实验结果可以看出重布线的方案能有效较少路径无意义地摆动的情况,让路径尽可能平坦。
本次实验未改动过控制器部分代码,仅仅修改路径规划的核心代码,控制器可以不加修改地直接复用,这也体现了人类抽象能力的伟大。