move_base分析(五) planService和makePlan函数

move_base源码没有获取机器人当前位姿的地方,这属于局部路径算法的范畴。只能获得全局目标的位姿

bool MoveBase::makePlan(const geometry_msgs::PoseStamped& goal, vector& plan)

此函数只在一个地方被调用,就是planThread()线程被唤醒之后开始规划路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
boost::unique_lock<costmap_2d::Costmap2D::mutex_t> lock(*(planner_costmap_ros_->getCostmap()->getMutex() ));
plan.clear();
if(planner_costmap_ros_ == NULL) {
log.error("Planner costmap ROS is NULL, unable to create global plan");
return false;
}
//get the starting pose of the robot
tf::Stamped<tf::Pose> global_pose;
if(!planner_costmap_ros_->getRobotPose(global_pose)) {
log.warn("Unable to get starting pose of robot, unable to create global plan");
return false;
}
geometry_msgs::PoseStamped start;
tf::poseStampedTFToMsg(global_pose, start);

// 这里就是 GlobalPlanner::makePlan 了
if(!planner_->makePlan(start, goal, plan) || plan.empty()){
ROS_WARN("[move_base] Failed to find a plan to point (%.2f, %.2f)",
goal.pose.position.x, goal.pose.position.y);
return false;
}
return true;

代码并不复杂, 最后实际就进入bool GlobalPlanner::makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, double tolerance, std::vector<geometry_msgs::PoseStamped>& plan)

这里要参考全局路径规划(二) makePlan


MoveBase::planService(nav_msgs::GetPlan::Request &req, nav_msgs::GetPlan::Response &resp)

req参数包含了起点和目标信息,这个服务回调函数的的核心是planner_->makePlan

  1. move_base server必须在inactive状态,否则不向下执行

  2. 判断global planner的costmap是否存在;若req给定机器人初始位姿则使用,否则使用getRobotPose获得机器人所在位置做初始位姿

  3. 调用clearCostmapWindows完成对机器人区域的clear, clear_radius由参数设置

  4. 调用 if(!planner_->makePlan(start, req.goal, global_plan) || global_plan.empty()) 这是完成plan计算的核心部分。判断这个调用是否成功,如果失败,则在目标区域附近搜索,多次更改req.goal的值,并重新调用makePlan;新的goal如下图:

各个可能位置之间的水平和竖直间距和req.tolerance正相关

如果还是失败,则此次路径规划无解。如果成功,直接break循环,把原来无法达到的目标点插入global_plan容器的最后,local planner一般会从新的目标点导航到原目标点.

  1. global_plan拷贝到resp.plan

MoveBase::clearCostmapWindows(double size_x, double size_y)

planService·用到了clearCostmapWindows函数,它只被调用了这一次,过程如下:

  • 通过planner_costmap_ros_->getRobotPose(global_pose); 获取在全局地图的global_pose
  • 以这个点为中心,找到以size_x和size_y为边长的矩形的四个顶点

  • 调用planner_costmap_ros_->getCostmap()->setConvexPolygonCost(clear_poly, costmap_2d::FREE_SPACE);完成对机器人所在区域的clear工作

  • 以上同样的操作在controller_costmap_ros上也操作一遍,这样globa costmap 和local costmap都已经在机器人所在的区域完成clear工作


障碍层 (一) 配置和常见问题

障碍层地图通过订阅传感器话题,将传感器输出的障碍物信息存进buffer(剔除过高、过远的点),在本层地图上将观测到的点云标记为障碍物,将传感器到点云的连线上的点标记为FREE_SPACE。最后在bound范围内,将本层地图合并到主地图上。

配置参数

通用代价地图中的障碍层常常是这样设置的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
obstacles:
enabled: true
obstacle_range: 2.0
raytrace_range: 5.0
track_unknown_space: true
combination_method: 1
observation_sources: laser_scan_sensor pointcloud2_sensor
laser_scan_sensor:
data_type: LaserScan
topic: /scan
marking: true
clearing: true
max_obstacle_height: 1.0
min_obstacle_height: -1.0

pointcloud2_sensor:
topic: /people_cloud
data_type: PointCloud2
sensor_frame: "/base_link"
obstacle_range: 5.0
raytrace_range: 5.0
observation_persistence: 5.0
marking: true
clearing: true
min_obstacle_height: 0.0
max_obstacle_height: 2.0

  • observation_persistence: 2.0. 如果设置为0,代价地图只考虑最近的观测数据

  • min_obstacle_height: 传感器读数的最小高度(以米为单位)视为有效。通常设置为地面高度。决定是否将地面的部分加入到代价地图,默认0

车周围实际没有障碍物,不同参数的情况如下:
min_obstacle_height=0.png
这种情况下,避障太保守了。
min_obstacle_height=0.1 部分地面加入代价地图.png
min_obstacle_height=0.2 地面未加入代价地图.png

  • max_obstacle_height: 默认2,单位米。插入代价地图的障碍物的最大高度,应当比机器人高度稍微高一点,应该是用于有机械臂的情况。设置为大于全局max_obstacle_height参数的值将会失效,设置为小于全局max_obstacle_height的值将过滤掉传感器上大于该高度以的点。

  • obstacle_range: 设置机器人检测障碍物的最大范围,意思是说超过该范围的障碍物,并不进行检测,只有靠近到该范围内才把该障碍物当作影响路径规划和移动的障碍物。对能否及早发现障碍物至关重要 ,跟车速也有关系。 如果车速0.6,obstacle_range设置为1.5,连正常避障都实现不了,至少也得3

  • raytrace_range: 在机器人移动过程中,实时清除代价地图上的障碍物的最大范围,更新可自由移动的空间数据。假如设置该值为3米,那么就意味着在3米内的障碍物,本来开始时是有的,但是本次检测却没有了,那么就需要在代价地图上来更新,将旧障碍物的空间标记为可以自由移动的空间

增大raytrace_rangeobstacle_range会增大数据量,影响代价地图刷新

  • inf_is_valid: 默认是false,但是对点云障碍无效,仅对雷达scan有效:
1
2
3
4
if (inf_is_valid)
{
ROS_WARN("obstacle_layer: inf_is_valid option is not applicable to PointCloud observations.");
}

障碍层加载过程.png
障碍层所用的参数:obstacles params

不能将近处障碍添加到代价地图


调来调去,发现min_obstacle_height还是要设置为-0.3比较合适,这时因为地面的原因,导致局部代价地图的四角都有了障碍,因此再降低obstacle_range避免这一现象
obstacle_range大于2.5.png

obstacle_range=2.2.png

局部代价地图中无法清除不在视野的障碍

costmap raytraces to clear out obstacles. 障碍物要想从地图上清除,它所占据的空间必须被新的观测看到。但是参数observation_persistence常常无效。

last_updated_ is updated on every new cloud that needs to be added to the buffer. purgeStaleObservations函数 checks the cloud’s time stamp with last_updated_. When we stop publishing a point cloud last_updated_ will never be updated. Hence the observations will not time-out and remain in the observation_list_. This may result in the following warning, when the robot drives away from the clouds in observation_list_: Sensor origin at (x, y) is out of map bounds. The costmap cannot raytrace for it

只适用于ROS2的navigation

将文件observation_buffer.cpp中的函数purgeStaleObservations中的last_updated_改为nh_->now(),会影响共享库liblayers.solibcostmap_2d.so

1
2
nav2_util::LifecycleNode::SharedPtr  nh
last_updated_(nh->now())


各种常用雷达
abstract Welcome to my blog, enter password to read.
Read more
(十三) optimizeTEB 4 图优化的过程

图优化 optimizeGraph

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// clear_after 调用时写死为false
bool TebOptimalPlanner::optimizeGraph(int no_iterations, bool clear_after)
{
if (!teb_.isInit() || teb_.sizePoses() < cfg_->trajectory.min_samples)
{
ROS_WARN("optimizeGraph(): TEB is empty or has too less elements. Skipping optimization.");
if (clear_after)
clearGraph();
return false;
}
// 头文件定义 boost::shared_ptr<g2o::SparseOptimizer> optimizer_;
// 开始图优化
optimizer_->setVerbose(cfg_->optim.optimization_verbose);
optimizer_->initializeOptimization();
int iter = optimizer_->optimize(no_iterations); // 优化结束

// 可以保存 海森矩阵
/* g2o::OptimizationAlgorithmLevenberg lm =
dynamic_cast<g2o::OptimizationAlgorithmLevenberg*>(optimizer_->solver());
lm->solver()->saveHessian("~/Matlab/Hessian.txt");*/
if (!iter)
{
ROS_ERROR("optimizeGraph(): Optimization failed! iter=%i", iter);
return false;
}
if (clear_after)
clearGraph();

return true;
}

再回到optimizeTEB,剩余部分是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
success = optimizeGraph(iterations_innerloop, false);
if (!success)
{
clearGraph();
return false;
}
optimized_ = true;
// 只在最后一次外循环时 compute cost vec
if (compute_cost_afterwards && i==iterations_outerloop-1)
computeCurrentCost(obst_cost_scale, viapoint_cost_scale, alternative_time_cost);

clearGraph();
weight_multiplier *= cfg_->optim.weight_adapt_factor;
// iterations_outerloop 结束

computeCurrentCost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void TebOptimalPlanner::computeCurrentCost( double obst_cost_scale, 
double viapoint_cost_scale, bool alternative_time_cost)
{
// check if graph is empty/exist, important if function is called
// between buildGraph and optimizeGraph/clearGraph
bool graph_exist_flag(false);
if (optimizer_->edges().empty() && optimizer_->vertices().empty())
{
// here the graph is build again, for time efficiency make sure to call this function
// between buildGraph and Optimize (deleted), but it depends on the application
buildGraph();
optimizer_->initializeOptimization();
}
else
graph_exist_flag = true;

optimizer_->computeInitialGuess();
// cost_ 在 TebOptimalPlanner构造函数里 初始化为 HUGE_VAL
cost_ = 0;
if (alternative_time_cost)
{
cost_ += teb_.getSumOfAllTimeDiffs();
/* we use SumOfAllTimeDiffs() here, because edge cost depends on
number of samples, which is not always the same for similar TEBs,
since we are using an AutoResize Function with hysteresis */
}
// now we need pointers to all edges -> calculate error for each edge-type
// since we aren't storing edge pointers, we need to check every edge
for (std::vector<g2o::OptimizableGraph::Edge*>::const_iterator it =
optimizer_->activeEdges().begin(); it!= optimizer_->activeEdges().end(); it++)
{
double cur_cost = (*it)->chi2();

if (dynamic_cast<EdgeObstacle*>(*it) != nullptr
|| dynamic_cast<EdgeInflatedObstacle*>(*it) != nullptr
|| dynamic_cast<EdgeDynamicObstacle*>(*it) != nullptr)
{
cur_cost *= obst_cost_scale;
}
else if (dynamic_cast<EdgeViaPoint*>(*it) != nullptr)
cur_cost *= viapoint_cost_scale;
else if (dynamic_cast<EdgeTimeOptimal*>(*it) != nullptr && alternative_time_cost)
{
// skip these edges if alternative_time_cost is active
continue;
}
cost_ += cur_cost;
}
// delete temporary created graph
if (!graph_exist_flag)
clearGraph();
}

相应的有个TebOptimalPlanner::getCurrentCost()返回每次图优化计算的代价,而它又用于HomotopyClassPlanner::computeCurrentCostHomotopyClassPlanner::selectBestTeb()

  • obst_cost_scale Specify extra scaling for obstacle costs (only used if compute_cost_afterwards is true)

  • viapoint_cost_scale Specify extra scaling for via-point costs (only used if compute_cost_afterwards is true)

  • alternative_time_cost Replace the cost for the time optimal objective by the actual (weighted) transition time (only used if compute_cost_afterwards is true)

总结

optimizeTEB包括两个循环:

  1. 通过调用TimedElasticBand::autoResize(),外循环resizes the trajectory

  2. optimizeGraph()是内循环部分,调用solver(使用g2o的sparse Levenberg-Marquardt方法) 遍历一定次数的optimization calls

  3. 内外循环次数的比例defines the contraction behavior and convergence rate of the trajectory optimization. 2~6个内循环足够了

  • TebOptPlannerContainer tebs_; : Container that stores multiple local teb planners (for alternative equivalence classes) and their corresponding costs

The number of outer loop iterations should be determined by considering the maximum CPU time required to match the control rate.

Optionally, the cost vector can be calculated by specifyingcompute_cost_afterwards, the cost vector can be accessed afterwards using getCurrentCost(). 目前使用TebOptimalPlanner不会计算代价,使用HomotopyClassPlanner会,第一次计算时,代价是一个很大很大的数,第二次减小很多,之后逐渐减小到1左右或者更小


代价地图的层(一) 概述

所有的层都是继承Layer类,这个类本身比较简单,是一个虚基类,定义了两个重要接口:
updateBoundsupdateCosts

1
2
3
4
5
6
7
8
9
10
11
12
13
// 每层的初始化
void Layer::initialize(LayeredCostmap* parent, std::string name,
tf::TransformListener *tf)
{
layered_costmap_ = parent;
name_ = name;
tf_ = tf;
onInitialize(); //虚函数,空
}
const std::vector<geometry_msgs::Point>& Layer::getFootprint() const
{
return layered_costmap_->getFootprint();
}

调用在Costmap2DROS的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (!private_nh.hasParam("plugins") )
resetOldParameters(private_nh);

if (private_nh.hasParam("plugins") )
{
XmlRpc::XmlRpcValue my_list;
private_nh.getParam("plugins", my_list);
for (int32_t i = 0; i < my_list.size(); ++i)
{
std::string pname = static_cast<std::string>(my_list[i]["name"]);
std::string type = static_cast<std::string>(my_list[i]["type"]);
ROS_INFO("Using plugin \"%s\"", pname.c_str() );

// plugin_loader_ 类型是 pluginlib::ClassLoader<Layer>,ROS插件机制
boost::shared_ptr<Layer> plugin = plugin_loader_.createInstance(type);
// 添加到容器 plugins_
layered_costmap_->addPlugin(plugin);
// 执行 Layer::initialize,它向各层地图通知主地图的存在,并调用
// oninitialize方法(Layer类中的虚函数,被定义在各层地图中)
plugin->initialize(layered_costmap_, name + "/" + pname, &tf_);
}
}

这里得到的插件即为各层子地图。如果是新定义的层,至少需要对虚函数onInitialize覆盖

Layer的所有虚函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* called by the LayeredCostmap to poll this plugin as to how
much of the costmap it needs to update.
Each layer can increase the size of this bounds.
*/
virtual void updateBounds(double robot_x, double robot_y, double robot_yaw,
double* min_x, double* min_y, double* max_x, double* max_y) {}

/** Actually update the underlying costmap, only within the bounds
* calculated during UpdateBounds(). */
virtual void updateCosts(Costmap2D& master_grid, int min_i, int min_j, int max_i, int max_j) {}

/* Stop publishers. */
virtual void deactivate() {}
/* Restart publishers if they've been stopped. */
virtual void activate() {}
virtual void reset() {}
/* make this layer match the size of the parent costmap. */
virtual void matchSize() {}
/** LayeredCostmap calls this whenever the footprint there
* changes (via LayeredCostmap::setFootprint()).
Override to be notified of changes to the robot's footprint. */
virtual void onFootprintChanged() {}
virtual void onInitialize() {}

  • 聚合layer的功能:LayeredCostmap* layered_costmap_
  • 此layer是否被使能,是否当前(更新信息):bool enabled_,current
  • layer名:std::string name_,
  • 小车: std::vector< geometry_msgs::Point> footprint_spec_,

三层的加载过程

Costmap2DROS::mapUpdateLoop —— Costmap2DROS::updateMap —— LayeredCostmap::updateMap —— 每一层调用updateBounds —— 每一层调用updateCosts(核心函数)


常用日志的宏

总共有二十多个宏,我只列出有价值而且容易用的

1
2
3
4
5
6
7
#define ROS_INFO(...)
#define ROS_INFO_COND(cond, ...)
#define ROS_INFO_ONCE(...)
#define ROS_INFO_STREAM(args)
#define ROS_INFO_NAMED(name, ...)
#define ROS_INFO_THROTTLE(rate, ...)
#define ROS_INFO_FILTER(filter, ...)

ROS_INFO_ONCE

适用于循环内,防止不断产生日志

1
2
for(int i=0; i<3; i++)
ROS_INFO_ONCE("1111111");

结果只有一个1111111

ROS_INFO_COND

ROS_INFO_COND(2>1, "log here");

ROS_INFO_STREAM

其实就是C++风格的ROS_INFO:
ROS_INFO_STREAM("velocity command: " << " linear=" << msg. linear . x << " angular=" << msg. angular . z)


全局路径规划(二) makePlan

makePlan

1
2
3
bool GlobalPlanner::makePlan(const geometry_msgs::PoseStamped& start,
const geometry_msgs::PoseStamped& goal,
std::vector<geometry_msgs::PoseStamped>& plan)

planService中计算导航路径用到makePlan函数,但planner_boost::shared_ptr<nav_core::BaseGlobalPlanner>,也就是抽象类,实际调用的函数是派生类的override函数bool GlobalPlanner::makePlan(start, goal, plan),内容相当复杂。

实际调用是重载函数makePlan(start, goal, default_tolerance_, plan);, 参数defaulttoleranceGlobalPlanner::initialize中由参数服务器的参数default_tolerance决定. 但是GlobalPlanner::makePlan实际没有处理default_tolerance参数,是Navfn::makePlan处理了。也就是说终点不合适时,GlobalPlanner不会改变全局路径终点

GlobalPlanner::makePlan 重载函数如下:

1
2
3
4
5
6
7
boost::mutex::scoped_lock lock(mutex_);
if (!initialized_) {
ROS_ERROR( "This planner has not been initialized yet, but it is being used,
please call initialize() before use");
return false;
}
plan.clear();

initialized_GlobalPlanner::initialize赋值true, frame_id_实际是GlobalPlanner::initialize函数中的costmap_ros->getGlobalFrameID()

继承关系 2.png

最重要的函数是 calculatePotentials 和 getPlanFromPotential

calculatePotentials

计算所有的可行点

1
2
3
bool found_legal = planner_->calculatePotentials(costmap_->getCharMap(), 
start_x, start_y, goal_x, goal_y,
nx*ny*2, potential_array_ );

实现方法显然是DijkstraExpansion::calculatePotentialsQuadraticCalculator::calculatePotential

getPlanFromPotential 和 getPath

getPlanFromPotential涉及到Traceback::virtual bool getPath,这是一个纯虚函数,实现有两种: class GradientPath : public Traceback(梯度路径), class GridPath : public Traceback(栅格路径)

GradientPath::getPath // 从可行点中计算路径path

OrientationFilter

核心是processPath函数,可以理解为给生成的 path 顺毛,因为A*算法规划的path只能保证路线的连续性,还要保证拐弯的角度别变得太快。 但是setMode设定模式只有在GlobalPlanner::reconfigureCB里使用

参考:
为ROS navigation功能包添加自定义的全局路径规划器
官方wiki教程-使用自己的路径规划算法
ROS插件机制


TEB的优缺点和优化
abstract Welcome to my blog, enter password to read.
Read more
move_base/status,no path和trapped状态
abstract Welcome to my blog, enter password to read.
Read more
振荡行为

CONTROLLING状态下,对振荡进行判断:

1
2
3
4
5
6
7
if(oscillation_timeout_ > 0.0 &&  ros::Time::now() - last_oscillation_reset_ > ros::Duration(oscillation_timeout_)  )
{
publishZeroVelocity();
state_ = CLEARING;
recovery_trigger_ = OSCILLATION_R;
movebase_log.error("oscillate in a small area for a long time !");
}

oscillation_timeout参数默认是0,我设置为10。这个时间是从到达振荡点和判定确实是振荡经过的时间,但整个恢复行为花的时间会有几十秒

判断last_oscillation_reset_的赋值是在

1
2
3
4
5
6
7
8
9
if(distance(current_position, oscillation_pose_) >= oscillation_distance_)
{
last_oscillation_reset_ = ros::Time::now();
oscillation_pose_ = current_position;

//if our last recovery was caused by oscillation, we want to reset the recovery index
if(recovery_trigger_ == OSCILLATION_R)
recovery_index_ = 0;
}

最后彻底失败,会abort:

1
2
3
4
5
else if(recovery_trigger_ == OSCILLATION_R)
{
movebase_log.error("Aborting because the robot appears to be oscillating over and over. Even after executing all recovery behaviors");
as_->setAborted(move_base_msgs::MoveBaseResult(), "Robot is oscillating. Even after executing recovery behaviors.");
}

出现振荡的日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[10:21:19.968 - INFO] (): robot is oscillating, and also recently_oscillated

# move_base CONTROLLING
[10:21:20.262 - ERROR] (): move_base CONTROLLING: oscillate in a small area for a long time !
# 进入CLEARING状态,OSCILLATION_R,准备进入第1个恢复行为
[10:21:20.262 - INFO] (): robot is oscillating, and also recently_oscillated

# 执行第1个恢复行为,结束后,状态变为PLANNING
[10:21:20.301 - INFO] (): move_base_recovery,state machine is CLEARING, Executing behavior 0 of 2
[10:21:20.301 - WARN] (): Clearing costmap to unstuck robot (3.000000m).

[10:21:20.628 - INFO] (): robot is oscillating, and also recently_oscillated
[10:21:33.657 - INFO] (): robot is oscillating, and also recently_oscillated
[10:21:33.724 - INFO] (): robot is oscillating, and also recently_oscillated

# 第一个恢复行为失败,仍然进入CLEARING状态, OSCILLATION_R
[10:21:33.846 - ERROR] (): move_base CONTROLLING: oscillate in a small area for a long time !

[10:21:33.848 - INFO] (): robot is oscillating, and also recently_oscillated

# 执行第2个恢复行为
[10:21:33.900 - INFO] (): move_base_recovery,state machine is CLEARING, Executing behavior 1 of 2
[10:21:33.900 - WARN] (): Clearing costmap to unstuck robot (0.000000m).


[10:21:37.559 - INFO] (): robot is oscillating, and also recently_oscillated
[10:21:37.617 - INFO] (): robot is oscillating, and also recently_oscillated
[10:21:37.805 - INFO] (): robot is oscillating, and also recently_oscillated
[10:21:37.863 - INFO] (): robot is oscillating, and also recently_oscillated

# 省略一段时间

[10:21:48.917 - ERROR] (): move_base CONTROLLING: oscillate in a small area for a long time !
# 两个恢复行为都失败,状态又变为CLEARING OSCILLATION_R
[10:21:48.919 - INFO] (): robot is oscillating, and also recently_oscillated

# 彻底失败,Abort
[10:21:49.103 - ERROR] (): Aborting because the robot appears to be oscillating over and over. Even after executing all recovery behaviors

有时会出现进行一次恢复行为后,机器人走了一点,但是又陷入振荡,然后又进行恢复行为