自动泊车混合A星规划算法学习-HybridAstar

最近想学习一下自动泊车的规划算法,在Github上找到了一个关于HybridAstar代码,做一下学习记录。
代码地址:HAStar_ParkingPlanner

1 泊车场景创建

代码中通过文件parking_scenario.txt来配置泊车场景,包括起始位姿,终点位姿,场景边界和障碍物顶点坐标(从右上角逆时针排序):

start_pose: -6.0, 10.5, 3.1416;
end_pose: 0.0, 3.5, 1.5708;
boundary: -15, 15, 0, 40;
obstacle: {13, 8},{1.5, 8},{1.5, 0},{13, 0};{-1.5, 8},{-13, 8},{-13, 0},{-1.5, 0};{1.5, 2},{-1.5, 2},{-1.5, 0},{1.5, 0}

画出来如下图所示,其中实心点代表车辆后轴中心,蓝色点表示起始位置,绿色点表示终点位置。障碍物顶点的连线构成一个个的障碍区域(红色区域)。
在这里插入图片描述

2 HAStar算法配置参数

代码通过hya_param.conf文件来定义算法运行时的一些配置参数:

xy_grid_resolution : 0.2            // 
phi_grid_resolution : 0.0
int next_node_num : 10              // 扩展节点时,每个节点往下扩展的新节点数量
step_size : 0.5                     // 节点扩展步长
traj_forward_penalty : 0.0          // 向前搜索的惩罚代价
traj_back_penalty : 0.0             // 向后搜索的惩罚代价
traj_gear_switch_penalty : 10.0     // 换挡的惩罚代价
traj_steer_penalty : 100.0          // 转向的惩罚代价
traj_steer_change_penalty : 10.0    // 转向变化的惩罚代价
grid_a_star_xy_resolution : 0.1     // 计算cost map时用的网格分辨率
node_radius : 0.5                   // 扩展节点时,节点跟障碍边缘的距离上限,超过则节点不可取
delta_t : 1.0

3 车辆参数

车辆参数通过vehicle_param.conf来配置:

front_edge_to_center : 3.89         //车辆前边缘到后轴中心的距离
back_edge_to_center : 1.043         //车辆后边缘到后轴中心的距离
length : 4.933                      //车长
width : 2.11                        //车宽
max_steer_angle : 8.20304748437     //这个值与steer_ratio的比值为等效转向轮的等效转角,用于计算
steer_ratio : 16                    //转弯半径
wheel_base : 2.8448                 //轴距

4 算法流程

代码中对HybridAstar的实现主要通过HybridAstar类来实现,利用HAStar配置参数和车辆参数创建类对象,直接调用Plan成员函数:

bool HybridAStar::Plan(
    const Pos3d& start_pose, 
    const Pos3d& end_pose,    
    const std::vector<double>& XYbounds,
    const std::vector<std::vector<Vec2d>>& obstacles_vertices_vec,   // 障碍区域顶点
    HybridAStartResult* result)   // 结果

4.1 计算障碍区域边线

遍历障碍区域,将障碍区的每条线都生成LineSegment2d对象:

struct LineSegment2d {
  Vec2d start;
  Vec2d end;
  Vec2d unit_direction;    // 单位方向向量(ux, uy)
  double heading = 0.0;    // 方向角,弧度
  double length = 0.0;
};

每个障碍区的边线作为一组,放入成员变量:

std::vector<std::vector<LineSegment2d>> obstacles_linesegments_vec_;

4.2 Node3d节点

路径搜索时每一步都会生成一个节点,代码里是Node3d类:

Node3d::Node3d(const std::vector<double> &traversed_x,
               const std::vector<double> &traversed_y,
               const std::vector<double> &traversed_phi,
               const std::vector<double> &XYbounds,
               const double xy_grid_resolution) {
  x_ = traversed_x.back();       // 节点x坐标
  y_ = traversed_y.back();       // 节点y坐标
  phi_ = traversed_phi.back();   // 节点方向

  // XYbounds in xmin, xmax, ymin, ymax
  x_grid_ = static_cast<int>((x_ - XYbounds[0]) / xy_grid_resolution);  // 网格坐标
  y_grid_ = static_cast<int>((y_ - XYbounds[2]) / xy_grid_resolution);  // 网格坐标
  phi_grid_ = static_cast<int>((phi_ - (-M_PI)) / xy_grid_resolution);  // 网格坐标

  traversed_x_ = traversed_x;      // 当前节点的路径点(从上一个节点的坐标到本节点的坐标)
  traversed_y_ = traversed_y;      // 当前节点的路径点
  traversed_phi_ = traversed_phi;  // 当前节点的路径点

  index_ = ComputeStringIndex(x_grid_, y_grid_, phi_grid_);  // 节点名称
  step_size_ = traversed_x.size();
}

4.2.1 节点有效性判断

遍历节点的每个轨迹点,计算车辆的轮廓,对每一条障碍边缘线,判断是否与车辆轮廓有交点,如果有则该节点无效。如果车辆轮廓与障碍边缘线的距离过近(代码中是1e-10),也无效。

4.3 计算cost map

代码中定义了GridSearch类,其成员函数GenerateDpMap用于计算cost map(作为后续HA*搜索时的启发值),
从终点位置开始,搜索每个网格点到终点的cost,每个网格点的搜索方向为上下左右和对角,共八个方向,如下图所示,上下左右代价为1,对角代价为1.414:
在这里插入图片描述
搜索过程为典型涉及三个容器变量:

std::priority_queue<std::pair<std::string, double>,
                      std::vector<std::pair<std::string, double>>, cmp> open_pq; // 待处理的网格点,按cost值从小到大排序
std::unordered_map<std::string, std::shared_ptr<Node2d>> open_set;  // 已扩展到的网格点
std::unordered_map<std::string, std::shared_ptr<Node2d>> dp_map_;   // 结果

open_pq和open_set最开始只有终点网格点,每次循环将open_pq中最小cost网格点放入dp_map_中,并弹出open_pq中的这个最小cost点,按上面所说的扩展方式扩展该最小cost网格点,检查扩展的8个网格点的有效性:

// 检查网格点是否越界
// 是否与障碍边缘的距离过近(<node_radius)

如果网格点已经在dp_map_中,则跳过后续处理;
如果网格点不在open_set里,则将网格点加入到open_set和open_pq中;
如果网格点已经在open_set里,则更新其cost值和PreNode;

4.4 Hybrid A*

这里也涉及到三个容器变量(HybridAStar类成员):

std::priority_queue<std::pair<std::string, double>,
                      std::vector<std::pair<std::string, double>>, cmp> open_pq_; // 待check的节点,按cost值从小到大排列 
std::unordered_map<std::string, std::shared_ptr<Node3d>> open_set_;   // 扩展到的节点
std::unordered_map<std::string, std::shared_ptr<Node3d>> close_set_;  // 已check过的节点

open_pq和open_set最开始只有起点节点。

4.4.1 节点扩展

4.4.1.1 检查当前节点到终点节点有无连接曲线
std::vector<Pos3d> GetTrajFromCurvePathsConnect(const Pos3d &start_pos,
                                                const Pos3d &end_pos,
                                                double r_min,         // 最小转弯半径
                                                double resolution) {  // 节点网格分辨率
  std::vector<Pos3d> traj_points;
  std::vector<CurvePath> curve_paths =
      CalCurvePathConnectTwoPose(start_pos, end_pos, r_min);
  if (curve_paths.empty()) {
    return traj_points;
  }

  for (const auto &path : curve_paths) {
    int traj_point_num = std::abs(path.dist) / resolution;
    for (int i = 0; i < traj_point_num; ++i) {
      const Pos3d path_start_pos{path.x, path.y, path.phi};
      Pos3d temp_pos = CalEndPosWithACurvePath(
          path_start_pos, sign(path.dist) * i * resolution, path.radius);
      traj_points.push_back(temp_pos);
    }
  }

  return traj_points;
}

其中CalCurvePathConnectTwoPose用于计算两个位姿之间有没有直线和圆弧组成的连接线(代码中也有关于RS曲线的内容):

std::vector<CurvePath> CalCurvePathConnectTwoPose(const Pos3d &start_pos,
                                                  const Pos3d &end_pos,
                                                  double r_min) 
double rx_start_pos_end_pos = CalProjectInX(start_pos, end_pos);   // end_pos在start_pos坐标系下的坐标x
double ry_start_pos_end_pos = CalProjectInY(start_pos, end_pos);   // end_pos在start_pos坐标系下的坐标y
double ry_end_pos_start_pos = CalProjectInY(end_pos, start_pos);   // start_pos在end_pos坐标系下的坐标y
double theta = NormalizeAngle(end_pos.phi - start_pos.phi);        // end_pos在start_pos坐标系下的航向角

(1)如果终点位置在起点位姿坐标系下的x值过小(1e-4),则认为没有连接线:

std::vector<CurvePath> curve_paths;
  if (std::abs(rx_start_pos_end_pos) < k_min_value) {   
    return curve_paths;
  }

这里有疑问,实际上通过一个半圆弧是可以连接起来的,如下图所示:
在这里插入图片描述
(2)如果车辆终点位置在起点位姿坐标系的x轴上且与x轴基本平行,则可通过直线路径连接:

if (std::abs(ry_start_pos_end_pos) < k_min_value &&
      std::abs(theta) < k_min_value) {
    CurvePath line_path{start_pos.x, start_pos.y, start_pos.phi,
                        rx_start_pos_end_pos, 0};
    curve_paths.push_back(line_path);
    return curve_paths;
  }

(3)当终点与起点连线与起始位姿x轴夹角小于终点位姿航向角的一半时,可以用圆弧连接:

if (std::abs(std::atan(ry_start_pos_end_pos / rx_start_pos_end_pos) -
               theta / 2.0) < k_min_value) {
    double r = rx_start_pos_end_pos / std::sin(theta);
    if (std::abs(r) < r_min) {
      return curve_paths;
    }
    CurvePath circle_path{start_pos.x, start_pos.y, start_pos.phi, r * theta,
                          r};
    curve_paths.push_back(circle_path);
    return curve_paths;
  }

在这里插入图片描述
(4)与x轴平行同向且不在x轴上,没法只通过一段圆弧到达:

if (std::abs(1.0 - std::cos(theta)) <= k_min_value) {  
    return curve_paths;
}

(5)直线+圆弧 or 圆弧+直线

if (std::abs(ry_end_pos_start_pos) > std::abs(ry_start_pos_end_pos)) {
    double r = ry_start_pos_end_pos / (1.0 - std::cos(theta));
    double line_length =
        ry_end_pos_start_pos / std::sin(theta) - r * std::tan(theta / 2.0);

    if (std::abs(r) < r_min) {
      return curve_paths;
    }

    CurvePath line_path{start_pos.x, start_pos.y, start_pos.phi, line_length,
                        0};
    Pos3d min_pos = CalEndPosWithACurvePath(line_path);
    CurvePath circle_path{min_pos.x, min_pos.y, min_pos.phi, r * theta, r};
    curve_paths.push_back(line_path);
    curve_paths.push_back(circle_path);
  } else { // 圆弧+直线
    double r = ry_end_pos_start_pos / (1.0 - std::cos(theta));
    double line_length =
        ry_start_pos_end_pos / std::sin(theta) - r * std::tan(theta / 2.0);

    if (std::abs(r) < r_min) {
      return curve_paths;
    }

    CurvePath circle_path{start_pos.x, start_pos.y, start_pos.phi, r * theta,
                          r};
    Pos3d min_pos = CalEndPosWithACurvePath(circle_path);
    CurvePath line_path{min_pos.x, min_pos.y, min_pos.phi, line_length, 0};
    curve_paths.push_back(circle_path);
    curve_paths.push_back(line_path);
  }

在这里插入图片描述

4.4.1.2 若有当前节点与终点连接曲线

生成离散的路径点,计算每个路径点的有效性(参考4.2.1),若路径有效,则生成最终的节点,并停止节点扩展,生成最终的路径结果。

4.4.1.3 若无当前节点与终点连接曲线
4.4.1.3.1 当前节点往下扩展新的节点
std::shared_ptr<Node3d> HybridAStar::Next_node_generator(
    std::shared_ptr<Node3d> current_node, size_t next_node_index)

在这里插入图片描述
根据配置文件中参数,代码中实际每次只往前扩展了1个step_size:

for (size_t i = 0; i < arc / step_size_; ++i) {
    
    const double next_x = last_x + traveled_distance * std::cos(last_phi); // 代码中traveled_distance = step_size_,向前取正,向后取负
    const double next_y = last_y + traveled_distance * std::sin(last_phi);
    const double next_phi = math::NormalizeAngle(
        last_phi +
        traveled_distance / vehicle_param_.wheel_base * std::tan(steering));  // 弧长/R
    intermediate_x.push_back(next_x);
    intermediate_y.push_back(next_y);
    intermediate_phi.push_back(next_phi);
    last_x = next_x;
    last_y = next_y;
    last_phi = next_phi;
  }

这里是把圆弧离散成了一段段(每段长度为traveled_distance)的。

4.4.1.3.2 新扩展的节点的cost

新节点的cost包括:
(1)路径cost,上一节点的路径cost加上上一节点到该节点的cost:

double HybridAStar::TrajCost(std::shared_ptr<Node3d> current_node,
                             std::shared_ptr<Node3d> next_node) {
  // evaluate cost on the trajectory and add current cost
  double piecewise_cost = 0.0;
  if (next_node->GetDirec()) {  // true 表示向前走,false表示后退
    piecewise_cost += static_cast<double>(next_node->GetStepSize() - 1) *  // 节点的stepsize指的是离散步的步数
                      step_size_ * traj_forward_penalty_;   // 往前走的代价
  } else {
    piecewise_cost += static_cast<double>(next_node->GetStepSize() - 1) *
                      step_size_ * traj_back_penalty_;      // 往后退的代价
  }
  if (current_node->GetDirec() != next_node->GetDirec()) {  // 改变行驶方向
    piecewise_cost += traj_gear_switch_penalty_;
  }
  piecewise_cost += traj_steer_penalty_ * std::abs(next_node->GetSteer());  // 转角代价,转角越大,代价越大
  piecewise_cost += traj_steer_change_penalty_ *  
                    std::abs(next_node->GetSteer() - current_node->GetSteer());   // 变转角代价
  return piecewise_cost;
}

(2)到终点的cost(启发值),等于2.4.3中计算得到的cost;

4.4.2 生成路径结果

从final_node_开始依次往前遍历到起点,得到整条路径的离散点的(x, y, phi);

struct HybridAStartResult {
  std::vector<double> x;       //路径离散点x坐标
  std::vector<double> y;       //路径离散点y坐标
  std::vector<double> phi;     //路径离散点航向角
  std::vector<double> v;
  std::vector<double> a;
  std::vector<double> steer;
  std::vector<double> accumulated_s;
};

对整条路径进行拆分,将同一挡位下的路径点组成一个HybridAStartResult对象;为每一个HybridAStartResult对象计算速度和加速度:

bool HybridAStar::GenerateSpeedAcceleration(HybridAStartResult* result) 

速度计算:
每段路径的起点和终点速度都是0;

for (size_t i = 1; i + 1 < x_size; ++i) {
    double discrete_v = (((result->x[i + 1] - result->x[i]) / delta_t_) *
                             std::cos(result->phi[i]) +
                         ((result->x[i] - result->x[i - 1]) / delta_t_) *
                             std::cos(result->phi[i])) /
                            2.0 +        // x方向速度在当前航向上的投影
                        (((result->y[i + 1] - result->y[i]) / delta_t_) *
                             std::sin(result->phi[i]) +
                         ((result->y[i] - result->y[i - 1]) / delta_t_) *
                             std::sin(result->phi[i])) /
                            2.0;         // y方向速度在当前航向上的投影
    result->v.push_back(discrete_v);
  }

加速度计算:

for (size_t i = 0; i + 1 < x_size; ++i) {
    const double discrete_a = (result->v[i + 1] - result->v[i]) / delta_t_;
    result->a.push_back(discrete_a);
  }

转向角计算:

for (size_t i = 0; i + 1 < x_size; ++i) {
    double discrete_steer = (result->phi[i + 1] - result->phi[i]) *
                            vehicle_param_.wheel_base / step_size_;
    if (result->v[i] > 0.0) {
      discrete_steer = std::atan(discrete_steer);
    } else {
      discrete_steer = std::atan(-discrete_steer);
    }
    result->steer.push_back(discrete_steer);
  }

将结果画出来:

在这里插入图片描述

  • 27
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值