全局路径规划(二) 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

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


(十二) optimizeTEB 3 AddEdgesObstacles

类的定义

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
/**
* Edge defining the cost function for keeping a minimum distance from obstacles.

* The edge depends on a single vertex \f$ \mathbf{s}_i \f$ and minimizes:
* \f$ \min \textrm{penaltyBelow}( dist2point ) \cdot weight \f$.
* \e dist2point denotes the minimum distance to the point obstacle.
* \e weight can be set using setInformation(). \n
* \e penaltyBelow denotes the penalty function, see penaltyBoundFromBelow()

* @see TebOptimalPlanner::AddEdgesObstacles, TebOptimalPlanner::EdgeInflatedObstacle
* @remarks Do not forget to call setTebConfig() and setObstacle()
*/
class EdgeObstacle : public BaseTebUnaryEdge<1, const Obstacle*, VertexPose>
{
public:
EdgeObstacle()
{
_measurement = NULL;
}
/**
* @brief Set pointer to associated obstacle for the underlying cost function
* @param obstacle 2D position vector containing the position of the obstacle
*/
void setObstacle(const Obstacle* obstacle)
{
_measurement = obstacle;
}
/**
* @brief Set pointer to the robot model
* @param robot_model Robot model required for distance calculation
*/
void setRobotModel(const BaseRobotFootprintModel* robot_model)
{
robot_model_ = robot_model;
}
/**
* @brief Set all parameters at once
* @param cfg TebConfig class
* @param robot_model Robot model required for distance calculation
* @param obstacle 2D position vector containing the position of the obstacle
*/
void setParameters(const TebConfig& cfg,
const BaseRobotFootprintModel* robot_model, const Obstacle* obstacle)
{
cfg_ = &cfg;
robot_model_ = robot_model;
_measurement = obstacle;
}
// cost function
void computeError()
{
ROS_ASSERT_MSG(cfg_ && _measurement && robot_model_, "You must call setTebConfig(), setObstacle() and setRobotModel() on EdgeObstacle()" );
const VertexPose* bandpt = static_cast<const VertexPose*>(_vertices[0]);
/* inline double penaltyBoundFromBelow( const double& var,
const double& a, const double& epsilon)
{
if (var >= a+epsilon)
return 0.;
else
return (-var + (a+epsilon));
} */
double dist = robot_model_->calculateDistance(bandpt->pose(), _measurement);

// Original obstacle cost
_error[0] = penaltyBoundFromBelow( dist,
cfg_->obstacles.min_obstacle_dist,
cfg_->optim.penalty_epsilon );
if (cfg_->optim.obstacle_cost_exponent != 1.0 && cfg_->obstacles.min_obstacle_dist > 0.0)
{
// Optional non-linear cost. Note the max cost (before weighting) is
// the same as the straight line version and that all other costs are
// below the straight line (for positive exponent), so it may be
// necessary to increase weight_obstacle and/or the inflation_weight
// when using larger exponents
_error[0] = cfg_->obstacles.min_obstacle_dist * std::pow(
_error[0] / cfg_->obstacles.min_obstacle_dist,
cfg_->optim.obstacle_cost_exponent);
}
ROS_ASSERT_MSG(std::isfinite(_error[0]),
"EdgeObstacle::computeError() _error[0]=%f\n",
_error[0]);
}
protected:
const BaseRobotFootprintModel* robot_model_;
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
};

一元边,观测值维度为1,测量值类型为Obstacle基类,连接VertexPose顶点。存储了某个障碍物的中心点的三维位置,形状与顶点的位置

只将离某个Pose最近的最左边与最右边的两个Obstacle加入优化中。(因为优化路径不会使得路径相对于障碍物的位置关系发生改变)。同时,还设了一个阈值,凡是离该Pose距离低于某个距离的障碍物也一并加入考虑之中。

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
void TebOptimalPlanner::AddEdgesObstacles(double weight_multiplier)
{
if (cfg_->optim.weight_obstacle==0 || weight_multiplier==0 || obstacles_==nullptr )
return;
// 判断 inflation_dist > min_obstacle_dist
bool inflated = cfg_->obstacles.inflation_dist > cfg_->obstacles.min_obstacle_dist;

Eigen::Matrix<double,1,1> information;
information.fill(cfg_->optim.weight_obstacle * weight_multiplier);

Eigen::Matrix<double,2,2> information_inflated;
information_inflated(0,0) = cfg_->optim.weight_obstacle * weight_multiplier;
information_inflated(1,1) = cfg_->optim.weight_inflation;
information_inflated(0,1) = information_inflated(1,0) = 0;

auto iter_obstacle = obstacles_per_vertex_.begin();

auto create_edge = [inflated, &information, &information_inflated, this] (int index,
const Obstacle* obstacle) {
if (inflated)
{
// 一元边,观测值维度为2,类型为Obstacle基类,连接VertexPose顶点
EdgeInflatedObstacle* dist_bandpt_obst = new EdgeInflatedObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
// 信息矩阵为2x2对角阵,(0,0) = weight_obstacle, (1,1) = weight_inflation
dist_bandpt_obst->setInformation(information_inflated);
dist_bandpt_obst->setParameters(*cfg_, robot_model_.get(), obstacle);
optimizer_->addEdge(dist_bandpt_obst);
}
else
{
// 一元边,观测值维度为1,测量值类型为Obstacle基类,连接VertexPose顶点
EdgeObstacle* dist_bandpt_obst = new EdgeObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
// 信息矩阵为 1x1 weight_obstacle * weight_multiplier
dist_bandpt_obst->setInformation(information);
dist_bandpt_obst->setParameters(*cfg_, robot_model_.get(), obstacle);
optimizer_->addEdge(dist_bandpt_obst);
};
};

/* iterate all teb points, skipping the last and, if the
EdgeVelocityObstacleRatio edges should not be created, the first one too*/
const int first_vertex = cfg_->optim.weight_velocity_obstacle_ratio == 0 ? 1 : 0;
for (int i = first_vertex; i < teb_.sizePoses() - 1; ++i)
{
double left_min_dist = std::numeric_limits<double>::max();
double right_min_dist = std::numeric_limits<double>::max();
ObstaclePtr left_obstacle;
ObstaclePtr right_obstacle;

const Eigen::Vector2d pose_orient = teb_.Pose(i).orientationUnitVec();

// iterate obstacles
for (const ObstaclePtr& obst : *obstacles_)
{
// we handle dynamic obstacles differently below
if(cfg_->obstacles.include_dynamic_obstacles && obst->isDynamic())
continue;

// calculate distance to robot model
double dist = robot_model_->calculateDistance(teb_.Pose(i), obst.get());

// force considering obstacle if really close to the current pose
if (dist < cfg_->obstacles.min_obstacle_dist*cfg_->obstacles.obstacle_association_force_inclusion_factor)
{
iter_obstacle->push_back(obst);
continue;
}
// cut-off distance
if (dist > cfg_->obstacles.min_obstacle_dist*cfg_->obstacles.obstacle_association_cutoff_factor)
continue;

// determine side (left or right) and assign obstacle if closer than the previous one
if (cross2d(pose_orient, obst->getCentroid()) > 0) // left
{
if (dist < left_min_dist)
{
left_min_dist = dist;
left_obstacle = obst;
}
}
else
{
if (dist < right_min_dist)
{
right_min_dist = dist;
right_obstacle = obst;
}
}
}
// create obstacle edges
if (left_obstacle)
iter_obstacle->push_back(left_obstacle);
if (right_obstacle)
iter_obstacle->push_back(right_obstacle);

// continue here to ignore obstacles for the first pose, but use them later to create the EdgeVelocityObstacleRatio edges
if (i == 0)
{
++iter_obstacle;
continue;
}
// create obstacle edges
for (const ObstaclePtr obst : *iter_obstacle)
create_edge(i, obst.get());
++iter_obstacle;
}
}

误差函数的计算

两个类根据机器人的轮廓模型计算当前Pose与某个障碍物的距离dist,至于如何计算,属于另一个话题。 然后EdgeObstacle的error如下

1
error = (dist > min_obstacle_dist+ε) ? 0 : (min_obstacle_dist+ε) - dist

EdgeInflatedObstacle则是

1
2
3
4
# Original "straight line" obstacle cost. The max possible value
# before weighting is min_obstacle_dist,和 EdgeObstacle的相同
error[0] = (dist > min_obstacle_dist+ε) ? 0 : (min_obstacle_dist+ε) - dist
error[1] = (dist > inflation_dist) ? 0 : (inflation_dist) - dist

但是这两个类的computeError在error[0]赋值结束后又补充这样一段:

1
2
3
4
5
6
7
8
9
10
if (cfg_->optim.obstacle_cost_exponent != 1.0 && cfg_->obstacles.min_obstacle_dist > 0.0)
{
/* Optional 非线性代价. Note the max cost (before weighting) is
the same as the straight line version and that all other costs are
below the straight line (for positive exponent), so it may be
necessary to increase weight_obstacle and/or the inflation_weight
when using larger exponents */
_error[0] = cfg_->obstacles.min_obstacle_dist * std::pow(
_error[0] / cfg_->obstacles.min_obstacle_dist, cfg_->optim.obstacle_cost_exponent);
}

涉及参数obstacle_cost_exponent,不明白这个公式是怎么来的


(十六) saturateVelocity
1
2
3
4
// 如果优化结果违反约束(由于是软约束,所以是可能的), Saturate 速度
saturateVelocity(cmd_vel.linear.x, cmd_vel.linear.y, cmd_vel.angular.z,
cfg_.robot.max_vel_x, cfg_.robot.max_vel_y,
cfg_.robot.max_vel_theta, cfg_.robot.max_vel_x_backwards);

saturateVelocity

若规划出来的速度超过限制速度,则令其等于最大限制速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Limit translational velocity for forward driving
if (vx > max_vel_x)
vx = max_vel_x;

// limit strafing velocity
if (vy > max_vel_y)
vy = max_vel_y;
else if (vy < -max_vel_y)
vy = -max_vel_y;

if(vx>= -0.12 && vx< 0)
{
vx = -0.12;
}
else if(vx< -0.12)
{
planner_->clearPlanner();
}
// Limit angular velocity
if (omega > max_vel_theta)
omega = max_vel_theta;
else if (omega < -max_vel_theta)
omega = -max_vel_theta;

剩余部分也很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 对汽车机器人,convert rot-vel to steering angle if desired
// The min_turning_radius is allowed to be slighly smaller since it is a soft-constraint
// and opposed to the other constraints not affected by penalty_epsilon. The user might add a safety margin to the parameter itself.
if (cfg_.robot.cmd_angle_instead_rotvel)
{ // 目前是false,省略
}
// a feasible solution should be found, reset counter
no_infeasible_plans_ = 0;
// 保存上一个速度命令 (for recovery analysis etc.)
last_cmd_ = cmd_vel;

// Now visualize everything
planner_->visualize();
visualization_->publishObstacles(obstacles_);
visualization_->publishViaPoints(via_points_);
visualization_->publishGlobalPlan(global_plan_);
return true; // 函数 computeVelocityCommands 结束

可视化部分,见下一篇


(十五) 输出速度命令

调用是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
// Get the velocity command for this sampling interval
if (!planner_->getVelocityCommand(cmd_vel.linear.x, cmd_vel.linear.y, cmd_vel.angular.z,
cfg_.trajectory.control_look_ahead_poses, robot_vel_.linear.x,
robot_vel_.angular.z) )
{
planner_->clearPlanner();
ROS_WARN("TebLocalPlannerROS: velocity command invalid. Resetting planner...");
++no_infeasible_plans_; // increase number of infeasible solutions in a row
time_last_infeasible_plan_ = ros::Time::now();
last_cmd_ = cmd_vel;
return false;
}

getVelocityCommand

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
bool TebOptimalPlanner::getVelocityCommand(double& vx, double& vy, double& omega, 
int look_ahead_poses, double current_vx, double current_vtheta) const
{
// TimedElasticBand teb_; //!< Actual trajectory object
if (teb_.sizePoses()<2)
{
ROS_ERROR("TebOptimalPlanner::getVelocityCommand():
The trajectory contains less than 2 poses. Make sure to init and
optimize/plan the trajectory fist." );
vx = 0;
vy = 0;
omega = 0;
return false;
}
// teb_.sizePoses() 在规划中一直很大,除非是接近终点时候
look_ahead_poses = std::max(1, std::min(look_ahead_poses, teb_.sizePoses() - 1));
double dt = 0.0;
/* double& TimeDiff(int index)
{
ROS_ASSERT(index<sizeTimeDiffs());
return timediff_vec_.at(index)->dt();
} */
ROS_INFO("\033[45;37m before counter, look_ahead_poses: %f \033[0m", look_ahead_poses);
for(int counter = 0; counter < look_ahead_poses; ++counter)
{
dt += teb_.TimeDiff(counter);
// TODO: change to look-ahead time? Refine trajectory ?
if(dt >= cfg_->trajectory.dt_ref * look_ahead_poses)
{
look_ahead_poses = counter + 1;
break;
}
}
if (dt<=0)
{
ROS_ERROR("getVelocityCommand() timediff<=0 is invalid!");
vx = 0; vy = 0; omega = 0;
return false;
}
// Get velocity from the first two configurations
extractVelocity(teb_.Pose(0), teb_.Pose(look_ahead_poses), dt, vx, vy, omega);

double v_new = vx;
double k = (v_new - current_vx) * (v_new - current_vx) /
( (v_new - current_vx) * (v_new - current_vx)
+ (cfg_->robot.acc_lim_x * dt) * (cfg_->robot.acc_lim_x * dt) );
vx = current_vx + k * (v_new - current_vx);
return true;
}

参数control_look_ahead_poses增大,会让look_ahead_poses变量增大,dt也会增大
间接导致最终的look_ahead_poses也比较大。如果参数dt_ref也增大,那此二者就更大了。

这里涉及到的公式太复杂了,实在看不出为什么增大 control_look_ahead_poses 会让速度平滑了

extractVelocity

提取机器人的速度在本体坐标系下的表示

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
// pose2 是 teb_.Pose(look_ahead_poses)
void TebOptimalPlanner::extractVelocity(const PoseSE2& pose1, const PoseSE2& pose2,
double dt, double& vx, double& vy, double& omega) const
{
if (dt == 0)
{
vx = 0; vy = 0; omega = 0;
return;
}
Eigen::Vector2d deltaS = pose2.position() - pose1.position();

if (cfg_->robot.max_vel_y == 0) // nonholonomic robot
{
Eigen::Vector2d conf1dir( cos(pose1.theta()), sin(pose1.theta()) );
// translational velocity
double dir = deltaS.dot(conf1dir);
// 无后退的情况下,sign函数都返回 1
vx = (double) g2o::sign(dir) * deltaS.norm() / dt;
vy = 0;
}
else // holonomic robot ...... 省略

// rotational velocity
double orientdiff = g2o::normalize_theta(pose2.theta() - pose1.theta());
omega = orientdiff / dt;
}

前面两个参数的增大导致deltaS.norm()dt都增大,从这里直接看不出vx是增大还是减小了,实际运行后,通过日志可以看到vx整体增大了。


1
2
3
4
5
6
inline int sign(T x)
{
if (x > 0) return 1;
else if (x < 0) return -1;
else return 0;
}

(四)configureBackupModes 1 处理 goal_idx 和 transformed_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
void TebLocalPlannerROS::configureBackupModes(
std::vector<geometry_msgs::PoseStamped>& transformed_plan,
int& goal_idx)
{
ros::Time current_time = ros::Time::now();
// reduced horizon backup mode 上篇博客所列的第四种情况下

// we do not reduce if the goal is already selected , because the orientation
// might change -> can introduce oscillations
// no_infeasible_plans_ 改成 no_feasible_plans_ 比较合适
if (cfg_.recovery.shrink_horizon_backup &&
goal_idx < (int)transformed_plan.size()-1 &&
(no_infeasible_plans_>0 || (current_time - time_last_infeasible_plan_).toSec() <
// keep short horizon for at least a few seconds
cfg_.recovery.shrink_horizon_min_duration ) )
{
ROS_INFO_COND(no_infeasible_plans_==1,
"Activating reduced horizon backup mode for at least %.2f sec (infeasible
trajectory detected).",
cfg_.recovery.shrink_horizon_min_duration);

// Shorten horizon if requested reduce to 50 percent:
int horizon_reduction = goal_idx/2;

if (no_infeasible_plans_ > 9)
{
ROS_INFO_COND(no_infeasible_plans_==10, "Infeasible trajectory detected
10 times in a row: further reducing horizon...");
horizon_reduction /= 2;
}
// we have a small overhead (过头) here, since we already transformed 50%
// more of the trajectory. But that's ok for now, since we do not
// need to make transformGlobalPlan more complex
// reduced horizon 应当很少发生
int new_goal_idx_transformed_plan = int(transformed_plan.size()) - horizon_reduction - 1;
goal_idx -= horizon_reduction;

// 从 transformed_plan 的 new_goal_idx 开始,一直删除到尾部
if (new_goal_idx_transformed_plan >0 && goal_idx >= 0)
transformed_plan.erase(transformed_plan.begin() + new_goal_idx_transformed_plan, transformed_plan.end());
else
goal_idx += horizon_reduction; // this should not happen, but safety first
}
// ......第二部分......
}
  • shrink_horizon_backup: true. 在自动检测到问题(plan not feasible)的情况下,允许planner临时缩小范围(50%). TEB将以更近的点作为规划目标,尝试重新规划出可行路径。调试时可关闭,以在可视化界面上观察原出错路径。另请参阅参数shrink_horizo​​n_min_duration

  • shrink_horizon_min_duration: 10,单位秒。 如果检测到不可行的轨迹,激活缩小的水平线后备模式,本参数为其最短持续时间。个人怀疑这个参数无意义,因为它更新值的地方也都有no_infeasible_plans_变量的更新,判断时,只根据no_infeasible_plans_即可,详见代码configureBackupModes


(四)configureBackupModes 2 FailureDetector和振荡恢复

configureBackupModes 第二部分

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
// detect and resolve oscillations
if (cfg_.recovery.oscillation_recovery)
{
double max_vel_theta;
// 车一般不能倒退,所以基本是 max_vel_x
double max_vel_current = last_cmd_.linear.x >= 0 ? cfg_.robot.max_vel_x : cfg_.robot.max_vel_x_backwards;
if (cfg_.robot.min_turning_radius!=0 && max_vel_current>0)
max_vel_theta = std::max( max_vel_current/std::abs(cfg_.robot.min_turning_radius), cfg_.robot.max_vel_theta );
else
max_vel_theta = cfg_.robot.max_vel_theta;

failure_detector_.update(last_cmd_, cfg_.robot.max_vel_x, cfg_.robot.max_vel_x_backwards, max_vel_theta,
cfg_.recovery.oscillation_v_eps, cfg_.recovery.oscillation_omega_eps);

bool oscillating = failure_detector_.isOscillating();
// 距离上次振荡过了多久,若小于配置值(10秒),说明最近也振荡了
bool recently_oscillated = (ros::Time::now()-time_last_oscillation_).toSec() < cfg_.recovery.oscillation_recovery_min_duration;
// 如果现在是振荡状态
if (oscillating)
{
if (!recently_oscillated) // 最近的状态不是振荡
{
// save current turning direction
// 以下其实都是对 last_preferred_rotdir_ 赋值
if (robot_vel_.angular.z > 0)
last_preferred_rotdir_ = RotType::left;
else
last_preferred_rotdir_ = RotType::right;
ROS_WARN("TebLocalPlannerROS: possible oscillation (of the robot
or its local plan) detected. Activating recovery strategy
(prefer current turning direction during optimization) ");
}
time_last_oscillation_ = ros::Time::now();
// 对 prefer_rotdir_ 赋值,最终影响 AddEdgesPreferRotDir
planner_->setPreferredTurningDir(last_preferred_rotdir_);
}
// 现在和最近都没有振荡 clear recovery behavior
else if (!recently_oscillated && last_preferred_rotdir_ != RotType::none)
{
last_preferred_rotdir_ = RotType::none;
planner_->setPreferredTurningDir(last_preferred_rotdir_);
ROS_INFO("TebLocalPlannerROS: oscillation recovery disabled/expired.");
}
}

但是如果机器人确实stucked,最终会在move_base里Abort

setPreferredTurningDir

设置期望的初始转弯方向 (也就是惩罚相反的方向)。A desired (initial) turning direction might be specified in case the planned trajectory oscillates between two solutions (in the same equivalence class!) with similar cost. 配置参数以调整惩罚的权重,Initial 说明惩罚只适用于trajectory的前几个位姿

参数dir可以是RotType::left, RotType::right, RotType::none

代码实际调用的是:

1
2
3
4
5
6
7
8
void HomotopyClassPlanner::setPreferredTurningDir(RotType  dir)
{
// set preferred turning dir for all TEBs
for (TebOptPlannerContainer::const_iterator it_teb = tebs_.begin(); it_teb != tebs_.end(); ++it_teb)
{
(*it_teb)->setPreferredTurningDir(dir);
}
}

结果还是TebOptimalPlanner的同名成员函数,赋值而已

1
virtual void setPreferredTurningDir(RotType dir) {  prefer_rotdir_ = dir;  }

赋值完了,prefer_rotdir_会在AddEdgesPreferRotDir里产生影响

AddEdgesPreferRotDir

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
void TebOptimalPlanner::AddEdgesPreferRotDir()
{
/* roesmann 注释: these edges can result in odd predictions, in particular
we can observe a substantional mismatch between open- and closed-loop planning
leading to a poor control performance.
目前这个功能只用于振荡恢复,短期使用这个约束不会有重大影响
可能让机器人脱离振荡 */
if (prefer_rotdir_ == RotType::none || cfg_->optim.weight_prefer_rotdir == 0)
return;
// 必须定义好转向
if (prefer_rotdir_ != RotType::right && prefer_rotdir_ != RotType::left)
{
ROS_WARN("TebOptimalPlanner::AddEdgesPreferRotDir(): unsupported RotType selected. Skipping edge creation.");
return;
}
// create edge for satisfiying kinematic constraints
Eigen::Matrix<double,1,1> information_rotdir;
information_rotdir.fill(cfg_->optim.weight_prefer_rotdir);
// 目前只使用前三个旋转
for (int i=0; i < teb_.sizePoses()-1 && i < 3; ++i)
{
EdgePreferRotDir* rotdir_edge = new EdgePreferRotDir;
rotdir_edge->setVertex(0, teb_.PoseVertex(i));
rotdir_edge->setVertex(1, teb_.PoseVertex(i+1));
rotdir_edge->setInformation(information_rotdir);

if (prefer_rotdir_ == RotType::left)
rotdir_edge->preferLeft();
else if (prefer_rotdir_ == RotType::right)
rotdir_edge->preferRight();

optimizer_->addEdge(rotdir_edge);
}
}

调用时在configureBackupModes第二部分,那里是对prefer_rotdir_赋值,

FailureDetector

FailureDetector的代码在TEB的recovery_behaviors.cppFailureDetector检测机器人是否 stucked或者振荡,分析last N(buffer的大小)个速度命令,看这些线速度和角速度的平均值是否小于oscillation_v_epsoscillation_omega_eps

构造函数和析构全是空。

buffer

1
2
3
// Set buffer length (measurement history)
// length: number of measurements to be kept
void setBufferLength(int length) { buffer_.set_capacity(length); }

调用:

1
2
failure_detector_.setBufferLength(std::round(
cfg_.oscillation_filter_duration * controller_frequency ) );

默认是10*30,其实比较大,可以降低oscillation_filter_duration

1
2
// Clear buffer, 置为不振荡状态
void clear();

FailureDetector::update

先看调用,只有第一个参数是速度控制量,其他都是配置参数:

1
2
3
4
failure_detector_.update(last_cmd_, cfg_.robot.max_vel_x, cfg_.robot.max_vel_x_backwards, 
max_vel_theta, cfg_.recovery.oscillation_v_eps, cfg_.recovery.oscillation_omega_eps);

bool oscillating = failure_detector_.isOscillating();

源码其实也简单

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
void FailureDetector::update(const geometry_msgs::Twist& twist, double v_max, double v_backwards_max, double omega_max, double v_eps, double omega_eps)
{
if (buffer_.capacity() == 0)
return;
VelMeasurement measurement;
/*
struct VelMeasurement
{
double v = 0;
double omega = 0;
}; */
measurement.v = twist.linear.x; // 不考虑 y 方向速度
measurement.omega = twist.angular.z;

if (measurement.v > 0 && v_max>0)
measurement.v /= v_max;
else if (measurement.v < 0 && v_backwards_max > 0)
measurement.v /= v_backwards_max;

if (omega_max > 0)
measurement.omega /= omega_max;

buffer_.push_back(measurement);
// immediately compute new state
detect(v_eps, omega_eps);
}

detect

  • v_eps: (0,1)内的normalized 线速度的平均值的阈值,默认0.1
  • omega_eps: (0,1)内的normalized 角速度的平均值,默认0.1

如果机器人stucked则返回true,否则false

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
bool detect(double v_eps, double omega_eps)
{
oscillating_ = false;
//至少有一半的速度数据才检测,也就是过一点时间才判断
if (buffer_.size() < buffer_.capacity()/2)
return false;
double n = (double)buffer_.size();
// 计算buffer内线速度和角速度的均值
double v_mean=0;
double omega_mean=0;
int omega_zero_crossings = 0;
for (int i=0; i < n; ++i)
{
v_mean += buffer_[i].v;
omega_mean += buffer_[i].omega;
if ( i>0 && g2o::sign(buffer_[i].omega) != g2o::sign(buffer_[i-1].omega) )
++omega_zero_crossings;
}
v_mean /= n;
omega_mean /= n;
if (std::abs(v_mean) < v_eps && std::abs(omega_mean) < omega_eps
&& omega_zero_crossings>1 )
{
oscillating_ = true;
}
return oscillating_;
}

参数

我用的参数值如下:

1
2
3
4
5
6
7
shrink_horizon_backup = true;
shrink_horizon_min_duration = 10: # 默认10
oscillation_recovery = true;
oscillation_v_eps = 0.1; # 线速度和角速度设置0.1已经足够
oscillation_omega_eps = 0.1;
oscillation_recovery_min_duration = 5;
oscillation_filter_duration = 6 # 默认10,这个大了会使buffer太大

要出现振荡,最直观的就是车左右摇摆或者车前后小步行走,因为代码中没有绝对值,否则朝一个方向小幅行走也算振荡了。实际中当然是不允许这种奇怪的行为


(十一) optimizeTEB 2 初始化g2o求解器和添加约束

g2o边和顶点的定义

所有边都在头文件base_teb_edges.h中,顶点就是g2o_types\vertex_timediff.hg2o_types\vertex_pose.h。两个顶点都比较简单,值得注意的是vertex_timediff.hsetToOriginImpl

1
2
3
4
virtual void setToOriginImpl()
{
_estimate = 0.1; // 不是0,不明白为什么
}

一元边:

1
2
template <int D, typename E, typename VertexXi>
class BaseTebUnaryEdge : public g2o::BaseUnaryEdge<D, E, VertexXi>

  • EdgeViaPoint:以指定的经过点位置与待优化位姿(顶点)的误差作为目标。(保证轨迹会经过指定点)

  • EdgeObstacle:以障碍物与待优化位姿(顶点)的距离经过惩罚函数后的输出作为目标(保证离障碍物大于一定距离)

  • EdgeTimeOptimal:以时间间隔dt最小作为目标(保证时间最短轨迹)

二元边
二元边

1
2
template <int D, typename E, typename VertexXi, typename VertexXj>
class BaseTebBinaryEdge : public g2o::BaseBinaryEdge<D, E, VertexXi, VertexXj>

主要是考虑了运动学的约束关系(车的相邻的两个位姿态必须落在一个圆弧上)。二元边的两个顶点分别是两个位姿,还限制了最小转弯半径。

多元边(构造函数中resize顶点的个数):

1
2
template <int D, typename E>
class BaseTebMultiEdge : public g2o::BaseMultiEdge<D, E>

  • EdgeVelocity:以实际速度和限定最大速度经过惩罚函数的输出作为目标函数,顶点为相邻的位姿和时间间隔。(限制实际速度不超过最大,程序中引入了sigmoid函数决定速度的符号(根据论文内的说法,引入此函数是因为优化算法只能求解连续函数)

  • Edgevelocityholonomic:与EdgeVelocity的区别在于,ds是机器人本体坐标系下的,然后去求速度在本体坐标系下的表示。其余思路一致


1
2
3
class VertexPose : public g2o::BaseVertex<3, PoseSE2 >

class VertexTimeDiff : public g2o::BaseVertex<1, double>

初始化 optimizer

planner_初始化在if (cfg_.hcp.enable_homotopy_class_planning)的判断里,然后实际调用TebOptimalPlanner::initialize或者HomotopyClassPlanner::initialize,前者调用了TebOptimalPlanner::initOptimizer

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
boost::shared_ptr<g2o::SparseOptimizer> TebOptimalPlanner::initOptimizer()
{
// Call register_g2o_types once, even for multiple TebOptimalPlanner instances (thread-safe)
static boost::once_flag flag = BOOST_ONCE_INIT;
// 将为TEB定义的顶点和边注册到g2o::Factory
boost::call_once(&registerG2OTypes, flag);

// allocating the optimizer
boost::shared_ptr<g2o::SparseOptimizer> optimizer = boost::make_shared<g2o::SparseOptimizer>();
// linear solver utilized for optimization
// 针对稀疏矩阵,写死为 g2o::LinearSolverCSparse
// typedef g2o::LinearSolverCSparse<TEBBlockSolver::PoseMatrixType> TEBLinearSolver;
TEBLinearSolver* linearSolver = new TEBLinearSolver(); // see typedef in optimization.h
linearSolver->setBlockOrdering(true);

// block solver utilized for optimization
// typedef g2o::BlockSolver< g2o::BlockSolverTraits<-1, -1> > TEBBlockSolver;
TEBBlockSolver* blockSolver = new TEBBlockSolver(linearSolver);
g2o::OptimizationAlgorithmLevenberg* solver =
new g2o::OptimizationAlgorithmLevenberg(blockSolver);

optimizer->setAlgorithm(solver);
optimizer->initMultiThreading(); // required for >Eigen 3.1

return optimizer;
}

注意算法求解器写死为 利文伯格法g2o::LinearSolverCSparse


构建超图 buildGraph

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
bool TebOptimalPlanner::buildGraph(double weight_multiplier)
{
// 构建图优化问题。若顶点和边非空,则 add
if (!optimizer_->edges().empty() || !optimizer_->vertices().empty())
{
ROS_WARN("Cannot build graph, because it is not empty. Call graphClear() ");
return false;
}
// 增加图的顶点,TimedElasticBand类型的路径点和dt为顶点
// 添加顺序会影响 稀疏矩阵的结构,进而影响优化效率
AddTEBVertices();
// add Edges (local cost functions)
// legacy_obstacle_association
/*已经修改了将轨迹姿势与优化障碍联系起来的策略(参见changelog)。
您可以通过将此参数设置为true来切换到 旧策略。
旧策略:对于每个障碍,找到最近的TEB姿势; 新策略:对于每个teb姿势,只找到“相关”的障碍 */
if (cfg_->obstacles.legacy_obstacle_association) // false
AddEdgesObstaclesLegacy(weight_multiplier);
else
AddEdgesObstacles(weight_multiplier); // 参数写死为 2

if (cfg_->obstacles.include_dynamic_obstacles)
AddEdgesDynamicObstacles();
/* error为其连接的Pose顶点的位置到这个Viapoint的距离的模长。
信息矩阵为1x1的矩阵,其值为 weight_viapoint
添加全局路径约束, 取决于参数 global_plan_viapoint_sep */
AddEdgesViaPoints();

// 对于TimedElasticBand类型中的位姿顶点序列-1条边,设置该边的顶点和权重矩阵,添加该边
AddEdgesVelocity();
AddEdgesAcceleration();
AddEdgesJerk(); // 实际不执行
// error直接就是连接的VertexTimeDiff的dt本身,信息矩阵为1x1矩阵,其值为weight_optimaltime
AddEdgesTimeOptimal(); //添加时间约束

if (cfg_->robot.min_turning_radius == 0 || cfg_->optim.weight_kinematics_turning_radius == 0)
AddEdgesKinematicsDiffDrive(); // differential drive robot
else
AddEdgesKinematicsCarlike(); // carlike robot since the turning radius is bounded from below.

AddEdgesPreferRotDir(); // 实际不执行
return true;
}

添加 vertex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void TebOptimalPlanner::AddTEBVertices()
{
// add vertices to graph
ROS_DEBUG_COND(cfg_->optim.optimization_verbose, "Adding TEB vertices ...");
unsigned int id_counter = 0; // used for vertices ids
// std::vector<ObstContainer> obstacles_per_vertex_; 对应n-1 initial vertices
obstacles_per_vertex_.resize(teb_.sizePoses());
auto iter_obstacle = obstacles_per_vertex_.begin();
for (int i=0; i < teb_.sizePoses(); ++i)
{
// 两种 vertex 都要添加
teb_.PoseVertex(i)->setId(id_counter++);
optimizer_->addVertex(teb_.PoseVertex(i) );
if (teb_.sizeTimeDiffs()!=0 && i<teb_.sizeTimeDiffs() )
{
teb_.TimeDiffVertex(i)->setId(id_counter++);
optimizer_->addVertex(teb_.TimeDiffVertex(i) );
}
iter_obstacle->clear();
// 这里和 AddEdgesObstacles 有关
(iter_obstacle++)->reserve( obstacles_->size() );
}
}

这里的Vertex没有调用setEstimate函数,不设置初值。

添加约束

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::AddEdgesVelocity()
{
// 只看 non-holonomic robot
if ( cfg_->optim.weight_max_vel_x==0 && cfg_->optim.weight_max_vel_theta==0)
return;

int n = teb_.sizePoses();
Eigen::Matrix<double,2,2> information;
// 对角线元素
information(0,0) = cfg_->optim.weight_max_vel_x;
information(1,1) = cfg_->optim.weight_max_vel_theta;
information(0,1) = 0.0;
information(1,0) = 0.0;
// 对所有的位姿,信息矩阵都是相同的
for (int i=0; i < n - 1; ++i)
{
EdgeVelocity* velocity_edge = new EdgeVelocity;
// 速度约束有三个顶点, 多元边
velocity_edge->setVertex(0,teb_.PoseVertex(i));
velocity_edge->setVertex(1,teb_.PoseVertex(i+1));
velocity_edge->setVertex(2,teb_.TimeDiffVertex(i));

velocity_edge->setInformation(information);
velocity_edge->setTebConfig(*cfg_);
optimizer_->addEdge(velocity_edge);
}
}

error有两项,分别是线速度与线速度线速度是否在设定好的区间内。

只要看懂了论文和g2o的应用套路,这里的添加的edge大部分都能看懂。比较复杂而且重要的是AddEdgesObstaclesAddEdgesViaPointsvia_point规定了轨迹应当经过这些点,否则会产生相应的cost。via_point边会与原规划的路径中与其距离最近的Pose顶点相连。

对于加速度约束,还要注意 vel_start_.firstvel_goal_.first,前者一般会添加,后者基本是false,信息矩阵一直是同一个。




1
2
3
4
5
6
7
8
9
10
11
void TebOptimalPlanner::clearGraph()
{
// clear optimizer states
if (optimizer_)
{
// neccessary, because optimizer->clear deletes pointer-targets
// therefore it deletes TEB states
optimizer_->vertices().clear();
optimizer_->clear();
}
}

局部规划可以让路径显示graph,在rviz中配置

参考:
TEB中的g2o问题的设置