A*算法项目实践之一:栅格法的使用与障碍物栅格的生成
使用路径搜索算法(本文使用A*算法)的第一步就是将地图用程序中的某些逻辑来表示,本文就笔者实际项目的一些经验来谈谈相关的方法,若有不对,请在评论区提醒笔者予以斧正 。
本项目基于VS2017,整个项目的代码以及上传到码云: A*算法项目实践(正在更新中~)
栅格形状、大小的确定
百度百科对栅格数据的定义:栅(shān)格数据就是将空间分割成有规律的网格,每一个网格称为一个单元,并在各单元上赋予相应的属性值来表示实体的一种数据形式。每一个单元(像素)的位置由它的行列号定义,所表示的实体位置隐含在栅格行列位置中,数据组织中的每个数据表示地物或现象的非几何属性或指向其属性的指针。
在实际的路径搜索问题,将搜寻的区域(一般为二维地图,形状不定)分割为有规律的栅格,每个栅格称为一个单元,每个栅格的位置由其行和列决定,这样就简化搜索区域为 2 维数组,数组的每一项代表一个栅格,它的状态就是可走 (walkalbe) 和不可走 (unwalkable),所谓的可走可以理解为该栅格没有障碍物——算法可拓展到该栅格,不可走可以理解为该栅格有障碍物——算法不能拓展到该栅格
那么如何确定栅格的大小和形状呢?
我们先确定几个原则:(1)很自然的,我们可以想到,用栅格的二维数组来表示搜索的区域,最起码应该能够保证栅格地图的大小与搜寻区域的大小是一致的,若栅格地图比搜索区域小——栅格地图没有正确表示实际搜寻区域,搜索算法可能失去某些可行解;若栅格地图比搜索区域大——栅格地图超出了实际搜索区域,搜索算法可能得到某些错误解,同理,我们也需要用栅格集合表示搜索区域的障碍物和移动的目标物,若整数个栅格表示的区域比障碍物要大,那么我们用了多余的区域表示障碍物——相对减少了实际可行的区域,若整数个栅格表示的区域比障碍物要小,那么我们没有正确表示障碍物——相对减少了搜索出的路径的准确度,综上,原则(1)为力求整数个栅格能够覆盖区域、障碍物和目标物。
(2)当搜索的区域确定时,我们要想到——栅格越小,搜索的区域就需要用更多栅格表示,那么搜索算法的问题空间就越大,计算时间就越长,因此不难得出原则(2)栅格的尺寸应尽可能的大。
原则(1)(2)用一句话可以概括为:
栅格的大小、形状是根据搜寻的区域、障碍物和移动的目标物形状来确定的,力求整数个栅格能够覆盖区域、障碍物和目标物,而且栅格的尺寸应尽可能的大。
举例来说,测试中的搜寻区域形状为矩形,大小为72m * 18.2,障碍物两种:形状都为矩形,大小分别为9.8m * 3.4m、8.2m*3.4,移动的目标物大小、形状与障碍物都相同。
在这种情况下,首先,因为搜寻区域、障碍物和移动的目标物的形状都为矩形,那么可以确定栅格的形状为正方形(规则四边形中最好选择正方形),其次,取72、18.2、9.8、3.4、8.2的最大公约数为0.2,因此可以确定正方形的边长为0.2m。
此外,为了满足原则(1),栅格可以为其它多边形而不是正方形,例如可以是六边形,矩形,甚至可以是任意多变形,但是,笔者认为栅格最好为正方形,因为容易计算与实现。
栅格地图的生成——障碍物的生成
栅格地图的生成需要所有障碍物信息,本项目中的障碍物为车(矩形),且能够得到障碍物的信息(位于项目中的Classes.h文件中)为:
//载具信息类
struct car_info
{
PPoint car_center;//中心点坐标
double car_length;//载具长
double car_width;//载具宽
double angle;//载具倾斜角
};
将障碍物对应的栅格置位的思路为:
0、计算车中心点坐标
1、根据长、宽和膨胀边界大小计算膨胀之后四个边界点的坐标;
2、取车边界四个点的栅格行列下标[_x_min,_x_max]、[_y_min,_y_max]范围;
2.1获得坐标的x、y的范围;
2.2转换坐标的x、y的范围得到栅格行列下标[_x_min,_x_max]、[_y_min,_y_max]范围;
2.3整理得到的栅格行列下标[_x_min,_x_max]、[_y_min,_y_max]范围范围,使其不越界;
3、在[_x_min,_x_max]、[_y_min,_y_max]范围之内的每一个栅格,判断其栅格中心点与车中心点之间的线段是否与车的边界线段相交;
4、将所有判断为不在外面的栅格置位;
重点:
问题1: 如何求一个障碍物覆盖的栅格——计算出被覆盖的栅格数组?
答:一个栅格被覆盖的情况有3种:(1)完全遮盖;(2)部分遮盖;(3)完全没有遮盖,毫无疑问,(1)类型的栅格需要被标记为1——不可走,(3)类型的栅格需要被标记为0——可走,然而,对于(2)类型的栅格,直观的办法是确定其被覆盖面积大小的占比是多少,若占比超过50%,则将其标记为1,否则标记为0,但是,计算一个正方形被遮盖的面积显而易见计算量较大,因此我们需要转化一下思路。
具体来说,在本项目中,障碍物为矩形,而一个矩形实质上是四条线段,一个栅格(正方形)可以用其中心点的坐标来表示,在解析几何中,线段与线段是否相交很容易判断,那么我们不妨用这种判定方法a:对某一个栅格,若障碍物矩形的中心点与栅格的中心点之间的线段与矩形的四个边不相交,那么该栅格就需要置为1,这种方式的计算量较少且思路非常清晰,但是,我们得着重考虑一下类型(2)的栅格,这部分的栅格可能满足上述判定方式,也有可能满足不了——这与它们被障碍物矩形遮盖的面积有关,但幸运的是,如果我们只是需要将类型(1)、(2)的栅格全部置为1(这一条不一定成立,请读者自行根据项目需求判断),那么问题就转换为了如何求得所有的(1)和(2)类型的栅格。
我们不妨将障碍物矩阵膨胀一下(长、宽各自加上一个值),然后再用上述判定方法a,如果膨胀的够大,理论上所有的(2)类型栅格都将通过判定方法a筛选出来——代价是“无辜”(本不该置为1)的栅格也被置为1;如果这种代价可以承受的话,那么何乐而不为呢?实际笔者使用的膨胀大小为正方形对角线长度的一半。
问题2:怎么减少求一个障碍物覆盖的栅格的计算量?
答:我们不能对搜索区域中的每一个栅格都进行判定,那样计算量很大,只需要对障碍物附近的栅格进行判断,如何确定附近栅格呢?——取障碍物矩形边界四个角的栅格行列范围:[_x_min,_x_max]、[_y_min,_y_max],即得到行、列的最大最小值,在这2个范围内使用2个for循环来遍历障碍物附近所有的栅格。
对应的功能函数Caculate_SetGridesOfCar(位于项目的Calculate.h和Calculate.c文件中):
#include "Calculate.h"
#include "Classes.h"
#include <cmath>
using namespace std;
//************************************
// Method: CalculateCarCenterPoint
// FullName: CalculateCarCenterPoint
// Access: public
// Returns: void
// Qualifier:根据载具长、宽、左后方点、倾斜角度计算载具中心点坐标
// Parameter: double car_length 车长
// Parameter: double car_width 车宽
// Parameter: PPoint & left_back 左后方点坐标
// Parameter: double angle 倾斜角 非弧度角 [0,360)
// Parameter: PPoint & center 计算得到的中心点坐标
//************************************
void CalculateCarCenterPoint(double car_length, double car_width, PPoint &left_back, double angle, PPoint ¢er)
{
//角度为负值,错误输入,返回
if (angle < 0) return;
//角度转换
angle = angle / 180 * 3.1415926;
center.x = left_back.x + cos(angle)*car_length / 2 + sin(angle)*car_width / 2;
center.y = left_back.y - cos(angle)*car_width / 2 + sin(angle)*car_length / 2;
}
//************************************
// Method: CaculateGridesOfCar
// FullName: CaculateGridesOfCar
// Access: public
// Returns: void
// Qualifier:计算障碍物遮盖的栅格并将对应的栅格置位
// Parameter: double car_length 车长
// Parameter: double car_width 车宽
// Parameter: PPoint & left_back 车左后点的直角坐标
// Parameter: double angle 倾斜角度 非弧度角 [0,360)
// Parameter: double expand_boundary膨胀边界
// Parameter: std::vector<std::vector<int>> & maze 栅格地图
//************************************
void Caculate_SetGridesOfCar(double car_length, double car_width, PPoint left_back, double angle, double expand_boundary, std::vector<std::vector<int>>& maze)
{
//0、计算车中心点坐标
PPoint car_center