Ros

adjust_global_plan之a_star_search

makePlan使用的是静态地图+实时障碍层,这个静态地图会和此时地图有所不同,像同一坐标值出现偏差。换句话说,makePlan中的LETHAL_OBSTACLE栅格,在此时地图可能变成INSCRIBED_INFLATED_OBSTACLE。

在ros官方设计中,makePlan是通过膨胀层(内切圆半径,图1青色格子),使得得到的路径考虑了机器人足迹。但我把青色格子语义变成了相邻障碍的危险栅格,使得makePlan得到的是没考虑机器人足迹的路径。这里需得到一条考虑了足迹的local_path。

图1 红色是调整过的local_path

 

二、a_star_search

a_star_search用于搜出一条从src到origin_dst的最佳路径。针对图2示例,源栅格src是(28, 28),目标栅格dst是(39, 10),要通过A*算法搜出一条从(28, 28)到(39, 39)的最“佳”路径。

 

  1. 将起始结点src放入OPEN表pq。
  2. 如果pq表不为空,从表头取一个结点n,如果为空算法失败
  3. n是目标解吗?1)不是,进入步骤4。2)是,成功找到一个解。算法结束。
  4. 展开和n相邻的8个结点。对每个衍生结点next,坐标记为i_loc,挨个进行下面操作。
    4.1. 用代价函数算出i_loc上代价,记为cost。
    4.2. cost加上n.g得到next.g,如果这个next.g比“上次”更好,就将它们放入pq,并把pq按代价排序,最小的放在表头,这个最“佳”节点将作为下一次判断的当前结点。
  5. 处理完8个next后,转去步骤2。

算法只使用OPEN表,没有CLOSE表。变量pq表示OPEN表,它是个有序表,每个单元表示一个结点n,按着n.t进行排序,表示起始结点若经过它,到达dst需要的代价t(n)。

如果orign_dst就是障碍物栅格(LETHAL_OBSTACLE),那a_star_search就不可能找到src到origin_dst路径。考虑到足迹要占用数个栅格,这时orign_dst在内切圆半径的那些栅格也不可能是dst。加上adjust_global_plan使用a_star_search只是要找到一条更优路径,不要求一定那到达原local_path的目标点。变量sq_max_stop_dist表示这个dst到origin_dst的容许距离,为减少开根号次数,前缀“sq_”表示这个平方值。

2.1 a_star_search源码

  • src。起始结点。
  • origin_dst。目标结点。变量名没用dst,是因为最终得到的路径结点可能不是origin_dst。在函数中,真正的目标结点才记为dst。
  • stop_at。失败阈值。不断搜索,最优结点n.g也已超过这值了,认为此次搜索失败。
  • calc。代价函数。既不是g(n),也不是h(n),是计算机器人在栅格n上的代价。对栅格n上代价,会立刻联想到可直接得到某个栅格代价的costmap。可机器人面积往往大于resolution*resolution,那它在该栅格上代价就是以n作为机器人中心点、包括其它涉及到栅格的某种代价“和”。至于怎么得到“涉及到栅格”,可借助机器人的内切圆半径。目前代价函数:以n为中心,涉及到的((cell_inscribed_radius - 1) * 2 + 1) * ((cell_inscribed_radius - 1) * 2 + 1)个栅格的平均代价,即这些栅格的代价和除以个数。
  • costmap。局部代价地图。
  • cell_inscribed_radius。以栅格数表示的内切圆半径。示例:3。
  • exclusion_rect。一个矩形形状的禁止区域。旧路径已有一定形状,新路径只可能沿着它进行优化。对图1,机器人坐标系下,路径应该基本在第4象限,或可能进第1象限。这时可以把位在第2象限的{0, 40, 20, 20}设为禁止区域。这一来避免让机器人在进这区域进行寻路,二来可以减少点计算量。
plain_route a_star_search(const SDL_Point& src, const SDL_Point& origin_dst, double stop_at, 
	const cost_calculator& calc, const costmap_2d::Costmap2D& costmap,
	int cell_inscribed_radius, const SDL_Rect& exclusion_rect)
{
	const int width = costmap.getSizeInCellsX();
	const int height = costmap.getSizeInCellsY();
	//----------------- PRE_CONDITIONS ------------------
	VALIDATE(width > 0, null_str);
	VALIDATE(width == height, null_str);
	VALIDATE(cell_inscribed_radius > 0, null_str);
	//---------------------------------------------------

	if (stop_at == 0) {
		stop_at = calc.default_stop_at(width);
	}

	// A* search: $src -> $dst
	SDL_Point dst = origin_dst; // dst maybe change
	int inscribeds;

	double sq_max_stop_dist = -1; // -1
	double dst_cost = calc.cost(dst, inscribeds);
	// calculate dst's cost, must use block.
	if (dst_cost == calc.NoPathValue || dst_cost == calc.NoInformation) {
		sq_max_stop_dist = (cell_inscribed_radius + 1) * (cell_inscribed_radius + 1);

	} else if (dst_cost == calc.InscribedInflatedObstacle) {
		sq_max_stop_dist = (cell_inscribed_radius - 1) * (cell_inscribed_radius - 1);	
	}

根据目标结点origin_dst不同情况,算出不一样的容许距离sq_max_stop_dist。

	costmap_2d::MapLocation locs[8];

	// increment search_counter but skip the range equivalent to uninitialized
	search_counter += 2;
	if (search_counter - bad_search_counter <= 1u) {
		search_counter += 2;
	}

全局变量search_counter加2。而且要避免出现0(bad_search_counter)或1。后文“search_counter和in字段”有解释search_counter。

	reallocate_pq(width, height);
	static std::vector nodes;
	nodes.resize(width * height);  // this create uninitalized nodes

	indexer index(width, height);
	comp node_comp(nodes);

	nodes[index(dst)].g = stop_at + 1;

1/3[dst.g]:设置[dst.g]的初值为stop_at + 1。

	nodes[index(src)] = node(0, src, SDL_Point{nposm, nposm}, dst, costmap);

	// rember destination cost.
	// is destination is all, don't remember! make below code find out right path.
	// [see remark#40]
	hash[index(dst)].in = search_counter;
	hash[index(dst)].cost = dst_cost;

	std::vector<int> pq;
	pq.push_back(index(src));

	const int costmap_cells = width * height;
	while (!pq.empty()) {
		node& n = nodes[pq.front()];

		n.in = search_counter;

		std::pop_heap(pq.begin(), pq.end(), node_comp);
		pq.pop_back();

		if (n.t >= nodes[index(dst)].g) {
			break;

除pq.empty()外,只有这里能跳出while,进入这里有多种情况。

  1. 寻路成功。3/3[dst.g]:经过“2/3[dst.g]”,dst.g已由初值“stop_at + 1”改为next那时算出的“cost”,n正好是那时的next,这个等式自然成立。
  2. 寻路失败。dst.g保持着初值stop_at + 1,但最佳节点n.t就已经超过它了。最佳都这样,那不必再找了。

为什么用n.t,而不是n.g?第一种情况寻路成功时,之前已经过“2/3[dst.g]”,用n.t还是n.g没区别。若是第二种情况寻路失败,用n.t有助于更快中止寻路。“快”是什么速度取决于n.h,即heuristic(...)算出的代价。

		}

		// last_location = n.curr;
		const int count = costmap.getMax8Cells(n.curr.x, n.curr.y, locs);
		for (int i = 0; i < count; i++) {
			const int i_index = index(locs[i].x, locs[i].y);
			VALIDATE(i_index >= 0 && i_index < costmap_cells, null_str);

			const SDL_Point i_loc{(int)locs[i].x, (int)locs[i].y};
			if (SDL_PointInRect(&i_loc, &exclusion_rect)) {
				continue;
			}
			node& next = nodes[i_index];

			double threshold = (next.in - search_counter <= 1u) ? next.g : stop_at + 1;
			// cost() is always >= 1  (assumed and needed by the heuristic)
			if (n.g + 1 >= threshold) {
				continue;

进入这里分两种情况。

  1. next已经被某个n衍生到过,next.g是那次算出的g。这里要注意一个规则,对一个节点next,最多可能从8个方向衍生到它。当被衍生到一次后,可能还有第二次、第三次。程序会保证每个栅格上代价至少是1。进入这里意味着此次衍生肯定不会比上次更好了,没必要再考虑next。
  2. next没被衍生到过,但n.g已经是stop_at,到它就超过失败阈值,没必要再考虑next。

由于stop_at往往设的是一个较大值,能进入这里往往的是第一种情况。

			}

			double cost;
			if (hash[i_index].in == search_counter) {
				cost = hash[i_index].cost;

之前已算过next在自个坐标上cost,从hash查找表拿就行。

			} else {
				cost = calc.cost(i_loc, inscribeds);
				{
					hash[i_index].in = search_counter;
					hash[i_index].cost = cost;
				}

之前没算过next在自个坐标上cost,用代价函数进行计算,并把结果存入hash查找表。

			}
图2 一次a_star_search寻路

变量n、next表示节点,它们类型是node、其中的t、g、h分别表示f(n)=g(n)+h(n)中的f、g、h计算出的double值。注:t、g、h默认值是1e25,即10的25次方,见Watch2中保留着初始值的next。

n是当前正在处理的节点,它总是此刻认为能找到目标解的最佳节点。当然,此刻它已离开pq。

next是由n衍生出的节点,即n相邻8个节点中的一个。接下就会把它的t、g、h赋值,并放入pq

			cost += n.g;
			// double cost = n.g + calc->cost(locs[i], n.g);
			if (cost >= threshold) {
				continue;
			}

			// may change dst?
			int x_diff = locs[i].x - origin_dst.x;
			int y_diff = locs[i].y - origin_dst.y;
			const int sq_dist = x_diff * x_diff + y_diff * y_diff;
			if (sq_dist <= sq_max_stop_dist) {
				dst.x = locs[i].x;
				dst.y = locs[i].y;
				// why -0.1? --make late sq_dist not enter here. 
				sq_max_stop_dist = sq_dist - 0.1;
			}

			bool in_list = next.in == search_counter + 1;

			next = node(cost, i_loc, n.curr, dst, costmap);

2/3[dst.g]:如果next恰好是dst,那会修改dst.g,值变成这里算出的cost。

			if (in_list) {
				std::push_heap(pq.begin(), std::find(pq.begin(), pq.end(), i_index) + 1, node_comp);
			} else {
				pq.push_back(i_index);
				std::push_heap(pq.begin(), pq.end(), node_comp);
			}
		}
	}

	plain_route route;
	if (nodes[index(dst)].g <= stop_at) {

		route.move_cost = static_cast<int>(nodes[index(dst)].g);
		for (node curr = nodes[index(dst)]; curr.prev.x != nposm; curr = nodes[index(curr.prev)]) {
			route.steps.push_back(curr.curr);
		}
		route.steps.push_back(src);
		std::reverse(route.steps.begin(), route.steps.end());
	} else {
		// aborted a* search
		route.move_cost = static_cast<int>(calc.NoPathValue);
	}

	release_pq();
	return route;
}

 

2.2 push_heap、pop_heap

参考“STL中heap相关函数的用法:make_heap,push_heap,pop_heap,sort heap”。

使用push_heap、pop_heap,目的是确保pq这个数组是个有序open表。pq这个数组的单元值是int,表示结点索引,越往后,其对应结点的t(g+h)越大。

push_heap

用于将堆底单元加入已排序堆结构中。

因为make_heap只能建堆,如果当前堆数据发生改变,就需要使用push_heap重回大堆/小堆。值得注意:first 到 last-1 之间的元素必须满足堆结构。它仅仅是将last之前元素插入堆中。意思就是,如果一次性插入多个元素,它只会把最后一个元素(堆底)加入堆结构中。

其参数与make_heap相同,不再赘述。

pop_heap

pop_heap(_RAIter,_RAIter) 默认为大顶堆。该函数作用是“删除”堆顶元素。为什么加引号——因为就像堆排序那样,所谓删除只是将堆顶元素移至堆底。

pop_heap(_RAIter,_RAIter,_Compare) _Compare有两种参数,一种是greater(小顶堆),一种是less(大顶堆)

比如pop_heap(nums.begin(), nums.end(),greater<int>()),它会将堆顶元素(即为数组第一个位置)和数组最后一个位置对调,然后你可以调用数组pop_back,删除这个元素

注意,pop_heap中的_Compare和make_heap中的_Compare参数必须是一致的,不然会失败

 

2.3 hash表

为什么要用hash?——计算i_loc上的cost较花时间。而在此次寻路过程中,这个cost是不会变的。那把i_index->cost做成一个查找表,这个查找表就是hash。一旦i_index已算出过cost,通过hash,下次以i_index为key就可搜到cost。

 

2.3 search_counter和in字段

search_counter是个全局变量,有着以下特点。

  1. 类型是uint32_t。
  2. 只会是偶数,但不会是0。
  3. 每次进入a_star_search时,会加2。之后在a_star_search期间,值就不会变了。所以虽然初值是0,但在a_start_search看来,可认为初值是2。
  4. 不断执行a_star_search,每次就会加2,迟早会到达uint32_t最大值,然后回转,这时会跳过0,进入2。

存在两种in字段:nodes[i_index].in和hash[i_index].in,类型和search_count一样,都是uint32_t。

nodes[i_index].in

nodes类型是std::vector<node>,为提高效率,把它作为全局变量。以下是一次a_star_search过程中,in和search_counter关系。

  1. 第一次进入a_star_search时in会置为0。之后就是上次a_star_search之后是什么值,此次就是什么值。
  2. 访问过、并放入pq表的结点会被置为in=search_counter+1。这里访问指的是src和衍生。
  3. 该结点移出pq表、要用于分析了,会被置为in=search_counter。

in变量用于判断某一栅格在此次a_star_search时是否已被访问过,判断代码是“(in - search_counter <= 1u)”,返回true意味着已访问过,否则没有。此次a_star_search没被访问过时,in值是不确定的,这个式子中in可能比search_counter小,但因为是无符号数减法,判断还是返回false,结论是没访问过。举个例子,in是11,search_counter是12,相减后的值4294967295(0xffffffff),不满足“<=1u”。

要满足“(in - search_counter <= 1u)”,意味着in必须大于等于search_counter,大于时差值只能是1。上面已说过,这两种情况都表示该栅格已访问过,区别在于,1时该结点没在pq表,0则正在pq表。

对in变量,要标志node是否访问过,用个bool变量不是更简单,干吗要in又要search_counter,这不是增加复杂度吗?——这么做原因是nodes是个静态变量,nodes中的元素数是地图格子数,用in、search_counter是为省去第“二”次及以后的初始化nodes时间。因为不初始化nodes,第N+1次a_star_search用的是第N次操作后形成的nodes,N+1次的in值自然也要基于N次in值,而那个in值将“小于”此次search_couter。

和in状态相关的另一个可能疑问,函数每次搜索前会调用nodes.resize,nodes.resize会不会把所有in复位为0?——不会。以下是resize的函数实现。

void resize(size_type new_size) { resize(new_size, T()); }
void resize(size_type new_size, const T& x) 
{
	if (new_size < size()) {
		erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以外的数据无效
	} else {
		insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区间内的数据有效
	}
}

hash[i_index].in

  1. 初始时全是search_counter - 2。reallocate_hash(...)负责初始化这些值。
  2. 该栅格计算过代价,被赋值上值后,变成search_counter。
  3. 不会出现search_counter+1。用“hash[i_index].in == search_counter”来判断此次是否被赋过值。

现在hash其实可认为就是函数内变量。要表示hash[i_index]是否计算过代价,用个bool变量就行了。用in、search_counter,是在想将来或许会把hash作为全局变量吧。

全部评论: 0

    写评论: