1. 引言
无人机航线规划是植保无人机能否正常作业的重点,航线规划的好坏决定了无人机能否对整个区域全覆盖、不飞出边界、不重复、不遗漏的扫描喷洒作业。目前市面上不乏好的植保航线规划算法,大多数是基于凸多边形的航线规划,对凹多边形的航线规划不能做到全覆盖、不飞出边界的要求。下面介绍我所实现的植保无人机航线规划算法,目的是解决区域全覆盖、不飞出边界等作业要求,性能上不一定是最优实现。
2. 作业区域要求
本算法对作业区域没有过多要求,但对区域过于复杂、作业面积过小或过大、区域中有两条边线之间夹角过小等不能实现最优。
3. 算法实现
3.1 区域旋转
在给定的参数中,选择的方向可能是任意方向,规定航线方向可以使航线规划更方便。所以整个区域在给定的方向需要旋转90°,使给定方向与水平方向平行。目前做法是以整个区域的最左上角坐标为原点进行旋转,给出代码:
/**
* 旋转坐标系
*
* @param boundPoints 需要旋转的坐标
* @param rotateRadian 旋转弧度
* @return 旋转的数据
* @throws IOException
* @throws ClassNotFoundException
*/
private static TranslateAndRotateBean translateAndRotateBoundFromWorld(List<? extends PointD> boundPoints, double rotateRadian) throws IOException, ClassNotFoundException {
MaxMinLatLng maxMinLatLng = getMaxMinLatLng(boundPoints);
PointD intersectPoint = new PointD(maxMinLatLng.getMinLat(), maxMinLatLng.getMinLng());//旋转后的原点
double reallyRotateRadian = -rotateRadian + PI12;
TranslateAndRotateBean translateAndRotateBean = new TranslateAndRotateBean(intersectPoint, reallyRotateRadian);
//旋转
for (PointD boundPoint : boundPoints) {
rotateFromWorld(boundPoint, translateAndRotateBean);
}
return translateAndRotateBean;
}
/**
* 旋转
*
* @param pointD 点
* @param t 旋转参数
*/
private static void rotateFromWorld(PointD pointD, TranslateAndRotateBean t) {
double x = pointD.x;
double y = pointD.y;
pointD.x = (x - t.getIntersectPoint().x) * cos(t.getReallyRotateRadian()) - (y - t.getIntersectPoint().y) * sin(t.getReallyRotateRadian()) + t.getIntersectPoint().x;
pointD.y = (y - t.getIntersectPoint().y) * cos(t.getReallyRotateRadian()) + (x - t.getIntersectPoint().x) * sin(t.getReallyRotateRadian()) + t.getIntersectPoint().y;
}
3.2 生成航线
根据3.1操作,得到了旋转后的区域,接下来的操作会针对旋转后的区域进行航线生成。
start=>start: 开始
1=>operation: 找到整个区域中最高点,
将此点记录到当前最高点
2=>condition: 当前最高点是否比最低点大
3=>operation: 根据给定喷幅从当前最高点向
下移动半个喷幅的距离,
对整个区域画横线
4=>operation: 获得横线与区域的交点,
将所有区域交点按照
从左至右的顺序排序
5=>condition: 交点数量是否大于2个
6=>operation: 取出前两个点,生成
航线并加入到航线集合中
7=>operation: 移出这两个交点
8=>operation: 将当前最高点再向
下移动半个喷幅高度
end=>end: 结束
start->1
1->2
2(no)->end
2(yes)->3
3->4
4->5
5(yes)->6
5(no)->8
6->7
7->5
8->2
- 首先找到整个区域中最高点,将此点记录到当前最高点;
- 判断当前最高点是否已经比最低点小,如果是则进行步骤8,如果否,则进行步骤3;
- 根据给定喷幅从当前最高点向下移动半个喷幅的距离,对整个区域画横线;
- 获得横线与区域的交点,将所有区域交点按照从左至右的顺序排序;
- 判断交点数量是否大于2个,如果是,进行步骤6,如果否进行步骤(这里存在一个问题,待后续介绍);
- 取出前两个点,生成航线并加入到航线集合中;
- 移出这两个交点,进行步骤4;
- 将当前最高点再向下移动半个喷幅高度,进行步骤2;
自此,所有航线都已经添加到航线集合中了。
3.3 连接航线
首先对生成的航线按照旋转的角度以及原来的旋转原点对航线还原成真实的航线。
拿出第一条航线,根据用户给定的起飞点的位置,判断这条航线的开始点、结束点哪个距离起飞点更近,如果结束点距离起飞点更近,则交换开始点、结束点。
取出第二条航线,判断第二条航线的起飞点与结束点哪个与上一条航线的结束点更近,如果结束点更近,则交换开始点与结束点,以此类推。
3.4 小结
此刻,航线已经生成,已经可以做到全覆盖,但是对于某些情况下,无法做到航线飞出区域外、空飞路径过长问题,所以目前生成的航线需要优化。
4 航线优化
4.1 航线顺序优化
航线生成时,所有航线都是一个个的添加到航线集合中,这样导致航线在有些情况下,两条航线的连接会穿过区域,使航线在区域外。所以优化之一就是在3.2基础上,在得到每条航线的交点时,记录每条航线与区域的哪两条边相交,将航线关联到两条边上,相同的两条边上的航线应该连接在一起。
/**
* 获取对应航线区域
*
* @param sprayGraphList 航线区域的Map
* @param startLinePointPosition 航线起始交点在区域中的边的位置
* @param endLinePointPosition 航线结束交点在区域中的边的位置
* @return 所对应的航线区域
*/
private static SprayGraph getAssignSprayGraph(Map<String, SprayGraph> sprayGraphList, int startLinePointPosition, int endLinePointPosition) {
String hashCode = startLinePointPosition + " " + endLinePointPosition;
SprayGraph sprayGraph = sprayGraphList.get(hashCode);
if (sprayGraph == null) {
sprayGraph = new SprayGraph();
sprayGraphList.put(hashCode, sprayGraph);
}
return sprayGraph;
}
SprayGraph类:是真正保存航线的位置。
sprayGraphList:这个Map中存储SprayGraph,它的键是以航线开始交点的边的位置和结束交点边的位置构成的字符串,因为生成航线时,航线的开始位置一定会在航线的结束位置的左边,所以不会出现”1 2“,”2 1“的情况。
4.2 添加A*算法
在4.1中,通过对航线顺序的优化,可以做到大部分航线能够在区域内,但还是有部分航线会越过区域边线飞到区域之外。所以从一条航线的结束点到下一条航线的开始点需要在区域内找出最短路径,目前使用A*算法来达到不飞到区域外的情况。关于A*算法的实现,将下下一篇文章中介绍。
4.3 减少过短航线
航线过短会让无人机在还没有加速时就要开始减速,影响无人机的作业效率。计算航线长度,判断航线长度是否小于给定的最短航线长度时,此航线将不加入到航线集合中。
4.4 航线缩进
航线缩进是为了保证无人机作业时不飞出作业区域,更是为了A*算法的实现提供实施基础。航线缩进的实现是以飞机的半径为准,所以航线的缩进要保证以飞机中心点画圆不能与区域有交点或仅有一个交点。