ROS-Navigation Move_base 源码阅读学习--恢复行为recovery_behavior(旋转恢复行为、代价地图清理恢复行为) - 教程
在ROS的move_base导航栈中,恢复行为是确保系统鲁棒性的关键组件。它定义了一组当机器人无法正常前进时所执行的策略,旨在使机器人状态恢复到可继续执行全局规划路径的水平。
根据导航状态机的设计,在PLANNING状态失败后,系统会依次执行用户定义的恢复行为。move_base架构中的恢复行为类都是通过插件plugind的方式动态加载,派生类中主要对基类RecoveryBehavior中的initialize
和runBehavior
两个函数进行重载,已实现自己定义的恢复行为。
其中,move_base中定义了两个最为核心和常用的恢复行为:clear_costmap_recovery
和rotate_recovery
clear_costmap_recovery:通过清除局部/全局代价地图中指定层(如障碍物层)的信息,来解决因动态障碍物遗留、传感器噪点或代价地图膨胀区过大导致的“虚假堵塞”问题。
rotate_recovery:一种简单却极其有效的策略,通过控制机器人在原地旋转360度,利用其传感器重新感知周围环境,从而可能发现之前被遮挡的自由空间。
本篇博客将深入这两个恢复行为的实现原理。
旋转恢复行为 RotateRecovery
RotateRecovery的核心思路是:
- 让机器人原地 旋转一整圈(360°)。
- 这样它的传感器(激光雷达、深度相机等)可以扫描周围环境,刷新局部 costmap。
- 如果之前 costmap 上残留了错误的障碍物(比如动态障碍物已经走开),这个旋转动作会帮助清理。
核心代码:
double dist_left;
if (!got_180) // 转到180°
{
// 先强制让机器人至少旋转半圈(起点朝向 + 180°)
// distance_to_180表示还差多少角度到180°
double distance_to_180 = std::fabs(angles::shortest_angular_distance(current_angle, start_angle + M_PI));
// 再继续旋转直到回到起点
dist_left = M_PI + distance_to_180;
if (distance_to_180 < tolerance_)
{
got_180 = true;
}
}
else // 回到起点角度
{
// 此时已经旋转半圈,计算当前角度与起点角度的差值
dist_left = std::fabs(angles::shortest_angular_distance(current_angle, start_angle));
}
double x = global_pose.pose.position.x, y = global_pose.pose.position.y;
double sim_angle = 0.0;
while (sim_angle < dist_left)
{
double theta = current_angle + sim_angle;
// 每隔sim_granularity_ 检查一次 footprint 是否会碰撞
double footprint_cost = world_model_->footprintCost(x, y, theta, local_costmap_->getRobotFootprint(), 0.0, 0.0);
if (footprint_cost < 0.0)
{
ROS_ERROR("Rotate recovery can't rotate in place because there is a potential collision. Cost: %.2f",
footprint_cost);
return; // 发生碰撞,直接退出
}
sim_angle += sim_granularity_;
}
// 计算旋转速度,确保能在剩余角度内停下
double vel = sqrt(2 * acc_lim_th_ * dist_left);
// 限制在最小、最大旋转速度范围内
vel = std::min(std::max(vel, min_rotational_vel_), max_rotational_vel_);
这里仔细思考,为什么要分成先假装转180,然后再回到起点角度?如果单纯判断"还没有转360°",会出现一个问题:机器人转过一半的时候,当前角度和起点角度刚好相差 180°,这时候它其实有两个候选方向能继续往下走(顺时针 / 逆时针),可能会引发歧义。
所以代码里采用了两阶段的一个技巧,使得机器人可以保证确实转了一圈:
- 阶段一:先强制机器人至少转到起点角度+180°
- 阶段二:再继续旋转直到回到起点角度
对于为什么旋转恢复行为是转一圈,而不是转半圈或者其他圈数呢?
这是因为一整圈的扫描可以让局部代价地图刷新,尤其是对动态障碍物(人走开、椅子被挪动等)。半圈是无法照顾到的,多圈也没必要,显得很呆。
地图清理恢复行为 ClearCostmapRecovery
ClearCostmapRecovery 的本质就是在机器人位置附近定义一个窗口,然后根据参数选择清理 costmap(全局/局部)的障碍物层,清理范围可以是窗口内或外,从而“重置”代价地图,帮助机器人恢复可行的导航环境。
ClearCostmapRecovery::runBehavior
if (!invert_area_to_clear_){ // 清理内部区域
ROS_WARN("Clearing %s costmap%s outside a square (%.2fm) large centered on the robot.", affected_maps_.c_str(),
affected_maps_ == "both" ? "s" : "", reset_distance_);
}else {
ROS_WARN("Clearing %s costmap%s inside a square (%.2fm) large centered on the robot.", affected_maps_.c_str(),
affected_maps_ == "both" ? "s" : "", reset_distance_);
}
ros::WallTime t0 = ros::WallTime::now();
// 清理全局代价地图
if (affected_maps_ == "global" || affected_maps_ == "both")
{
clear(global_costmap_);
if (force_updating_)
global_costmap_->updateMap(); // 立即刷新全局地图
ROS_DEBUG("Global costmap cleared in %fs", (ros::WallTime::now() - t0).toSec());
}
t0 = ros::WallTime::now();
// 清理局部代价地图
if (affected_maps_ == "local" || affected_maps_ == "both")
{
clear(local_costmap_);
if (force_updating_)
local_costmap_->updateMap(); // 立即刷新局部代价地图
ROS_DEBUG("Local costmap cleared in %fs", (ros::WallTime::now() - t0).toSec());
}
ClearCostmapRecovery::clear
获取 costmap 的所有 Layer 插件,如果该 Layer 在 clearable_layers_ 集合中(可被清理),并且确实是 CostmapLayer 类型,那么调用clearMap()
for (std::vector >::iterator pluginp = plugins->begin(); pluginp != plugins->end(); ++pluginp) {
boost::shared_ptr plugin = *pluginp;
// 去掉namespace前缀
std::string name = plugin->getName();
int slash = name.rfind('/');
if( slash != std::string::npos ){
name = name.substr(slash+1);
}
if(clearable_layers_.count(name)!=0){ // 判断该层是否在可清理层集合中
// 判断该层是否是CostmapLayer
if(!dynamic_cast(plugin.get())){
ROS_ERROR_STREAM("Layer " << name << " is not derived from costmap_2d::CostmapLayer");
continue;
}
boost::shared_ptr costmap;
costmap = boost::static_pointer_cast(plugin);
clearMap(costmap, x, y);
}
}
ClearCostmapRecovery::clearMap
计算以机器人为中心、边长为 reset_distance 的正方形区域。如果 invert_area_to_clear=false
,那么清理正方形外部区域;为true,则清理正方形内部区域
void ClearCostmapRecovery::clearMap(boost::shared_ptr costmap,
double pose_x, double pose_y){
boost::unique_lock lock(*(costmap->getMutex()));
// 计算以机器人为中心的清理矩形区域,start_point(左下角)、end_point(右上角)
double start_point_x = pose_x - reset_distance_ / 2;
double start_point_y = pose_y - reset_distance_ / 2;
double end_point_x = start_point_x + reset_distance_;
double end_point_y = start_point_y + reset_distance_;
int start_x, start_y, end_x, end_y;
// 将世界坐标转化为地图索引坐标
costmap->worldToMapNoBounds(start_point_x, start_point_y, start_x, start_y);
costmap->worldToMapNoBounds(end_point_x, end_point_y, end_x, end_y);
// 清除区域
costmap->clearArea(start_x, start_y, end_x, end_y, invert_area_to_clear_);
double ox = costmap->getOriginX(), oy = costmap->getOriginY();
double width = costmap->getSizeInMetersX(), height = costmap->getSizeInMetersY();
// 更新边界,保证代价地图线程能刷新变化
costmap->addExtraBounds(ox, oy, ox + width, oy + height);
return;
}
我已经将代码的注释开源,需要的自取(目前还在更新)。如果感觉对你有帮助,请点一个star,谢谢。