TEB(Timed Elastic Band) 是一个基于图优化的局部路径规划算法,具有较好的动态避障能力,在ROS1/ROS2的导航框架中均被采用。该图优化以g2o优化框架实现,以机器人在各个离散时刻的位姿和离散时刻之间的时间间隔为顶点,约束其中的加速度、速度,到达时间和到障碍物的距离等值,优化目标是使得机器人在其运动学约束下绕开障碍物最快到达目标点,实现了高效的局部路径规划功能。整个算法流程如下,其中的重点是图结构的构建。
TEB Github
一,加入顶点
顶点包括:离散时刻机器人位姿和离散时间间隔。
void TebOptimalPlanner::AddTEBVertices()
{
// add vertices to graph
RCLCPP_DEBUG_EXPRESSION(node_->get_logger(), cfg_->optim.optimization_verbose, "Adding TEB vertices ...");
unsigned int id_counter = 0; // used for vertices ids
obstacles_per_vertex_.resize(teb_.sizePoses());
auto iter_obstacle = obstacles_per_vertex_.begin();
for (int i=0; i<teb_.sizePoses(); ++i)
{
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();
(iter_obstacle++)->reserve(obstacles_->size());
}
}
二,构建边
bool TebOptimalPlanner::buildGraph(double weight_multiplier)
{
if (!optimizer_->edges().empty() || !optimizer_->vertices().empty())
{
RCLCPP_WARN(node_->get_logger(), "Cannot build graph, because it is not empty. Call graphClear()!");
return false;
}
optimizer_->setComputeBatchStatistics(cfg_->recovery.divergence_detection_enable);
// add TEB vertices
AddTEBVertices();
// add Edges (local cost functions)
// 增加与静态障碍物相关的边
if (cfg_->obstacles.legacy_obstacle_association)
AddEdgesObstaclesLegacy(weight_multiplier);
else
AddEdgesObstacles(weight_multiplier);
// 增加与动态障碍物有关的边
if (cfg_->obstacles.include_dynamic_obstacles)
AddEdgesDynamicObstacles();
// 增加约束到中间点的边,只关注目标点时,可以不增加这类边
AddEdgesViaPoints();
// 增加速度约束边
AddEdgesVelocity();
// 增加加速度约束边
AddEdgesAcceleration();
// 增加总耗时约束边
AddEdgesTimeOptimal();
// 增加最短路径边, 可不增加
AddEdgesShortestPath();
// 针对差速底盘和阿克曼底盘的运动学约束边
if (cfg_->robot.min_turning_radius == 0 || cfg_->optim.weight_kinematics_turning_radius == 0)
AddEdgesKinematicsDiffDrive(); // we have a differential drive robot
else
AddEdgesKinematicsCarlike(); // we have a carlike robot since the turning radius is bounded from below.
// 增加偏好转向的边, 可不增加
AddEdgesPreferRotDir();
// 增加接近障碍物时速度约束边
if (cfg_->optim.weight_velocity_obstacle_ratio > 0)
AddEdgesVelocityObstacleRatio();
return true;
}
图优化涉及到多种约束边时,各自的权重很重要,代码中对各个权重做了注释,也能很好地看出各个约束边的作用。
double weight_max_vel_x; //!< Optimization weight for satisfying the maximum allowed translational velocity
double weight_max_vel_y; //!< Optimization weight for satisfying the maximum allowed strafing velocity (in use only for holonomic robots)
double weight_max_vel_theta; //!< Optimization weight for satisfying the maximum allowed angular velocity
double weight_acc_lim_x; //!< Optimization weight for satisfying the maximum allowed translational acceleration
double weight_acc_lim_y; //!< Optimization weight for satisfying the maximum allowed strafing acceleration (in use only for holonomic robots)
double weight_acc_lim_theta; //!< Optimization weight for satisfying the maximum allowed angular acceleration
double weight_kinematics_nh; //!< Optimization weight for satisfying the non-holonomic kinematics
double weight_kinematics_forward_drive; //!< Optimization weight for forcing the robot to choose only forward directions (positive transl. velocities, only diffdrive robot)
double weight_kinematics_turning_radius; //!< Optimization weight for enforcing a minimum turning radius (carlike robots)
double weight_optimaltime; //!< Optimization weight for contracting the trajectory w.r.t. transition time
double weight_shortest_path; //!< Optimization weight for contracting the trajectory w.r.t. path length
double weight_obstacle; //!< Optimization weight for satisfying a minimum separation from obstacles
double weight_inflation; //!< Optimization weight for the inflation penalty (should be small)
double weight_dynamic_obstacle; //!< Optimization weight for satisfying a minimum separation from dynamic obstacles
double weight_dynamic_obstacle_inflation; //!< Optimization weight for the inflation penalty of dynamic obstacles (should be small)
double weight_velocity_obstacle_ratio; //!< Optimization weight for satisfying a maximum allowed velocity with respect to the distance to a static obstacle
double weight_viapoint; //!< Optimization weight for minimizing the distance to via-points
double weight_prefer_rotdir; //!< Optimization weight for preferring a specific turning direction (-> currently only activated if an oscillation is detected, see 'oscillation_recovery'
double weight_adapt_factor; //!< Some special weights (currently 'weight_obstacle') are repeatedly scaled by this factor in each outer TEB iteration (weight_new = weight_old*factor); Increasing weights iteratively instead of setting a huge value a-priori leads to better numerical conditions of the underlying optimization problem.
double obstacle_cost_exponent; //!< Exponent for nonlinear obstacle cost (cost = linear_cost * obstacle_cost_exponent). Set to 1 to disable nonlinear cost (default)
三,避障相关约束
TEB通过让位姿远离障碍物来达到避障的目的,并将障碍物分为动态障碍物和静态障碍物处理。
if (cfg_->obstacles.legacy_obstacle_association)
AddEdgesObstaclesLegacy(weight_multiplier);
else
AddEdgesObstacles(weight_multiplier);
if (cfg_->obstacles.include_dynamic_obstacles)
AddEdgesDynamicObstacles();
参数Legacy_obstacle_association:
True: 对于每个障碍物,找到最近的TEB位姿,使其距离大于设定距离;
False: 对于每个TEB位姿,找到相关的障碍物,使其距离大于设定距离。
void TebOptimalPlanner::AddEdgesObstaclesLegacy(double weight_multiplier)
{
if (cfg_->optim.weight_obstacle==0 || weight_multiplier==0 || obstacles_==nullptr)
return; // weight_multiplier 被固定设为1.0
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;
bool inflated = cfg_->obstacles.inflation_dist > cfg_->obstacles.min_obstacle_dist;
// 如果障碍物膨胀距离大于距离障碍物的最小距离,才会考虑膨胀
for (ObstContainer::const_iterator obst = obstacles_->begin(); obst != obstacles_->end(); ++obst)
{
if (cfg_->obstacles.include_dynamic_obstacles && (*obst)->isDynamic()) // we handle dynamic obstacles differently below
continue;
int index;
if (cfg_->obstacles.obstacle_poses_affected >= teb_.sizePoses())
index = teb_.sizePoses() / 2;
else
index = teb_.findClosestTrajectoryPose(*(obst->get()));
//对于每个障碍物,找到最近的TEB位姿
// check if obstacle is outside index-range between start and goal
if ( (index <= 1) || (index > teb_.sizePoses()-2) ) // start and goal are fixed and findNearestBandpoint finds first or last conf if intersection point is outside the range
continue;
if (inflated)
{ //考虑障碍物膨胀
// 这种情况下,首先会使得障碍物与位姿距离大于设定的最小距离,其次距离在膨胀半径范围内是越大越好,距离超过膨胀半径,则残差为0
EdgeInflatedObstacle* dist_bandpt_obst = new EdgeInflatedObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
dist_bandpt_obst->setInformation(information_inflated);
dist_bandpt_obst->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst);
}
else
{ // 不考虑障碍物膨胀,仅使得位姿距离障碍物大于设定的最小距离,距离大于该最小距离,则残差为0
EdgeObstacle* dist_bandpt_obst = new EdgeObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
dist_bandpt_obst->setInformation(information);
dist_bandpt_obst->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst);
}
// 考虑障碍物对最近TEB位姿附近的位姿的影响
for (int neighbourIdx=0; neighbourIdx < floor(cfg_->obstacles.obstacle_poses_affected/2); neighbourIdx++)
{
if (index+neighbourIdx < teb_.sizePoses())
{
if (inflated)
{
EdgeInflatedObstacle* dist_bandpt_obst_n_r = new EdgeInflatedObstacle;
dist_bandpt_obst_n_r->setVertex(0,teb_.PoseVertex(index+neighbourIdx));
dist_bandpt_obst_n_r->setInformation(information_inflated);
dist_bandpt_obst_n_r->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst_n_r);
}
else
{
EdgeObstacle* dist_bandpt_obst_n_r = new EdgeObstacle;
dist_bandpt_obst_n_r->setVertex(0,teb_.PoseVertex(index+neighbourIdx));
dist_bandpt_obst_n_r->setInformation(information);
dist_bandpt_obst_n_r->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst_n_r);
}
}
if ( index - neighbourIdx >= 0) // needs to be casted to int to allow negative values
{
if (inflated)
{
EdgeInflatedObstacle* dist_bandpt_obst_n_l = new EdgeInflatedObstacle;
dist_bandpt_obst_n_l->setVertex(0,teb_.PoseVertex(index-neighbourIdx));
dist_bandpt_obst_n_l->setInformation(information_inflated);
dist_bandpt_obst_n_l->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst_n_l);
}
else
{
EdgeObstacle* dist_bandpt_obst_n_l = new EdgeObstacle;
dist_bandpt_obst_n_l->setVertex(0,teb_.PoseVertex(index-neighbourIdx));
dist_bandpt_obst_n_l->setInformation(information);
dist_bandpt_obst_n_l->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dist_bandpt_obst_n_l);
}
}
}
}
}
void TebOptimalPlanner::AddEdgesObstacles(double weight_multiplier)
{
if (cfg_->optim.weight_obstacle==0 || weight_multiplier==0 || obstacles_==nullptr )
return; // if weight equals zero skip adding edges!
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)
{
EdgeInflatedObstacle* dist_bandpt_obst = new EdgeInflatedObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
dist_bandpt_obst->setInformation(information_inflated);
dist_bandpt_obst->setParameters(*cfg_, robot_model_.get(), obstacle);
optimizer_->addEdge(dist_bandpt_obst);
}
else
{
EdgeObstacle* dist_bandpt_obst = new EdgeObstacle;
dist_bandpt_obst->setVertex(0,teb_.PoseVertex(index));
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)
{ // 对于每一个TEB位姿,找到有影响的障碍物,并施加约束
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;
}
}
}
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;
}
}
涉及的参数
min_obstacle_dist: 机器人距离障碍物的最小距离,已经考虑了机器人的footprint形状;
inflation_dist: 膨胀距离,已经考虑了机器人的footprint形状;
obstacle_association_cutoff_factor: 在AddEdgesObstacles中,如果障碍物距离TEB位姿大于min_obstacle_dist* obstacle_association_cutoff_factor 则不会被考虑;
obstacle_association_force_inclusion_factor: 在AddEdgesObstacles中,如果障碍物距离TEB位姿小于min_obstacle_dist* obstacle_association_force_inclusion_factor 则会被考虑;
介于上述两者之间的,则只考虑左右方向最近的障碍物。
动态障碍物处理
对于每一个动态障碍物,所有TEB位姿都会被考虑进去,与静态障碍物的处理不同之处在于,会根据障碍物的速度和时间对未来其位置进行预测,以达到动态避障的目的。如果环境是低动态,可以将include_dynamic_obstacles设为False,及可不考虑动态障碍物。
void TebOptimalPlanner::AddEdgesDynamicObstacles(double weight_multiplier)
{
if (cfg_->optim.weight_obstacle==0 || weight_multiplier==0 || obstacles_==NULL )
return; // if weight equals zero skip adding edges!
Eigen::Matrix<double,2,2> information;
information(0,0) = cfg_->optim.weight_dynamic_obstacle * weight_multiplier;
information(1,1) = cfg_->optim.weight_dynamic_obstacle_inflation;
information(0,1) = information(1,0) = 0;
for (ObstContainer::const_iterator obst = obstacles_->begin(); obst != obstacles_->end(); ++obst)
{
if (!(*obst)->isDynamic())
continue;
// Skip first and last pose, as they are fixed
double time = teb_.TimeDiff(0);
for (int i=1; i < teb_.sizePoses() - 1; ++i)
{
EdgeDynamicObstacle* dynobst_edge = new EdgeDynamicObstacle(time);
dynobst_edge->setVertex(0,teb_.PoseVertex(i));
dynobst_edge->setInformation(information);
dynobst_edge->setParameters(*cfg_, robot_model_.get(), obst->get());
optimizer_->addEdge(dynobst_edge);
time += teb_.TimeDiff(i); // we do not need to check the time diff bounds, since we iterate to "< sizePoses()-1".
}
}
}
障碍物的来源
- 直接来自代价地图,包括静态层和障碍物层的障碍物
void TebLocalPlannerROS::updateObstacleContainerWithCostmap()
- 来自costmap_converter,最终来源与costmap一致
void TebLocalPlannerROS::updateObstacleContainerWithCostmapConverter()
- 来自收到的障碍物消息
void TebLocalPlannerROS::updateObstacleContainerWithCustomObstacles()
四,优缺点
优点:可自定义多种约束,动态避障效果较好,可跟随各种路径;
缺点:算力较大,在低算力嵌入式板子上很难跑起来,调参需要经验。