前面介绍了ROS计算全覆盖路径的整体流程,在完成区间分割之后,就需要计算区间之间的遍历顺序,本篇会对这部分的代码功能做一个分析记录。
计算各区间的遍历顺序,本质上可以看作是一个TSP问题(即旅行商问题),只不过不要求回到起点位置,故可以用求解TSP的方式求出各个区间的遍历顺序。求解TSP的方式分为两类:一类是求出最优解,另一种则是求出近似解。由于TSP属于NP完全问题,而且一般应用在智能扫地机上面的算法板算力并不高,故一般在实际应用中,不会要求求出最优解,能得到一个相对较合理的近似解即可。
ROS中调用计算TSP顺序的代码如下:
std::vector<int> optimal_order;
if (cell_visiting_order == OPTIMAL_TSP)
{
GeneticTSPSolver tsp_solver;
optimal_order = tsp_solver.solveGeneticTSP(rotated_room_map, polygon_centers, 0.25, 0.0, map_resolution, start_cell_index, 0);
if (optimal_order.size()!=polygon_centers.size())
{
std::cout << "=====================> Genetic TSP failed with 25% resolution, falling back to 100%. <=======================" << std::endl;
optimal_order = tsp_solver.solveGeneticTSP(rotated_room_map, polygon_centers, 1.0, 0.0, map_resolution, start_cell_index, 0);
}
}
else if (cell_visiting_order == LEFT_TO_RIGHT)
{
...//此处省略
}
这段代码在前面有简单分析过,ROS默认使用的计算TSP遍历顺序的算法是遗传算法,且会先将地图缩放0.25倍后进行计算。本篇中,主要讲另一种求解TSP遍历顺序的方式,其源码位置为autopnp/ipa_building_navigation/common/include/ipa_building_navigation/nearest_neighbor_TSP.h,这是一种计算最邻近区域的方式。算法思想很简单,每次都取离当前位置最近的区域为下一个清扫区域,到达下一个区域后,再取最近的区域为下一个清扫区域。
该方法的函数的接口定义如下:
// compute TSP and distance matrix without cleaning it
// this version does not exclude infinite paths from the TSP ordering
std::vector<int> solveNearestTSP(const cv::Mat& original_map, const std::vector<cv::Point>& points, double downsampling_factor,
double robot_radius, double map_resolution, const int start_node, cv::Mat* distance_matrix=0);
original_map 是经过预处理以及角度旋转后的地图数据,用来计算点跟点之间的路径长度;points 是前面做区间分割的时候,计算得出的每一个区间的中心点坐标数组;downsampling_factor 则是地图的缩放比率,即把传入的地图按照给定的缩放比率进行缩放,在缩放后的地图上寻找路径,需要注意的是,这种方式虽然用来处理大地图的时候,可以提高计算速度,但是存在一个隐患:当地图中存在窄通道时,缩放后该通道会闭合,从而导致两个区间不连通无法计算出区间距离;robot_radius 是机器人的半径;map_resolution 是栅格地图的分辨率;start_node 为初始区间序号,通过机器人当前坐标算出距离最近的区间;distance_matrix 则是用来返回距离矩阵。函数具体实现如下:
std::vector<int> NearestNeighborTSPSolver::solveNearestTSP(const cv::Mat& original_map, const std::vector<cv::Point>& points,
double downsampling_factor, double robot_radius, double map_resolution, const int start_node, cv::Mat* distance_matrix)
{
std::cout << "NearestNeighborTSPSolver::solveNearestTSP: Constructing distance matrix..." << std::endl;
//calculate the distance matrix
cv::Mat distance_matrix_ref;
if (distance_matrix != 0)
distance_matrix_ref = *distance_matrix;
DistanceMatrix distance_matrix_computation;
distance_matrix_computation.constructDistanceMatrix(distance_matrix_ref, original_map, points, downsampling_factor, robot_radius, map_resolution, pathplanner_);
return solveNearestTSP(distance_matrix_ref, start_node);
}
distance_matrix_ref 是一个二维的距离矩阵,该矩阵保存各个区间的两两距离,在进行距离计算时,只需计算二维矩阵对角右上方的两点距离,计算完毕即可得出所有的区间之间的最短距离。
最短距离搜索策略则是:
(1)先将两点之间连一条直线,计算该直线上是否穿过障碍物,假如没有穿过障碍物,则直线距离即为两点之间的最短距离;
(2)假如两点之间的连线穿过障碍物,则使用A*算法计算出两点之间的最短距离。
计算生成距离矩阵后,就可以通过距离矩阵,计算得出最邻近点的遍历序列。这部分的代码如下:
std::vector<int> NearestNeighborTSPSolver::solveNearestTSP(const cv::Mat& path_length_matrix, const int start_node)
{
std::vector<int> calculated_order; //solution order
if(path_length_matrix.rows > 1) //check if clique has more than one member or else this algorithm produces a order of size=3
{
int last_node; //index of the last spectated node
std::vector<bool> visited(path_length_matrix.rows, false);
int current_node = start_node; //index of the current spectated node
calculated_order.push_back(current_node);
visited[current_node] = true;
//check every Point for the next nearest neighbor and add it to the order
do
{
int next_node; //saver for next node
double min_distance = 1e100; //saver for distance to current next node
for (int current_neighbor = 0; current_neighbor < path_length_matrix.cols; current_neighbor++)
{
if (visited[current_neighbor]==false) //check if current neighbor hasn't been visited yet
{
const double length = path_length_matrix.at<double>(current_node, current_neighbor);
if (length < min_distance && length > 0)
{
next_node = current_neighbor;
min_distance = length;
}
}
}
calculated_order.push_back(next_node); //add the found nearest neighbor to the order-vector
visited[next_node] = true;
current_node = next_node;
} while (calculated_order.size() < path_length_matrix.rows); //when the order has as many elements as the pathlength Matrix has the solver is ready
}
else
{
calculated_order.push_back(start_node);
}
return calculated_order;
}
需要注意的是,这段代码实现存在一个隐患,上面已经讲到,调用该接口默认会将地图进行缩放,缩放地图可能就导致某一个区间与其他区间不连通,而这段代码里面没有对 next_node 进行初始化,假设由于缩放导致初始区间跟其他所有区间都不连通,则在执行完 for 循环后,next_node 的值是一个随机值,从而可能导致运行到 visited[next_node] = true;
计算得出各区间的遍历顺序后,后面就会按照遍历顺序,依次生成各个区间内弓字形覆盖路径,这个具体实现以及存在的一些问题后续再补充。