(九) TimedElasticBand 类和优化之前的准备

本文是对上一篇的补充

TimedElasticBandtimed_elastic_band.h,此类定义了一条时变弹性带的轨迹模型,只有两个protected成员

1
2
3
4
5
6
7
8
9
10
//Internal container storing the sequence of optimzable pose vertices
PoseSequence pose_vec_;
// Internal container storing the sequence of optimzable timediff vertices
TimeDiffSequence timediff_vec_;


// Container of poses that represent the spatial(空间的) part of the trajectory
typedef std::vector<VertexPose*> PoseSequence;
// Container of time differences that define the temporal(时间的) of the trajectory
typedef std::vector<VertexTimeDiff*> TimeDiffSequence;

VertexPose是g2o类型,在g2o_types\vertex_pose.h,保存和封装SE2 pose (position and orientation) into a vertex(图顶点),用于g2o的图优化。 类VertexTimeDiff是g2o类型,在g2o_types\vertex_timediff.h,保存和封装时间差deltaT为一个vertex,用于g2o的图优化.

在TEB初始化的过程中,将起点的Pose与终点的Pose所在的顶点设为固定的,使得g2o不对这两个Pose进行优化。

初始化某一个TEB时,其路线简单的由起点和终点的连线组成。在这条直线上均匀采样点作为待优化的顶点。采样的步长由参数min_samples决定。而 timediff 顶点的初始值为 步长除以参数max_vel_x

每有一个pose顶点就产生一个time_diff顶点,time_diff顶点实际上是每两个Pose之间所需要的时间。

初始化轨迹 initTrajectoryToGoal

以指定的plan(或者起点和终点)进行插值产生的轨迹

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
bool TimedElasticBand::initTrajectoryToGoal(
const std::vector<geometry_msgs::PoseStamped>& plan,
double max_vel_x, double max_vel_theta, bool estimate_orient,
int min_samples, bool guess_backwards_motion )
{
if (!isInit()) // timediff_vec_ pose_vec_ 都是空
{
PoseSE2 start(plan.front().pose);
PoseSE2 goal(plan.back().pose);
// double dt = 0.1;
addPose(start); // 向 pose_vec_ 添加 起点 此时的 BackPose()是 start
//StartConf 称为固定约束,第一个参数是 index
setPoseVertexFixed(0,true); // pose_vec_.at(index)->setFixed(status);

bool backwards = false;
// 参数 guess_backwards_motion 在这里
// check if the goal is behind the start pose (w.r.t. start orientation)
if ( guess_backwards_motion &&
( goal.position()-start.position()).dot(start.orientationUnitVec()) < 0)
backwards = true;
// TODO: dt ~ max_vel_x_backwards for backwards motions
// 取自全局路径点
for (int i=1; i<(int)plan.size()-1; ++i)
{
double yaw;
if (estimate_orient)
{
// get yaw from the orientation of the distance vector
// between pose_{i+1} and pose_{i}
double dx = plan[i+1].pose.position.x - plan[i].pose.position.x;
double dy = plan[i+1].pose.position.y - plan[i].pose.position.y;
yaw = std::atan2(dy,dx);
if (backwards)
yaw = g2o::normalize_theta(yaw+M_PI);
}
else
{
yaw = tf::getYaw(plan[i].pose.orientation);
}
PoseSE2 intermediate_pose(plan[i].pose.position.x, plan[i].pose.position.y, yaw);
/* 根据最大线速度和角速度,估计从 BackPose 到 intermediate_pose所花的时间,
假设匀速运动。也就是说这是估算了一个最小值 */
double dt = estimateDeltaT(BackPose(), intermediate_pose, max_vel_x, max_vel_theta);
// intermediate_pose添加到 pose_vec_, 约束false, 它成为BackPose()
// dt 添加到 timediff_vec_ , 约束false
addPoseAndTimeDiff(intermediate_pose, dt);
}
// number of samples 小于参数 min_samples, insert manually
// -1 的意思是不包含goal point,它在之后添加
if ( sizePoses() < min_samples-1 )
{
ROS_DEBUG("initTEBtoGoal(): number of generated samples is less than
specified by min_samples. Forcing the insertion of more samples ...");
while (sizePoses() < min_samples-1)
{
// 策略: interpolate between the current pose and the goal
// 只处理了 BackPose() 到 goal 这一段,而且是逐步二分
PoseSE2 intermediate_pose = PoseSE2::average(BackPose(), goal);
double dt = estimateDeltaT(BackPose(), intermediate_pose, max_vel_x, max_vel_theta);
// let the optimier correct the timestep (TODO: better initialization
addPoseAndTimeDiff( intermediate_pose, dt );
}
}
// Now add final state with given orientation
double dt = estimateDeltaT(BackPose(), goal, max_vel_x, max_vel_theta);
// goal 也插入 pose_vec_, 成为 BackPose()
addPoseAndTimeDiff(goal, dt);
// GoalConf 也成为了固定约束
setPoseVertexFixed(sizePoses()-1,true);
}
else // size!=0
{
ROS_WARN("Cannot init TEB between given configuration and goal, because
TEB vectors are not empty or TEB is already initialized (call this function
before adding states yourself)!");
ROS_WARN("Number of TEB configurations: %d, Number of TEB timediffs: %d",
(unsigned int) sizePoses(), (unsigned int) sizeTimeDiffs());
return false;
}
return true;
}

  1. 设置起点并且固定(不允许被优化)。
  2. 设置到目标一条直线上平均间隔的点作为初始位姿。
  3. 设置初始的时间间隔序列。
  4. 设置终点并固定(不允许被优化)。

大致流程
注意: dt的单位是秒,不是时间戳,是路径点之间的时间差。所有点都插入 pose_vec_,但是除起点外的timestep都插入 timediff_vec_

热启动 updateAndPruneTEB

更新和改变轨迹,即根据现在的位置更新起点(vector中的第一个点),改变终点。调用 teb_.updateAndPruneTEB(start, goal, cfg_->trajectory.min_samples);

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
// new_start new_goal 的根源都在 transformed_plan
void TimedElasticBand::updateAndPruneTEB(boost::optional<const PoseSE2&> new_start,
boost::optional<const PoseSE2&> new_goal, int min_samples)
{
// first and simple approach: change only start confs(and virtual start conf for inital velocity)
// TEST if optimizer can handle this "hard" placement
if ( new_start && sizePoses()>0 )
{
// find nearest state (using L2-norm) in order to prune the trajectory
// (remove already passed states)
double dist_cache = (new_start->position()- Pose(0).position()).norm();
double dist;
// satisfy min_samples, otherwise max 10 samples
int lookahead = std::min<int>( sizePoses()-min_samples, 10);

int nearest_idx = 0;
for (int i = 1; i<=lookahead; ++i)
{
dist = (new_start->position()- Pose(i).position() ).norm();
if (dist < dist_cache)
{
dist_cache = dist;
nearest_idx = i;
}
else break;
}
// prune trajectory at the beginning (and extrapolate
//sequences at the end if the horizon is fixed )
if (nearest_idx>0)
{
// nearest_idx is equal to the number of samples to be removed (since it counts from 0 )
// WARNING delete starting at pose 1, and overwrite the original pose(0) with
// new_start, since Pose(0) is fixed during optimization!

// delete first states such that the closest state is the new first one
deletePoses(1, nearest_idx);
deleteTimeDiffs(1, nearest_idx); // delete corresponding time differences
}
// update start
Pose(0) = *new_start;
}
if (new_goal && sizePoses()>0)
{
BackPose() = *new_goal;
}
};

当机器人位置变化时,初始轨迹是删去已经走过的,保留后续的。

作用:

  1. 取当前位置为起点,删去已经走过的轨迹,并重新设定终点。
  2. 对于位姿与时间序列,删减去新起点之前的位姿和时间,也就是删除两个容器中旧的元素
  3. 将新的终点加入序列

(十) optimizeTEB 1 流程和 autoResize

optimizeTEB 的流程就是循环几次下列操作(多次图优化):

  1. teb_.autoResize() 在外循环里
  2. buildGraph函数;
  3. optimizeGraph 内循环就是g2o的optimize函数的参数
  4. computeCurrentCost函数(目前的图优化的花费,只在最后一次外循环时进行一次)
  5. clearGraph函数;
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
bool TebOptimalPlanner::optimizeTEB( int  iterations_innerloop, 
int iterations_outerloop, bool compute_cost_afterwards,
double obst_cost_scale, double viapoint_cost_scale,
bool alternative_time_cost )
{
if (cfg_->optim.optimization_activate == false)
return false;

bool success = false;
optimized_ = false;
// ROS源码里是 1
double weight_multiplier = 2.0;
// TODO: we introduced the non-fast mode with the support of dynamic obstacles
// ( which leads to better results in terms of x-y-t homotopy planning )
// 但此模式尚未完全测试
// 目前保持legacy fast mode为默认
bool fast_mode = !cfg_->obstacles.include_dynamic_obstacles; // false

for(int i=0; i<iterations_outerloop; ++i)
{
if (cfg_->trajectory.teb_autosize) // 设置为 true
{
teb_.autoResize(cfg_->trajectory.dt_ref, cfg_->trajectory.dt_hysteresis,
cfg_->trajectory.min_samples, cfg_->trajectory.max_samples, fast_mode);
}
success = buildGraph(weight_multiplier);
if (!success)
{
clearGraph();
return false;
}
success = optimizeGraph(iterations_innerloop, false);
if (!success)
{
clearGraph();
return false;
}
optimized_ = true;
// compute cost vec only in the last iteration
if (compute_cost_afterwards && i==iterations_outerloop-1)
computeCurrentCost(obst_cost_scale, viapoint_cost_scale, alternative_time_cost);

clearGraph();
// 根据目前配置,变成 4 了
weight_multiplier *= cfg_->optim.weight_adapt_factor;
}
return true;
}

optimization_activate

  1. 外部循环通过调用autoResize()来根据时间分辨率调整轨迹,应根据CPU的控制速率要求来定

  2. 内部循环调用optimizeGraph()进行优化,内部循环的时间经过实验2-6次足够

  3. 最后调用computeCurrentCost()计算轨迹花费,将图中所有边的error的平方和构成cost

autoResize

一般不要用快速模式,也就是要进行100次外循环

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
// iterate through all TEB states and add/remove states
void TimedElasticBand::autoResize(double dt_ref, double dt_hysteresis,
int min_samples, int max_samples, bool fast_mode)
{
bool modified = true;
// 确保不会get stuck in some oscillation, 因此最大100此循环
for (int rep = 0; rep < 100 && modified; ++rep)
{
modified = false;
// TimeDiff connects Point(i) with Point(i+1)
// TimeDiff return timediff_vec_.at(index)->dt();
for(int i=0; i < sizeTimeDiffs(); ++i)
{
if(TimeDiff(i) > dt_ref + dt_hysteresis && sizeTimeDiffs()<max_samples)
{
//ROS_DEBUG("teb_local_planner: autoResize() inserting new bandpoint i=%u, #TimeDiffs=%lu",i,sizeTimeDiffs() );
double newtime = 0.5 * TimeDiff(i);
TimeDiff(i) = newtime;
// 时间差太大,插入一个元素
insertPose(i+1, PoseSE2::average(Pose(i),Pose(i+1)) );
insertTimeDiff(i+1, newtime);

modified = true;
}
// only remove samples if size is larger than min_samples.
else if(TimeDiff(i) < dt_ref - dt_hysteresis && sizeTimeDiffs()>min_samples)
{
//ROS_DEBUG("teb_local_planner: autoResize() deleting bandpoint i=%u, #TimeDiffs=%lu",i,sizeTimeDiffs() );
if(i < ((int)sizeTimeDiffs()-1))
{
// 时间差太小,合并 i 到 i+1
TimeDiff(i+1) = TimeDiff(i+1) + TimeDiff(i);
deleteTimeDiff(i);
deletePose(i+1);
}
else
{ // last motion should be adjusted, shift time to the interval before
// 合并 i 到 i-1
TimeDiff(i-1) += TimeDiff(i);
deleteTimeDiff(i);
deletePose(i);
}

modified = true;
}
}
// 如果是快速模式则结束,也就是rep<100不起作用
if (fast_mode) break;
}
}

这里其实是动态对initTrajectoryToGoal中建立的弹性带进行调整,不能让位姿点和deltT太密集,也不能太稀疏。min_samplesmax_samples就是一组限制条件,dt_refdt_hysteresis是另一组


ROS自带的恢复行为

原理图

所有的recovery behaviors都必须实现nav_core::RecoveryBehavior定义的接口(纯虚函数)。目前有类ClearCostmapRecovery, MoveSlowAndClear, RotateRecovery RotateRecovery一般不用

每个恢复行为类主要实现两个函数,一个负责初始化,另一个负责执行恢复行为。

1
2
virtual void initialize()
virtual void runBehavior() = 0;

参数

恢复行为在move_base_params.yaml中定义,如果不定义,会使用默认的配置。MoveBase构造函数中的使用:

1
2
if(!loadRecoveryBehaviors(private_nh))
loadDefaultRecoveryBehaviors();

如果加载自己的恢复行为不成功,就加载系统默认的。 如果用默认参数,则是按顺序加载了①conservative_clear, ②rotate, ③aggressive_clear, ④rotate. ①/③都是ClearCostmapRecovery类,负责清理机器人一定范围外的costmap上的数据,区别在于③保留的范围更小,清理的更多。恢复行为是按照列表顺序调用的,当①②无效后,才会执行③,清理更多区域。 默认配置中,aggressive_reset清除的范围是4 * local_costmap/circumscribed_radius以外


我的配置是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
recovery_behaviors:
- name: 'conservative_reset'
type: 'clear_costmap_recovery/ClearCostmapRecovery'
- name: 'aggressive_reset'
type: 'clear_costmap_recovery/ClearCostmapRecovery'
conservative_reset:
reset_distance: 3.0
layer_names:
- obstacles

aggressive_reset:
reset_distance: 0.0
layer_names:
- obstacles

函数中的变量recovery_behaviors_插入的就是conservative_resetaggressive_reset

两个恢复行为都是ClearCostmapRecovery类,相关参数:

  • reset_distance (double, default: 3.0)指定距离机器人半径几米的区域之外的障碍在恢复代价地图为静态地图时候将被清除。
  • layer_names (std::vector, default:”obstacles”)哪一层的信息将会被清除


还有两个重要参数:

  • oscillation_distance (double, default: 0.5) 来回震荡的距离,超过一定的时间没有移动足够的距离就会进行recovery behavior。移动达到了这个距离,就会重置oscillation_timeout的计时器

  • oscillation_timeout (double, default: 0.0) 在执行恢复行为前允许的振荡时间,0代表无穷的时间

我的小车不允许有一些奇怪的行为,比如振荡。所以不用rotate recoveryoscillation_distance设置为0.05也是为了避免振荡

状态机

Movebase有三个状态:

  • PLANNING:全局规划中,尚未得到全局路径
  • CONTROLLING:全局规划完成,进入局部规划
  • CLEARING:恢复行为

当 ①全局规划失败 ②局部规划失败 ③机器人震荡时会进入到恢复行为。

三种情况对应的恢复行为起因

1
2
3
4
5
6
enum RecoveryTrigger
{
PLANNING_R, // 全局规划失败,即无法算出 plan
CONTROLLING_R, // 局部规划失败,从而无法得到速度命令
OSCILLATION_R // 机器人在不断抖动,长时间被困在一小片区域
};

恢复行为的状态机.png

日志:

1
2
3
4
5
6
7
move_base_recovery,state machine is CLEARING, Executing behavior 0 of 2
Clearing costmap to unstuck robot (3.000000m).

move_base_recovery,state machine is CLEARING, Executing behavior 1 of 2
Clearing costmap to unstuck robot (0.000000m).

Aborting because a valid plan could not be found. Even after executing all recovery behaviors

RotateRecovery::runBehavior()

只是在原地逆时针旋转180度,这样costmap就会自己更新,mark或者clear一点的值,有没有路是局部代价地图在转一圈过程中发现的。

这里的目标是转过180度,将当前角度与目标180的偏差记录在distleft中,并通过这个偏差产生角速度的控制量,具体为 `vel = sqrt(2 * acc_lim_th * distleft)` ,并且要确保这个速度在角速度阈值之内,发布这个角速度,并循环检测偏差是否满足tolerance范围,当符合时退出。

按默认情况,它先清理周围一定范围以外的costmap(障碍层),接下来重新执行规划,若不奏效,则旋转180度,再执行规划。交替两次后,已转过360度,若还是没能排除故障,则恢复行为失败,关闭Movebase规划器。

ClearCostmapRecovery::runBehavior()

它将距离机器人 一定距离的正方形区域外的 代价地图重置,理解不了看看参数reset_distance就明白了。 ClearCostmapRecovery自身不会让机器人运动

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void ClearCostmapRecovery::runBehavior(){
// 省略:判断是否初始化,两个代价地图是否为空,否则return
// reset_distance_ 就是定义的参数
clear_costmap_log.warn("Clearing costmap to unstuck robot (%fm).", reset_distance_);
clear(global_costmap_);
clear(local_costmap_);
}

void ClearCostmapRecovery::clear(costmap_2d::Costmap2DROS* costmap)
{
std::vector<boost::shared_ptr<costmap_2d::Layer> >* plugins = costmap->getLayeredCostmap()->getPlugins();

tf::Stamped<tf::Pose> pose;
// 在代价地图的global坐标系中的坐标 x y
if(!costmap->getRobotPose(pose)){
ROS_ERROR("Cannot clear map because pose cannot be retrieved");
return;
}
double x = pose.getOrigin().x();
double y = pose.getOrigin().y();
// 遍历代价地图的每一层,转为costmap_2d::Layer,获取名称
for (std::vector<boost::shared_ptr<costmap_2d::Layer> >::iterator pluginp = plugins->begin(); pluginp != plugins->end(); ++pluginp)
{
boost::shared_ptr<costmap_2d::Layer> plugin = *pluginp;
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){
boost::shared_ptr<costmap_2d::CostmapLayer> costmap;
costmap = boost::static_pointer_cast<costmap_2d::CostmapLayer>(plugin);
clearMap(costmap, x, y);
}
}
}

void ClearCostmapRecovery::clearMap(boost::shared_ptr<costmap_2d::CostmapLayer> costmap,
double pose_x, double pose_y)
{
boost::unique_lock<costmap_2d::Costmap2D::mutex_t> lock(*(costmap->getMutex()));

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);
// 获取字符格式的代价地图
unsigned char* grid = costmap->getCharMap();
for(int x=0; x<(int)costmap->getSizeInCellsX(); x++)
{
bool xrange = x>start_x && x<end_x;
for(int y=0; y<(int)costmap->getSizeInCellsY(); y++)
{
// 注意是不处理 reset_distance_正方形范围内的代价值
if(xrange && y>start_y && y<end_y)
continue;
int index = costmap->getIndex(x,y);
// 范围外的代价值重置为 NO_INFORMATION
if(grid[index]!=NO_INFORMATION){
grid[index] = NO_INFORMATION;
}
}
}
double ox = costmap->getOriginX(), oy = costmap->getOriginY();
double width = costmap->getSizeInMetersX(), height = costmap->getSizeInMetersY();

costmap->addExtraBounds(ox, oy, ox + width, oy + height);
return;
}

MoveSlowAndClear

这个使用较少。 它实现了缓慢移动的恢复行为,清理 costmap后什么都不管,按照前进速度和转角速度走。从代码里可以看到,根据指定的距离,这是通过先清除全局 costmap 跟局部 costmap 一圈的 obstacle layer 的障碍,然后直接发指令实现的。由于只清除了 obstacle layer ,其实 static layer 的障碍还在,而且这里清不清除跟发指令关系不大,该撞上去的还是会撞的,相当于闭着眼睛往前走。

禁止所有恢复行为造成的问题

不如自己不定义,还会使用默认的参数,所以将下面代码去掉:

1
2
if(!loadRecoveryBehaviors(private_nh))
loadDefaultRecoveryBehaviors();

结果发现平时正常导航的情况居然出错了

1
2
3
4
found_legal is 0
[ERROR] Failed to get a plan.
[ INFO] Robot in controlling, but can't get new global plan
[ERROR] 17:51:42.074 (): Aborting because a valid plan could not be found. Even after executing all recovery behaviors

并不是每次的导航都会出错,这要看全局路径,全局路径部分的bool found_legal = planner_->calculatePotentials有时会返回0,也就是找不到全局路径,此时会触发恢复行为。所以如果全禁止,会直接 Abort

正常的日志是这样的:

1
2
3
4
5
6
7
8
9
10
Received move_base goal (x: 62.766, y: -1.474, yaw deg: -5, yaw rad: -0.09)
[ERROR] Failed to get a plan.
[ INFO] Robot in controlling, but can't get new global plan
[INFO] move_base_recovery,state machine is CLEARING, Executing behavior 0 of 2
[ WARN] Clearing costmap to unstuck robot (0.000000m).
[ERROR] Failed to get a plan.
[ INFO] Robot in controlling, but can't get new global plan
[ INFO] move_base_recovery,state machine is CLEARING, Executing behavior 0 of 2
[ WARN] Clearing costmap to unstuck robot (0.000000m).
[ INFO] 到达目标

也就是获取不到全局路径时,会很快执行一次恢复行为,clear costmap,然后又找到了路径,这个过程之快,几乎看不到。

参考: 导航recovery机制


(七) Teb Optimal的路径规划
1
2
3
4
5
6
7
8
9
10
11
12
13
// 禁止配置参数的更改 during the following optimization step
boost::mutex::scoped_lock cfg_lock(cfg_.configMutex() );
bool success = planner_->plan(transformed_plan, &robot_vel_, cfg_.goal_tolerance.free_goal_vel);
if (!success)
{
planner_->clearPlanner(); // force reinitialization for next time
ROS_WARN("teb_local_planner was not able to obtain a local plan for the current setting.");

++no_infeasible_plans_; // increase number of infeasible solutions in a row
time_last_infeasible_plan_ = ros::Time::now();
last_cmd_ = cmd_vel;
return false;
}

clearPlanner()

不会停止规划,而是强制下次重新初始化planner,要停止继续规划,只能return。 如果本次导航失败或路径不好,出现一大堆TebPose缠绕,在下一次导航时,它们可能还在,要消除就需要clearPlanner();
下次导航前,需要clearPlanner.png


实际中用的是Homotopy Planner,但是它太复杂了,我们先看TebOptimal Planner,前者取的其实是 best Teb Optimal Planner
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
bool TebOptimalPlanner::plan(
const std::vector<geometry_msgs::PoseStamped>& initial_plan,
const geometry_msgs::Twist* start_vel,
bool free_goal_vel )
{
ROS_ASSERT_MSG(initialized_, "Call initialize() first.");
// 每次收到新目标时执行,然后pose size都是3
if (!teb_.isInit() )
{
teb_.initTrajectoryToGoal(initial_plan, cfg_->robot.max_vel_x,
cfg_->robot.max_vel_theta, cfg_->trajectory.global_plan_overwrite_orientation,
cfg_->trajectory.min_samples, cfg_->trajectory.allow_init_with_backwards_motion);
// 0 个 intermediate samples, dt=1 --> autoResize will add more samples
// before first optimization
}
else // 热启动
{
PoseSE2 start_(initial_plan.front().pose);
PoseSE2 goal_(initial_plan.back().pose);
if (teb_.sizePoses()>0
&& ( goal_.position() - teb_.BackPose().position()).norm() <
cfg_->trajectory.force_reinit_new_goal_dist
&& fabs(g2o::normalize_theta(goal_.theta() - teb_.BackPose().theta())) <
cfg_->trajectory.force_reinit_new_goal_angular ) // actual warm start!

teb_.updateAndPruneTEB(start_, goal_, cfg_->trajectory.min_samples);

else // goal is too far away from BackPose, reinit相当于 isInit()为false
{
// 当导航较远的goal时,则首先clear轨迹序列,然后重新起点终点插值
teb_.clearTimedElasticBand();
teb_.initTrajectoryToGoal( start, goal, 0, cfg_->robot.max_vel_x,
cfg_->trajectory.min_samples,
cfg_->trajectory.allow_init_with_backwards_motion );
}
}
// 当前从里程计获取的速度
if (start_vel)
// 赋值给 std::pair<bool, geometry_msgs::Twist> vel_start_;
setVelocityStart(*start_vel); // start pose的速度
if (free_goal_vel) // 一般为 false
setVelocityGoalFree(); // 这是唯一调用此函数的地方,不看了
else
// reactivate and 使用之前设置的速度 ( 如果未修改则是0 )
vel_goal_.first = true;

return optimizeTEB(cfg_->optim.no_inner_iterations, cfg_->optim.no_outer_iterations);
}

isInit()检查trajectory是否初始化: bool isInit() const {return !timediff_vec_.empty() && !pose_vec_.empty();}

1
2
3
4
5
6
7
8
9
10
11
12
13
// 清空 timediff_vec_ 和 pose_vec_, 之后 isInit返回 false
void TimedElasticBand::clearTimedElasticBand()
{
for (PoseSequence::iterator pose_it = pose_vec_.begin();
pose_it != pose_vec_.end(); ++pose_it)
delete *pose_it;
pose_vec_.clear();

for (TimeDiffSequence::iterator dt_it = timediff_vec_.begin();
dt_it != timediff_vec_.end(); ++dt_it )
delete *dt_it;
timediff_vec_.clear();
}

关于变量pose_vec_timediff_vec_,看下篇文章


plan 函数涉及参数 max_vel_x, min_samples, force_reinit_new_goal_dist, allow_init_with_backwards_motion, free_goal_vel

TebOptimalPlanner::plan()主要是根据输入的初始路径初始化或更新时间弹性带(轨迹)的初始状态,设置了轨迹起始点以及速度加速度的约束, 最后调用 optimizeTEB() 函数:这里是核心中的核心,最关键的就是建图buildGraph和优化OptimizeGraph两个步骤

将轨迹优化问题构建成了一个g2o图优化问题,并通过g2o中关于大规模稀疏矩阵的优化算法解决,涉及到构建超图(hyper-graph),简单来说 将机器人位姿和时间间隔描述为节点(vertex),目标函数以及约束函数作为边(edge)。 graph中,每一个约束都为一条边,并且每条边允许连接的节点的数目是不受限制的。

建图是将路径的一系列约束加进去,比如与障碍物保持一定距离,速度、加速度限制、时间最小约束、最小转弯半径约束、旋转方向约束等,这些都转换成了代价函数放在g2o中,g2o优化时会使这些代价函数达到最小。

TebOptimalPlanner 的初始化

TebLocalPlannerROS::initialize有这样一段

1
2
3
4
5
6
7
8
9
10
if (cfg_.hcp.enable_homotopy_class_planning)
{
// 省略
}
else
{
planner_ = PlannerInterfacePtr(new TebOptimalPlanner(cfg_,
&obstacles_, robot_model, visualization_, &via_points_) );
ROS_INFO("Parallel planning in distinctive topologies disabled.");
}

TebOptimalPlanner的构造函数就一句initialize(cfg, obstacles, robot_model, visual, via_points);,实际是:
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
void TebOptimalPlanner::initialize(const TebConfig& cfg, 
ObstContainer* obstacles,
RobotFootprintModelPtr robot_model,
TebVisualizationPtr visual,
const ViaPointContainer* via_points)
{
// init optimizer (set solver and block ordering settings)
optimizer_ = initOptimizer();
cfg_ = &cfg;
obstacles_ = obstacles;
robot_model_ = robot_model;
via_points_ = via_points;
cost_ = HUGE_VAL;
prefer_rotdir_ = RotType::none;
setVisualization(visual);
// 在上面的 setVelocityStart 里赋值
vel_start_.first = true;
vel_start_.second.linear.x = 0;
vel_start_.second.linear.y = 0;
vel_start_.second.angular.z = 0;

vel_goal_.first = true;
vel_goal_.second.linear.x = 0;
vel_goal_.second.linear.y = 0;
vel_goal_.second.angular.z = 0;
initialized_ = true;
}

initOptimizer()函数在(十) optimizeTEB 2 图优化的过程


(六) 静态障碍和自定义障碍

对应的片段:

1
2
3
4
5
6
7
8
9
10
11
12
// clear currently existing obstacles
obstacles_.clear(); // 初始化时 obstacles_.reserve(500);
// 用代价地图信息更新 obstacle container 或者
// 用costmap_converter提供的polygons
if (costmap_converter_)
updateObstacleContainerWithCostmapConverter();
else
updateObstacleContainerWithCostmap();

// 考虑custom obstacles (must be called after other updates,
// since the container is not cleared )
updateObstacleContainerWithCustomObstacles();

obstacles_来源于两部分:

  1. 来自代价地图updateObstacleContainerWithCostmap() 或者 CostmapConverter 的updateObstacleContainerWithCostmapConverter()

  2. 来自用户发布的障碍物消息updateObstacleContainerWithCustomObstacles()

分别由参数include_costmap_obstaclescostmap_converter_plugin决定,还有动态障碍include_dynamic_obstacles。 之后又对应buildGraph中的AddEdgesObstaclesAddEdgesDynamicObstacles

其实目前用的是costmap_converter

updateObstacleContainerWithCostmap

获得当前朝向的单位向量

1
2
3
4
Eigen::Vector2d  PoseSE2::orientationUnitVec() const
{
return Eigen::Vector2d(std::cos(_theta), std::sin(_theta) );
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Add costmap obstacles if desired
if (cfg_.obstacles.include_costmap_obstacles)
{
Eigen::Vector2d robot_orient = robot_pose_.orientationUnitVec();

for (unsigned int i=0; i<costmap_->getSizeInCellsX()-1; ++i)
{
for (unsigned int j=0; j<costmap_->getSizeInCellsY()-1; ++j)
{
if (costmap_->getCost(i,j) == costmap_2d::LETHAL_OBSTACLE)
{
Eigen::Vector2d obs;
costmap_->mapToWorld(i,j, obs.coeffRef(0), obs.coeffRef(1));

// 检查障碍是否 not far behind the robot
Eigen::Vector2d obs_dir = obs - robot_pose_.position();
// if(obs_dir.norm() < 1.5) //改变最大速度约束
// cfg_.robot.max_vel_x = 0.3;
// 如果norm值过大 或者 obs_dir和robot_orient的点乘积小于0, 不算障碍
if ( obs_dir.dot(robot_orient) < 0 && obs_dir.norm() > cfg_.obstacles.costmap_obstacles_behind_robot_dist )
continue;
// 点障碍
obstacles_.push_back(ObstaclePtr(new PointObstacle(obs)));
} } } }

这里是参数include_costmap_obstacles唯一出现的地方。 参数costmap_obstacles_behind_robot_dist默认是1.
函数根据costmap中的障碍物信息更新障碍物容器。在这里,可以找到前方距离障碍物的最短距离。然后根据该距离值调整max_vel_x,这样优化时最大线速度的约束会随着障碍物的远近而调整,在一定程度上会改善避障效果。

自定义障碍

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
void TebLocalPlannerROS::updateObstacleContainerWithCustomObstacles()
{
// Add custom obstacles obtained via message
boost::mutex::scoped_lock l(custom_obst_mutex_);
// 类型 costmap_converter::ObstacleArrayMsg
if (!custom_obstacle_msg_.obstacles.empty())
{
// We only use the global header to specify the obstacle coordinate system
// instead of individual ones
Eigen::Affine3d obstacle_to_map_eig;
try
{
tf::StampedTransform obstacle_to_map;
tf_->waitForTransform(global_frame_, ros::Time(0),
custom_obstacle_msg_.header.frame_id, ros::Time(0),
custom_obstacle_msg_.header.frame_id, ros::Duration(0.5) );
tf_->lookupTransform(global_frame_, ros::Time(0),
custom_obstacle_msg_.header.frame_id, ros::Time(0),
custom_obstacle_msg_.header.frame_id, obstacle_to_map);
tf::transformTFToEigen(obstacle_to_map, obstacle_to_map_eig);
}
catch (tf::TransformException ex)
{
ROS_ERROR("%s",ex.what());
obstacle_to_map_eig.setIdentity();
}
for (size_t i=0; i<custom_obstacle_msg_.obstacles.size(); ++i)
{
if (custom_obstacle_msg_.obstacles.at(i).polygon.points.size() == 1 &&
custom_obstacle_msg_.obstacles.at(i).radius > 0 ) // circle
{
Eigen::Vector3d pos( custom_obstacle_msg_.obstacles.at(i).polygon.points.front().x,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().y,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().z );
obstacles_.push_back(ObstaclePtr(new CircularObstacle( (obstacle_to_map_eig * pos).head(2), custom_obstacle_msg_.obstacles.at(i).radius) ) );
}
else if (custom_obstacle_msg_.obstacles.at(i).polygon.points.size() == 1 ) // point
{
Eigen::Vector3d pos( custom_obstacle_msg_.obstacles.at(i).polygon.points.front().x,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().y,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().z );
obstacles_.push_back(ObstaclePtr(new PointObstacle( (obstacle_to_map_eig * pos).head(2) )));
}
else if (custom_obstacle_msg_.obstacles.at(i).polygon.points.size() == 2 ) // line
{
Eigen::Vector3d line_start( custom_obstacle_msg_.obstacles.at(i).polygon.points.front().x,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().y,
custom_obstacle_msg_.obstacles.at(i).polygon.points.front().z );
Eigen::Vector3d line_end( custom_obstacle_msg_.obstacles.at(i).polygon.points.back().x,
custom_obstacle_msg_.obstacles.at(i).polygon.points.back().y,
custom_obstacle_msg_.obstacles.at(i).polygon.points.back().z );
obstacles_.push_back(ObstaclePtr(new LineObstacle( (obstacle_to_map_eig * line_start).head(2),
(obstacle_to_map_eig * line_end).head(2) ) ) );
}
else if (custom_obstacle_msg_.obstacles.at(i).polygon.points.empty())
{
ROS_WARN("Invalid custom obstacle received. List of polygon vertices is empty. Skipping...");
continue;
}
else // polygon
{
PolygonObstacle* polyobst = new PolygonObstacle;
for (size_t j=0; j<custom_obstacle_msg_.obstacles.at(i).polygon.points.size(); ++j)
{
Eigen::Vector3d pos( custom_obstacle_msg_.obstacles.at(i).polygon.points[j].x,
custom_obstacle_msg_.obstacles.at(i).polygon.points[j].y,
custom_obstacle_msg_.obstacles.at(i).polygon.points[j].z );
polyobst->pushBackVertex( (obstacle_to_map_eig * pos).head(2) );
}
polyobst->finalizePolygon();
obstacles_.push_back(ObstaclePtr(polyobst));
}
// Set velocity, if obstacle is moving
if(!obstacles_.empty())
{
obstacles_.back()->setCentroidVelocity(custom_obstacle_msg_.obstacles[i].velocities, custom_obstacle_msg_.obstacles[i].orientation);
obstacles_.back()->setDynamic();
}
}
}
}

消息costmap_converter/ObstacleArrayMsg定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std_msgs/Header header    # frame_id 为map
costmap_converter/ObstacleMsg[] obstacles

# costmap_converter/ObstacleMsg 的成员如下
std_msgs/Header header # frame_id 为map
# Obstacle footprint (polygon descriptions)
geometry_msgs/Polygon polygon
# radius for circular/point obstacles
float64 radius
# Obstacle ID
# Specify IDs in order to provide (temporal) relationships
# between obstacles among multiple messages.
int64 id
# Individual orientation (centroid)
geometry_msgs/Quaternion orientation
# Individual velocities (centroid)
geometry_msgs/TwistWithCovariance velocities

Obstacle定义在obstacle.h,派生类有PointObstacle, CircularObstacle, LineObstacle,PolygonObstacle

1
2
typedef boost::shared_ptr<Obstacle> ObstaclePtr;
typedef std::vector<ObstaclePtr> ObstContainer;


(三) 从全局路径获得 transform_plan

MoveBase::executeCycle() —-> computeVelocityCommands,就是从这个函数开始调用TEB算法,下面转入teb_local_planner_ros.cpp文件中。

开始代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// check if plugin initialized,省略
cmd_vel.linear.x = 0;
cmd_vel.linear.y = 0;
cmd_vel.angular.z = 0;
goal_reached_ = false;
// 修改源码
if(!rotate_to_path_ && new_goal_)
{
double start_angle = tf::getYaw(path_start_orientation_);
tf::Stamped<tf::Pose> robot_pose;
costmap_ros_->getRobotPose(robot_pose);
double robot_angle = tf::getYaw(robot_pose.getRotation());

bool result = pointToPath(start_angle, robot_angle, cmd_vel);
if(result)
{
rotate_to_path_ = true;
return true;
}
return true;
}

// Get robot pose in global frame(map坐标系),转为PoseSE2类型
tf::Stamped<tf::Pose> robot_pose;
costmap_ros_->getRobotPose(robot_pose);
robot_pose_ = PoseSE2(robot_pose);

// Get robot velocity from odom
tf::Stamped<tf::Pose> robot_vel_tf;
odom_helper_.getRobotVel(robot_vel_tf);
robot_vel_.linear.x = robot_vel_tf.getOrigin().getX();
robot_vel_.linear.y = robot_vel_tf.getOrigin().getY();
robot_vel_.angular.z = tf::getYaw(robot_vel_tf.getRotation());

// prune global plan to cut off parts of the past (spatially before the robot)
pruneGlobalPlan(*tf_, robot_pose, global_plan_);

// Transform global plan to the frame of interest (w.r.t. the local costmap)
std::vector<geometry_msgs::PoseStamped> transformed_plan;
int goal_idx;
tf::StampedTransform tf_plan_to_global;
if (!transformGlobalPlan(*tf_, global_plan_, robot_pose, *costmap_, global_frame_, cfg_.trajectory.max_global_plan_lookahead_dist,
transformed_plan, &goal_idx, &tf_plan_to_global))
{
ROS_WARN("Could not transform the global plan to the frame of the controller");
return false;
}

// update via-points container 这里是false,不用看了
if (!custom_via_points_active_)
updateViaPointsContainer(transformed_plan, cfg_.trajectory.global_plan_viapoint_sep);

// check if global goal is reached
tf::Stamped<tf::Pose> global_goal;
tf::poseStampedMsgToTF(global_plan_.back(), global_goal);
global_goal.setData( tf_plan_to_global * global_goal );
double dx = global_goal.getOrigin().getX() - robot_pose_.x();
double dy = global_goal.getOrigin().getY() - robot_pose_.y();
double delta_orient = g2o::normalize_theta( tf::getYaw(global_goal.getRotation()) - robot_pose_.theta() );
// 是否达到目标位置
if(fabs(std::sqrt(dx*dx+dy*dy)) < cfg_.goal_tolerance.xy_goal_tolerance)
position_reanched_ = true;

// 修改源码
if(position_reached_)
{
double goal_angle = tf::getYaw(goal_orientation_);
double robot_angle = tf::getYaw(robot_pose.getRotation());

bool result = pointToPath(goal_angle, robot_angle, cmd_vel);
if(result)
{
goal_reached_ = true;
return true;
}
return true;
}

if(!goal_reached_)
{
if(fabs(std::sqrt(dx*dx+dy*dy)) < cfg_.goal_tolerance.xy_goal_tolerance
&& fabs(delta_orient) < cfg_.goal_tolerance.yaw_goal_tolerance
&& (!cfg_.goal_tolerance.complete_global_plan || via_points_.size() == 0))
{
goal_reached_ = true;
return true;
}
}
// check if we should enter any backup mode and apply settings
configureBackupModes(transformed_plan, goal_idx);

// Return false if the transformed global plan is empty
if (transformed_plan.empty())
{
ROS_WARN("Transformed plan is empty. Cannot determine a local plan.");
return false;
}

  • pruneGlobalPlan: 截取全局路径的一部分作为局部规划的初始轨迹,主要是把全局路径中机器人已经经过的位姿删去。取决于参数 dist_behind_robot,机器人身后要保留的距离,单位米,默认1,不能太小,尤其不能小于地图的cellsize。 在调用前后,global_plan_常常不发生变化,所以不必太关注。

  • transformGlobalPlan: 根据local costmap地图大小,将此地图内的全局规划器的规划保存到transformed_plan中。transformed_plan所在的坐标系仍然为global_frames

global_plan_对应的就是话题/move_base/TebLocalPlannerROS/global_plan,它的size和成员一般在规划两三次后就会变,size一般是逐步变小,transformed_plan也跟随变化。

transformGlobalPlan函数之后,transformed_plan开始和global_plan_相等,transformed_plan逐渐取global_plan_的后面部分,此时goal_idx == global_plan.size() - 1。 有时候transformed_planglobal_plan_中间部分,此时goal_idx != global_plan.size() - 1

这三者会出现以下几种情况:

  1. global_plan_ size: 49, transformed_plan size:49, goal_idx: 48 : transformed_plan是从global_plan第1个点开始(id=0), 二者完全相同goal_idx对应transformed_planglobal_plan_最后一个点

  2. global_plan_ size: 102, transformed_plan size:69, goal_idx: 68: transformed_plan是从global_plan第1个点开始(id=0), 但这次 transformed_plan 取得是 global_plan 前半部分goal_idx对应transformed_plan最后一个点,

  3. global_plan_ size: 49, transformed_plan size:45, goal_idx: 48: transformed_plan是从global_plan第5个点开始(id=4),取global_plan后半部分goal_idx对应transformed_planglobal_plan_最后一个点

  4. global_plan size: 98, transformed_plan size:69, goal_idx: 73: transformed_plan是从global_plan第6个点开始(id=5), 也就是取中间部分goal_idx对应transformed_plan最后一个点。 goal_idx != transformed_plan.size()-1

goal_idx 是 global_plan 中的id,它对应的点一定和 transformed_plan 最后一个点相同,所以后面才可以赋值给goal_pointrobot_goal_


(二) 算法的初始化

MoveBase中加载局部路径规划器

1
tc_->initialize(blp_loader_.getName(local_planner), &tf_, controller_costmap_ros_);

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
void TebLocalPlannerROS::initialize(std::string name, tf::TransformListener* tf, costmap_2d::Costmap2DROS* costmap_ros)
{
if(!initialized_)
{
// create Node Handle with name of plugin (as used in move_base for loading)
ros::NodeHandle nh("~/" + name);

// get parameters of TebConfig via the nodehandle and override the default config
cfg_.loadRosParamFromNodeHandle(nh); // TebConfig cfg_;
// nh.param("trapped_threshold", trapped_threshold_, 20);
nh.param("filter_back_velocity", filter_back_velocity_, true);
// nh.param("setting_acc", setting_acc_, 1.5);

obstacles_.reserve(500); // ObstContainer obstacles_;

// 在TebVisualization构造函数里,注册话题 global_plan, local_plan,teb_poses, teb_markers, teb_feedback
visualization_ = TebVisualizationPtr(new TebVisualization(nh, cfg_));

//创建robot footprint/contour model for optimization,对5种类型分别处理
// 点类型最简单,返回新建的PointRobotFootprint共享指针
// 对于polygon类型,只对 Point2dContainer vertices_;赋值
RobotFootprintModelPtr robot_model = getRobotFootprintFromParamServer(nh);

// 根据参数,选择创建哪种 planner
if (cfg_.hcp.enable_homotopy_class_planning)
{
planner_ = PlannerInterfacePtr(new HomotopyClassPlanner(cfg_, &obstacles_, robot_model, visualization_, &via_points_));
ROS_INFO("Parallel planning in distinctive topologies enabled.");
}
else
{
planner_ = PlannerInterfacePtr(new TebOptimalPlanner(cfg_, &obstacles_, robot_model, visualization_, &via_points_));
ROS_INFO("Parallel planning in distinctive topologies disabled.");
}
// init other variables
tf_ = tf; // 在 move_base_node.cpp中最初定义
// 这个就是 controller_costmap_ros_ = new costmap_2d::Costmap2DROS("local_costmap", tf_);
costmap_ros_ = costmap_ros;
// locking should be done in MoveBase
costmap_ = costmap_ros_->getCostmap();

// 创建 CostmapModel 共享指针
costmap_model_ = boost::make_shared<base_local_planner::CostmapModel>(*costmap_);

// 就是return global_frame, 实际是 "map"
global_frame_ = costmap_ros_->getGlobalFrameID();
cfg_.map_frame = global_frame_; // 更新cf_的成员

// 代价地图的local frame, 实际是 "base_footprint"
robot_base_frame_ = costmap_ros_->getBaseFrameID();

//Initialize a costmap to polygon converter
if (!cfg_.obstacles.costmap_converter_plugin.empty())
{
try
{
costmap_converter_ = costmap_converter_loader_.createInstance(cfg_.obstacles.costmap_converter_plugin);
std::string converter_name = costmap_converter_loader_.getName(cfg_.obstacles.costmap_converter_plugin);
// replace '::' by '/' to convert the c++ namespace to a NodeHandle namespace
boost::replace_all(converter_name, "::", "/");
costmap_converter_->setOdomTopic(cfg_.odom_topic);
costmap_converter_->initialize(ros::NodeHandle(nh, "costmap_converter/" + converter_name));
costmap_converter_->setCostmap2D(costmap_);

costmap_converter_->startWorker(ros::Rate(cfg_.obstacles.costmap_converter_rate), costmap_, cfg_.obstacles.costmap_converter_spin_thread);
ROS_INFO_STREAM("Costmap conversion plugin " << cfg_.obstacles.costmap_converter_plugin << " loaded.");
}
catch(pluginlib::PluginlibException& ex)
{
ROS_WARN("The specified costmap converter plugin cannot be loaded. All occupied costmap cells are treated as point obstacles. Error message: %s", ex.what());
costmap_converter_.reset();
}
}
else
ROS_INFO("No costmap conversion plugin specified. All occupied costmap cells are treaten as point obstacles.");

// 类型是 vector<geometry_msgs::Point>
footprint_spec_ = costmap_ros_->getRobotFootprint();
costmap_2d::calculateMinAndMaxDistances(footprint_spec_, robot_inscribed_radius_, robot_circumscribed_radius);
// 更新cf_的成员
cfg_.robot.robot_inscribed_radius = robot_inscribed_radius_;
cfg_.robot.robot_circumscribed_radius = robot_circumscribed_radius;

// init the odom helper to receive the robot's velocity from odom messages
odom_helper_.setOdomTopic(cfg_.odom_topic);

// setup dynamic reconfigure
dynamic_recfg_ = boost::make_shared< dynamic_reconfigure::Server<TebLocalPlannerReconfigureConfig> >(nh);
dynamic_reconfigure::Server<TebLocalPlannerReconfigureConfig>::CallbackType cb = boost::bind(&TebLocalPlannerROS::reconfigureCB, this, _1, _2);
dynamic_recfg_->setCallback(cb);

// validate optimization footprint and costmap footprint
// 对于point类型,第1个参数是0,要求最好第1项+第3项 > 第2项
// 否则 Infeasible optimziation results might occur frequently
validateFootprints(robot_model->getInscribedRadius(), robot_inscribed_radius_,
cfg_.obstacles.min_obstacle_dist);

// custom obstacles的回调函数,话题 /move_base/TebLocalPlannerROS/obstacles
custom_obst_sub_ = nh.subscribe("obstacles", 1, &TebLocalPlannerROS::customObstacleCB, this);

// custom via-points的回调,话题 /move_base/TebLocalPlannerROS/via_points
via_points_sub_ = nh.subscribe("via_points", 1, &TebLocalPlannerROS::customViaPointsCB, this);
// queue_follow 所用,话题 /rectangle_distance
front_dist_sub_ = nh.subscribe("/rectangle_distance", 1, &TebLocalPlannerROS::frontDistanceCB, this);
// 新添加
footprint_sub_ = nh.subscribe("/footprint", 10, &TebLocalPlannerROS::footprintCB, this);


ros::NodeHandle nh_move_base("~");
double controller_frequency = 5;
// 实际配置是 30
nh_move_base.param("controller_frequency", controller_frequency, controller_frequency);
// initialize failure detector,Detect if the robot got stucked
failure_detector_.setBufferLength(std::round(cfg_.recovery.oscillation_filter_duration*controller_frequency));
// 初始化,返回一个很大的数
min_distance_custom_obstacle = std::numeric_limits<double>::max();

// set initialized flag
initialized_ = true;
ROS_DEBUG("teb_local_planner plugin initialized.");
}
else
ROS_WARN("teb_local_planner has already been initialized, doing nothing.");
}

关于RobotFootprintModelPtr,看TEB中的footprint模型

calculateMinAndMaxDistances函数和RobotFootprintModelPtr说明同时使用了两种footprint模型


(五) 局部子目标的朝向覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 局部的子目标 (transformed plan最后一个元素)
tf::Stamped<tf::Pose> goal_point;
tf::poseStampedMsgToTF(transformed_plan.back(), goal_point);
robot_goal_.x() = goal_point.getOrigin().getX();
robot_goal_.y() = goal_point.getOrigin().getY();

if (cfg_.trajectory.global_plan_overwrite_orientation)
{
robot_goal_.theta() = estimateLocalGoalOrientation(global_plan_, goal_point,
goal_idx, tf_plan_to_global);
// overwrite/update goal orientation of the transformed plan with the actual goal
// (enable using the plan as initialization)
transformed_plan.back().pose.orientation = tf::createQuaternionMsgFromYaw(robot_goal_.theta());
}
else
robot_goal_.theta() = tf::getYaw(goal_point.getRotation());

// plan only contains the goal, 一般在导航的最后出现size是1的情况
// 用实际的机器人位姿更新 transformed plan 的起点(allows using the plan as initial trajectory)
if (transformed_plan.size()==1)
{ // 插入起点,但不初始化
transformed_plan.insert(transformed_plan.begin(), geometry_msgs::PoseStamped() );
}
tf::poseTFToMsg(robot_pose, transformed_plan.front().pose); // 把起点更新为 机器人当前位姿

参数global_plan_overwrite_orientation为True时,执行estimateLocalGoalOrientation() 将未来一段的规划的姿态角的平均值返回为估计的局部目标姿态

transformed_plan.back()就是所谓的 local goal ,但是遇到需要终点转弯时,local goal的朝向不是最终的goal的朝向,仍然是规划的


swap内存

解决编译时内存不足c++: internal compiler error: Killed (program cc1plus)

1
2
3
4
5
6
7
8
9
10
11
# 创建分区文件, 大小 3G
sudo dd if=/dev/zero of=/swapfile bs=1M count=3000

sudo mkswap /swapfile

sudo swapon /swapfile

# After compiling, you may wish to

sudo swapoff /swapfile
sudo rm /swapfile

其实swap空间就是把硬盘上的空间用来当内存使用,是一种折中的处理方法。


(一) 理论和安装

理论

time elastic band(时间橡皮筋)从elastic band算法(1993年提出)优化而来。TEB的初始轨迹是全局路径传来的transformed_plan,中间插入N个控制橡皮筋形状的路径点(机器人位姿),在点与点之间定义时间差,转换为明确依赖于时间的轨迹,从而实现对机器人的实时控制。由于其模块化的形式,该方法可以扩展目标函数和约束。

TEB的创新在于考虑了动态约束和增加时间信息,如有限的机器人速度和加速度,将规划问题用加权多目标优化框架表示,也是一个bundle adjustment问题。在实时应用中,在线对全局路径进行大规模的计算往往是不可行的,TEB大多数目标函数是局部(local)的,因为它们依赖于一些邻近的中间 configurations。TEB的这种局部性会生成一个稀疏系统矩阵,才可以实现快速有效的大规模优化。作者写第一篇论文时(2012),把约束表示为一个分段连续、可微的代价函数,到了下一篇论文(2013),改成了使用g2o的图优化方法(利文伯格法)。

路径(Path)与轨迹(Trajectory)区别在于,轨迹包含了时间信息。所以TEB算法里老用Trajectory这个词。

各类目标函数

目标函数的梯度可以理解为作用在弹性带上的外力。

TEB的目标函数分为两类:

  • 约束:例如 速度、加速度限制
  • 目标:例如 最快、最短路径或者到障碍的距离

目标函数依赖的参数只取决于邻近位姿的子集。比如:

  • 速度(加速度)约束取决于两个(三个)连续的位姿和一个(两个)时间差。
  • 障碍物的清除和在中间路线上进行导航,只影响单个位姿和它的k个最近的邻居(实际上大约3-5)。
  • 遵守机器人的非完整运动学约束涉及到了两个相邻的位姿,这两个位姿位于一个常曲率的普通圆弧上

但是最快和最短路径是例外,显然是因为它们需要全局地依赖于所有的参数。

TEB同时考虑原始路径的via-point约束和避开障碍。这两个目标函数相似,不同之处在于via-point吸引路径,而障碍物排斥路径。

约束用到了几个顶点(待优化变量),对应的就是几元边。 比如速度约束用到了两个位姿和一个时间差,是三元边。 加速度约束就是五元边。 障碍约束和via-point就是一元边。非完整运动学约束是二元边。
图的构建过程

为什么可以用非线性优化的框架?

平时所说的图优化的最小二乘形式是这样的:
图优化的最小二乘形式

TEB所优化的目标函数是这样的:
TEB的目标函数
我们先把上面的F(x)看成 , 其中

图优化中,顶点是待估计的参数,观测数据构成了边。 TEB的待估计参数就是 ,也就是位姿和时间差的组合。 边表示目标函数 ,并连接多个顶点。

这个图的顶点vertexs是橡皮筋的状态(机器人姿态+时间),起点和终点当然是固定的,图的边是我们自己定义的优化目标函数。默认使用利文伯格法,优化器是g2o::CSparse

g2o框架在批处理模式下优化了TEB,因此每次迭代都会生成一个新的图。在一个机器人控制周期内对轨迹修改步骤进行多次迭代(四个循环包括5次利文伯格的迭代)。 g2o框架提供了两种基于Cholesky decomposition 的求解器: CHOLMODCSparse。默认是CSParse

对优化后的TEB进行验证,检查是否违反了硬约束,如果违反,在这种情况下机器人要么停止,要么重新调用运动规划器。

TEB每次迭代的平均运行时间依赖于TEB的维数,TEB的维数随位姿数(configurations)线性增长。对于10000多个states,对应大约2500种configurations。

Homotopy Planner

TEB中实现了两种规划器,一个就是普通的TebOptimalPlanner,另一个是HomotopyClassPlanner,HomotopyClassPlanner是一种同时优化多个轨迹的方法,由于目标函数的非凸性(非凸函数)会生成一系列最优的候选轨迹,最终在备选局部解的集合中寻求总体最佳候选轨迹,对应论文:《Integrated online trajectory planning and optimization in distinctive topologies》

optimizer本身只能找到局部最优轨迹,有时找到的路径invalid,所以一般都是用HomotopyClassPlanner。HomotopyClassPlanner类像是多个TebOptimalPlanner类实例的组合。HomotopyClassPlanner类中也会实例化一个由全局规划器生成的路径作为参考的对象。除此之外,它还会使用probabilistic roadmap (PRM) methods在障碍物周边采样一些keypoints,将这些keypoints连接起来,去除方向没有朝向目标点的连接和与障碍物重叠的连接。这样就形成了一个网络,然后将起始点和终点接入到这个网络。如下图所示:

使用Depth First Search(深度优先方法)搜索所有可行的路径。将这些路径作为参考,实例化多个TebOptimalPlanner类的实例。采用多线程并行优化,得到多条优化后的路径。将这些路径进行可行性分析,选出代价值最小的最优路径。不得不说HomotopyClassPlanner类里的方法是一个鲁棒性和可靠性更高的方法。


安装

编译teb时报错,或者在没有编译TEB的计算机上通过.so运行TEB时会报错
加载TEB算法时报警.png
libteb_local_planner.so 缺链接库.png
实际的链接情况.png

因为没有安装g2o和ceres,不过不需要自己安装g2o。 先运行sudo apt-get install ros-melodic-teb-local-planner,这样会在/opt/ros里安装g2o,然后把g2o的so文件复制到/usr/local/lib

1
2
sudo cp /opt/ros/melodic/lib/libg2o* /usr/local/lib
sudo cp -r /opt/ros/melodic/include/g2o /usr/local/include

如果自己之前安装的g2o的相关共享库没有清除干净,解决方法为:

  • 删除/usr/local/include/g2o,指令为 sudo rm -rf /usr/local/include/g2o
  • 删除/usr/local/lib下有关libg2o*.so的库文件,先进入目录/usr/local/lib,然后挨个(可多个同时)删除: `sudo rm -rf libg2o.so libg2o_.so libg2o_*.so`

参考:
对ROS局部运动规划器Teb的理解
Trajectory modification considering dynamic constraints of autonomous robots
Efficient Trajectory Optimization using a Sparse Model—使用稀疏模型对有效轨迹进行优化