机器人规划与控制研究所 ——机器人/自动驾驶规划与控制方向综合、全面、专业的平台。4万人订阅的微信大号。点击标题下蓝字“机器人规划与控制研究所”关注,我们将为您提供有价值、有深度的延伸阅读。
2025年做了基于ego-planner的二次开发,适配2d地面场景的机器人的自主规划能力。今天再给大家分享一个适用于自主绕障的A*算法,他可以作为局部规划器规划失败得时候,使用A*在局部环境下快速规划出一条安全路线,然后再使用局部轨迹优化器去平滑。这个A*算法我也是从ego-planner里抽出来的,熟悉ego-planner算法的朋友应该知道,A*算法间接参与ego-planner的优化。https://github.com/JackJu-HIT/A-star-ROS2/tree/master
也是使用了ROS2作为外壳,主要用于可视化显示,内部都是纯c++调用,非常方便项目移植。其实在自主绕障这块,一些场景仅仅用A* + 轨迹优化(仅平滑即可)其实也够用,A*搜出来的路径无碰撞还快。voidTrajectoryAndObstaclesPublisher::init_ego_planner_base(){ planner_ = std::make_shared(); Eigen::Vector2i map_size(map_x_size_,map_y_size_); map_ = std::make_shared(map_resolution_,map_size); map_->setInflateRadius(0.5); Eigen::Vector2i map_size_2(500,500); planner_->initGridMap(map_,map_size_2); }
这里的map_size_2(500,500
);中500表示起点和终点的范围是50米,也就说,A*最大只能规划50米长的路径,一般来说,针对于局部规划的自主绕障够用,如需更大可以对这个参数进行调整。
* @Function: Ego Planner + RViz Interactive Input (Global Path + Obstacles) * @Create by: juchunyu@qq.com * @Date: 2026-1-11 20:00:01 */TrajectoryAndObstaclesPublisher::TrajectoryAndObstaclesPublisher() : Node("ego_planner_interactive_node"), has_valid_global_path_(false), has_obstacles_(false), should_plan_(false), needs_replan_(false){ global_path_pub_ = this->create_publisher<: class="code-snippet__variable">msg::Path>("visual_global_path", 10); local_traj_pub_ = this->create_publisher<: class="code-snippet__variable">msg::Path>("visual_local_trajectory", 10); obs_pub_ = this->create_publisher<: class="code-snippet__variable">msg::PointCloud2>("visual_obstacles", 10); obs_local_pub_ = this->create_publisher<: class="code-snippet__variable">msg::PointCloud2>("visual_local_obstacles", 10); goal_pose_sub_ = this->create_subscription<: class="code-snippet__variable">msg::PoseStamped>( "/goal_pose", 10, std::bind(&TrajectoryAndObstaclesPublisher::goal_pose_callback, this, std::placeholders::_1) ); rviz_point_sub_ = this->create_subscription<: class="code-snippet__variable">msg::PointStamped>( "/clicked_point", 10, std::bind(&TrajectoryAndObstaclesPublisher::rviz_point_callback, this, std::placeholders::_1) ); pose_estimate_sub_ = this->create_subscription<: class="code-snippet__variable">msg::PoseWithCovarianceStamped>( "/initialpose", 10, std::bind(&TrajectoryAndObstaclesPublisher::pose_estimate_callback, this, std::placeholders::_1) ); trigger_plan_sub_ = this->create_subscription<: class="code-snippet__variable">msg::Bool>( "/trigger_plan", 10, std::bind(&TrajectoryAndObstaclesPublisher::trigger_plan_callback, this, std::placeholders::_1) ); init_ego_planner_base(); timer_ = this->create_wall_timer( std::chrono::milliseconds(200), std::bind(&TrajectoryAndObstaclesPublisher::publish_and_plan, this) ); RCLCPP_INFO(this->get_logger(), "Interactive Ego Planner Node Ready!"); RCLCPP_INFO(this->get_logger(), "=== 使用方法 ==="); RCLCPP_INFO(this->get_logger(), "1. 添加全局路径:使用2D Nav Goal工具设置终点"); RCLCPP_INFO(this->get_logger(), "2. 添加障碍物(三种方法任选):"); RCLCPP_INFO(this->get_logger(), " - 方法A:使用Publish Point工具点击地图"); RCLCPP_INFO(this->get_logger(), " - 方法B:使用2D Pose Estimate工具点击Grid任意位置(推荐)"); RCLCPP_INFO(this->get_logger(), " - 方法C:发布PointCloud2到 /rviz_input_obstacles"); RCLCPP_INFO(this->get_logger(), "3. 触发规划:ros2 topic pub /trigger_plan std_msgs/Bool \"{data: true}\""); RCLCPP_INFO(this->get_logger(), "4. 停止规划:ros2 topic pub /trigger_plan std_msgs/Bool \"{data: false}\""); RCLCPP_INFO(this->get_logger(), "5. 支持多次规划:可以反复发送true/false控制规划"); RCLCPP_INFO(this->get_logger(), ""); RCLCPP_INFO(this->get_logger(), "=== RViz2设置步骤 ==="); RCLCPP_INFO(this->get_logger(), "1. 添加Grid显示"); RCLCPP_INFO(this->get_logger(), "2. 添加2D Nav Goal工具(话题:/goal_pose)"); RCLCPP_INFO(this->get_logger(), "3. 添加2D Pose Estimate工具(使用默认话题:/initialpose)"); RCLCPP_INFO(this->get_logger(), "4. 添加Publish Point工具(使用默认话题:/clicked_point)"); RCLCPP_INFO(this->get_logger(), "5. 添加PointCloud2显示(话题:/visual_obstacles)"); RCLCPP_INFO(this->get_logger(), "6. 添加Path显示(话题:/visual_global_path和/visual_local_trajectory)");}void TrajectoryAndObstaclesPublisher::init_ego_planner_base(){ planner_ = std::make_shared(); Eigen::Vector2i map_size(map_x_size_,map_y_size_); map_ = std::make_shared(map_resolution_,map_size); map_->setInflateRadius(0.5); Eigen::Vector2i map_size_2(500,500); planner_->initGridMap(map_,map_size_2); }void TrajectoryAndObstaclesPublisher::
add_obstacle_at_position(double x, double y){ std::lock_guard<: class="code-snippet__variable">mutex> lock(data_mutex_); ObstacleInfo obs; obs.x = x; obs.y = y; obs.z = 0.0;
obstacles_.push_back(obs); has_obstacles_ = true;
if (should_plan_) { needs_replan_ = true; RCLCPP_INFO(this->get_logger(), "障碍物更新,已标记需要重新规划"); }
RCLCPP_INFO(this->get_logger(), "添加障碍物: (%.2f, %.2f), 总障碍物数量: %zu", x, y, obstacles_.size());}void TrajectoryAndObstaclesPublisher::pose_estimate_callback(const geometry_msgs::msg::PoseWithCovarianceStamped::SharedPtr msg){ std::lock_guard<: class="code-snippet__variable">mutex> lock(data_mutex_); cur_pose_.x = msg->pose.pose.position.x; cur_pose_.y = msg->pose.pose.position.y; cur_pose_.z = 0; tf2::Quaternion q( msg->pose.pose.orientation.x, msg->pose.pose.orientation.y, msg->pose.pose.orientation.z, msg->pose.pose.orientation.w); cur_pose_.z = tf2::getYaw(q); std::cout << std::endl; if (has_valid_global_path_) { needs_replan_ = true; if (should_plan_) { RCLCPP_INFO(this->get_logger(), "初始位置更新,触发重新规划!"); } else { RCLCPP_WARN(this->get_logger(), "初始位置已更新,但规划未启动!请先发送 /trigger_plan true 启动规划"); } } else { RCLCPP_WARN(this->get_logger(), "初始位置已更新,但无有效全局路径,无法规划!"); }}void TrajectoryAndObstaclesPublisher::trigger_plan_callback(const std_msgs::msg::Bool::SharedPtr msg){ }void TrajectoryAndObstaclesPublisher::goal_pose_callback(const geometry_msgs::msg::PoseStamped::SharedPtr msg){ add_obstacle_at_position(msg->pose.position.x, msg->pose.position.y);}void TrajectoryAndObstaclesPublisher::generate_straight_path(const geometry_msgs::msg::PoseStamped& start,
const geometry_msgs::msg::PoseStamped& goal){ global_plan_traj_.clear(); double dx = goal.pose.position.x - start.pose.position.x; double dy = goal.pose.position.y - start.pose.position.y; double distance = std::sqrt(dx*dx + dy*dy); int num_points = std::max(2, static_cast<int>(distance / 0.1)); for (int i = 0; i < num_points; ++i) { double ratio = static_cast<double>(i) / (num_points - 1); PathPoint point; point.x = start.pose.position.x + ratio * dx; point.y = start.pose.position.y + ratio * dy; point.z = 0.0; global_plan_traj_.push_back(point); }}void TrajectoryAndObstaclesPublisher::rviz_point_callback(const geometry_msgs::msg::PointStamped::SharedPtr msg){ std::lock_guard<: class="code-snippet__variable">mutex> lock(data_mutex_); PathPoint point; point.x = msg->point.x; point.y = msg->point.y; point.z = 0; global_plan_traj_.push_back(point); std::cout << point.x endl; has_valid_global_path_ = true;
if (should_plan_) { needs_replan_ = true; RCLCPP_INFO(this->get_logger(), "路径更新,已标记需要重新规划"); }}void TrajectoryAndObstaclesPublisher::rviz_global_path_callback(const nav_msgs::msg::Path::SharedPtr msg){ if (msg->poses.empty()) { RCLCPP_WARN(this->get_logger(), "从RViz接收到空的全局路径!"); has_valid_global_path_ = false; return; } std::lock_guard<: class="code-snippet__variable">mutex> lock(data_mutex_); global_plan_traj_.clear(); for (const auto& pose_stamped : msg->poses) { PathPoint path_point; path_point.x = pose_stamped.pose.position.x; path_point.y = pose_stamped.pose.position.y; path_point.z = pose_stamped.pose.position.z; global_plan_traj_.push_back(path_point); } has_valid_global_path_ = true;
if (should_plan_) { needs_replan_ = true; RCLCPP_INFO(this->get_logger(), "路径更新,已标记需要重新规划"); }
RCLCPP_INFO(this->get_logger(), "从RViz接收到全局路径 (大小: %zu)", global_plan_traj_.size());}
void TrajectoryAndObstaclesPublisher::publish_and_plan(){ std::lock_guard<: class="code-snippet__variable">mutex> lock(data_mutex_); if (needs_replan_ ) { std::vector global_plan_traj_temp; if (!global_plan_traj_.empty()) { discretize_trajectory(global_plan_traj_, global_plan_traj_temp, 0.1);
float mindist = 100000000; int minddex = 0; for(int i = 0; i < global_plan_traj_temp.size();i++) { double dist = distance(global_plan_traj_temp[i], cur_pose_); if(dist < mindist) { mindist = dist; minddex = i; }
} std::vector global_plan_traj_after; global_plan_traj_after.push_back(cur_pose_); for(int i = minddex; i < global_plan_traj_temp.size();i++) { global_plan_traj_after.push_back(global_plan_traj_temp[i]); } discretize_trajectory(global_plan_traj_after, global_plan_traj_temp, 0.1); } for(int i = 0; i < obstacles_.size();i++) { Eigen::Vector2d coord(obstacles_[i].x,obstacles_[i].y); Eigen::Vector2i res; res = map_->worldToGrid(coord); map_->setObstacle(res, true); } Eigen::Vector2d start_pt(cur_pose_.x, cur_pose_.y); Eigen::Vector2d end_pt(global_plan_traj_temp[global_plan_traj_temp.size()-1].x, global_plan_traj_temp[global_plan_traj_temp.size()-1].y); std::cout << std::endl; { map_->inflate(); inflate_flag_ = false; } auto start = std::chrono::system_clock::now(); bool success = planner_->AstarSearch(0.1, start_pt, end_pt); auto end = std::chrono::system_clock::now(); auto elapsed = std::chrono::duration_cast<: class="code-snippet__variable">chrono::milliseconds>(end - start); printf("a_star_Planner Total Running Time: %d ms \n", elapsed.count()); std::cout << start_pt endl
;
if (success) { planned_traj.clear(); std::vector<Eigen::Vector2d> path = planner_->getPath(); std::cout ; for (const auto& pt : path) { PathPoint temp; temp.x = pt.x() ; temp.y = pt.y(); temp.z = 0; planned_traj.push_back(temp); std::cout << pt.x() y() } } else { std::cout ; }
if (has_obstacles_) { RCLCPP_INFO(this->get_logger(), "规划完成! 包含障碍物避让. 路径点: %zu, 障碍物: %zu", global_plan_traj_.size(), obstacles_.size()); } else { RCLCPP_INFO(this->get_logger(), "规划完成! 无障碍物. 路径点: %zu", global_plan_traj_.size()); }
needs_replan_ = false; } publish_global_path(); publish_planned_trajectory(); publish_obstacles(); publish_local_obstacles();}void TrajectoryAndObstaclesPublisher::publish_global_path(){ nav_msgs::msg::Path visual_path; visual_path.header.stamp = this->now(); visual_path.header.frame_id = "map"; for (const auto& path_point : global_plan_traj_) { geometry_msgs::msg::PoseStamped pose; pose.header = visual_path.header; pose.pose.position.x = path_point.x; pose.pose.position.y = path_point.y; pose.pose.position.z = path_point.z; tf2::Quaternion q; q.setRPY(0.0, 0.0, 0.0); pose.pose.orientation.x = q.x(); pose.pose.orientation.y = q.y(); pose.pose.orientation.z = q.z(); pose.pose.orientation.w = q.w(); visual_path.poses.push_back(pose); } global_path_pub_->publish(visual_path);}void TrajectoryAndObstaclesPublisher::publish_planned_trajectory(){ nav_msgs::msg::Path visual_traj; visual_traj.header.stamp = this->now(); visual_traj.header.frame_id = "map"; for (size_t i = 0; i < planned_traj.size(); ++i) { geometry_msgs::msg::PoseStamped pose; pose.header = visual_traj.header; pose.pose.position.x = planned_traj[i].x; pose.pose.position.y = planned_traj[i].y; pose.pose.position.z = planned_traj[i].z; tf2::Quaternion q; q.setRPY(0.0, 0.0, 0.0); pose.pose.orientation.x = q.x(); pose.pose.orientation.y = q.y(); pose.pose.orientation.z = q.z(); pose.pose.orientation.w = q.w(); visual_traj.poses.push_back(pose); } local_traj_pub_->publish(visual_traj);}
void TrajectoryAndObstaclesPublisher::publish_obstacles(){ sensor_msgs::msg::PointCloud2 visual_obs; visual_obs.header.stamp = this->now(); visual_obs.header.frame_id = "map"; visual_obs.width = obstacles_.size(); visual_obs.height = 1; visual_obs.is_dense = true; sensor_msgs::PointCloud2Modifier modifier(visual_obs); modifier.setPointCloud2FieldsByString(1, "xyz"); sensor_msgs::PointCloud2Iterator<float> iter_x(visual_obs, "x"); sensor_msgs::PointCloud2Iterator<float> iter_y(visual_obs, "y"); sensor_msgs::PointCloud2Iterator<float> iter_z(visual_obs, "z"); for (const auto& obs : obstacles_) { *iter_x = static_cast<float>(obs.x); *iter_y = static_cast<float>(obs.y); *iter_z = 0.0f; ++iter_x; ++iter_y; ++iter_z; } obs_pub_->publish(visual_obs);}double TrajectoryAndObstaclesPublisher::distance(const PathPoint& p1, const PathPoint& p2) { double dx = p2.x - p1.x; double dy = p2.y - p1.y; return std::sqrt(dx*dx + dy*dy);} * 将轨迹离散为均匀间隔的点(间隔10cm) * @param original_trajectory 原始轨迹(由多个顶点组成的折线) * @param discrete_trajectory 输出的离散轨迹 * @param interval 间隔距离(单位:米,默认0.1米即10cm) */void TrajectoryAndObstaclesPublisher::discretize_trajectory(const std::vector& original_trajectory, std::vector& discrete_trajectory, double interval) { if (original_trajectory.size() 2) { std::cerr << std::endl; return; } discrete_trajectory.clear(); discrete_trajectory.push_back(original_trajectory[0]); for (size_t i = 0; i < original_trajectory.size() - 1; ++i) { const PathPoint& start = original_trajectory[i]; const PathPoint& end = original_trajectory[i+1]; double seg_length = distance(start, end); if (seg_length 1e-6) { continue; } int num_points = static_cast<int>(seg_length / interval); double last_interval = seg_length - num_points * interval; for (int j = 1; j <= num_points; ++j) { double ratio; if (j < num_points) { ratio = (j * interval) / seg_length; } else { ratio = 1.0; } PathPoint p; p.x = start.x + ratio * (end.x - start.x); p.y = start.y + ratio * (end.y - start.y); discrete_trajectory.push_back(p); }
}}void TrajectoryAndObstaclesPublisher::publish_local_obstacles(){ std::vector obstacles;
std::vector<Eigen::Vector2d> inflated_cloud = map_->getObstaclePointCloud(); for (const auto& point : inflated_cloud) { ObstacleInfo temp; temp.x = static_cast<float>(point.x()); temp.y = static_cast<float>(point.y()); obstacles.push_back(temp); } if (obstacles.empty()) return; sensor_msgs::msg::PointCloud2 visual_obs; visual_obs.header.stamp = this->now(); visual_obs.header.frame_id = "map"; visual_obs.height = 1; visual_obs.width = obstacles.size(); visual_obs.is_dense = true; sensor_msgs::PointCloud2Modifier modifier(visual_obs); modifier.setPointCloud2FieldsByString(1, "xyz"); modifier.resize(obstacles.size()); sensor_msgs::PointCloud2Iterator<float> iter_x(visual_obs, "x"); sensor_msgs::PointCloud2Iterator<float> iter_y(visual_obs, "y"); sensor_msgs::PointCloud2Iterator<float> iter_z(visual_obs, "z"); for (const auto& obs : obstacles) { *iter_x = static_cast<float>(obs.x); *iter_y = static_cast<float>(obs.y); *iter_z = 0.0f; ++iter_x; ++iter_y; ++iter_z; } obs_local_pub_->publish(visual_obs);}int main(int argc, char *argv[]){ rclcpp::init(argc, argv); auto node = std::make_shared(); rclcpp::spin(node); rclcpp::shutdown(); return 0;}
1. 核心功能概述
这个节点 (ego_planner_interactive_node) 的主要作用是:
- 接收输入:通过 RViz2 的工具栏接收用户输入的
全局路径点、障碍物位置和机器人初始位置。
- 构建地图:将输入的障碍物映射到内部的栅格地图 (
GridMap2D) 中,并进行膨胀处理。 - 规划路径:利用 A 算法*,计算从机器人当前位置到全局路径终点的无碰撞路径。
- 可视化:将计算出的轨迹、原始路径、障碍物点云发布回 RViz2 进行显示。
2. 代码逻辑流程图
- 创建 Subscriber (订阅 RViz 的点击事件)。
- 初始化
AStar 规划器和 GridMap2D 地图。
- 设置起点 (
/initialpose
):用户点击 "2D Pose Estimate" -> 更新机器人位置 cur_pose_ -> 标记需要重规划。 - 设置路径 (
/clicked_point):用户点击 "Publish Point" -> 添加点到全局路径 global_plan_traj_ -> 标记需要重规划。 - 设置障碍 (
/goal_pose):用户点击 "2D Nav Goal" -> 注意:代码中将此操作映射为添加障碍物。
**循环处理 (publish_and_plan 定时器)**:
-
检查是否需要重规划 (
needs_replan_)。 - 预处理:对全局路径进行离散化(插值),并截取距离机器人最近的点之后的路径。
- A 搜索*:运行
planner_->AstarSearch() 计算路径。
3. 关键代码段解析
A. 类的定义与初始化
使用了标准的 ROS 2
rclcpp::Node 继承方式。
TrajectoryAndObstaclesPublisher::TrajectoryAndObstaclesPublisher()
: Node("ego_planner_interactive_node"), ...
{
// 定义发布者:用于在RViz画线和画点
global_path_pub_ = create_publisher<:msg::path>("visual_global_path", 10);
// ... 其他发布者
// 关键订阅:
// 1. "Publish Point" -> 添加全局路径的路点
rviz_point_sub_ = create_subscription...("/clicked_point", ...);
// 2. "2D Pose Estimate" -> 设置机器人起点并触发规划
pose_estimate_sub_ = create_subscription...("/initialpose", ...);
// 3. "2D Nav Goal" -> 代码逻辑是将其作为障碍物添加
goal_pose_sub_ = create_subscription...("/goal_pose", ...);
// 初始化规划器和地图
init_ego_planner_base();
}
B. 核心规划逻辑 (publish_and_plan)
这是定时器调用的主函数,包含了路径规划的完整生命周期。
void TrajectoryAndObstaclesPublisher::publish_and_plan()
{
// 线程锁,防止数据竞争
std::lock_guard<std::mutex> lock(data_mutex_);
if (needs_replan_) // 只有当数据发生变化时才规划
{
// 1. 全局路径处理
// 将稀疏的点击点离散化为密集的路径点 (0.1m间隔)
discretize_trajectory(global_plan_traj_, global_plan_traj_temp, 0.1);
// 2. 寻找最近点 (Pruning)
// 找到全局路径上离机器人当前位置最近的点,从那里开始规划
// 这防止了机器人掉头回去跑之前的路径
float mindist = 100000000;
// ... (寻找最小距离索引 minddex) ...
// 重新构建要追踪的路径
// 起点是机器人当前位置,终点是全局路径的剩余部分
global_plan_traj_after.push_back(cur_pose_);
// ... 添加剩余点 ...
// 3. 设置障碍物到地图
for(int i = 0; i < obstacles_.size(); i++) {
// 将世界坐标转换为栅格索引,并标记为占据
res = map_->worldToGrid(coord);
map_->setObstacle(res, true);
}
// 4. 执行 A* 搜索
// map_->inflate(); // 膨胀障碍物,防止贴边碰撞
bool success = planner_->AstarSearch(0.1, start_pt, end_pt);
// 5. 获取结果
if (success) {
// 获取规划出的平滑路径并存入 planned_traj
std::vector<:vector2d> path = planner_->getPath();
// ... 转换格式 ...
}
}
// 6. 发布可视化数据
publish_global_path(); // 用户点的红线
publish_planned_trajectory(); // A*算出的绿线
publish_obstacles(); // 障碍物点
publish_local_obstacles(); // 膨胀后的地图障碍物
}
C. 数据转换与可视化
代码中大量的 publish_... 函数负责将内部的数据结构(如 ObstacleInfo 或 Eigen::Vector2d)转换为 ROS 2 的消息格式(nav_msgs::Path 或 sensor_msgs::PointCloud2)。
特别注意 discretize_trajectory 函数: 它实现了简单的线性插值。因为 A* 算法通常需要在栅格地图上搜索,如果目标点太远,或者作为引导的全局路径点太稀疏,直接规划效果不好。这个函数把两个点击点之间补全了很多中间点。
4. 使用时的操作逻辑
如果你运行这个节点并在 RViz2 中操作,逻辑如下:
- 添加路径:点击 RViz 工具栏的 Publish Point,在地图上依次点几个点。这些点会连成一条直线(全局参考路径)。
- 添加障碍物:点击 RViz 工具栏的 2D Nav Goal(注意不是设目标,而是设障碍)。每点一下,那个位置就会生成一个障碍物。
- 开始规划:点击 RViz 工具栏的
2D Pose Estimate 设置机器人的起点。代码接收到这个消息后,会触发
pose_estimate_callback,进而置位 needs_replan_,A* 算法开始计算从起点绕过障碍物到达全局路径终点的轨迹。
5. 代码特点总结
- 交互性强:利用 RViz 原生工具进行调试,不需要写额外的 UI。
- 鲁棒性:加入了路径裁剪(Pruning)逻辑,确保机器人始终向着前方规划。
void TrajectoryAndObstaclesPublisher::init_ego_planner_base(){ planner_ = std::make_shared(); Eigen::Vector2i map_size(map_x_size_,map_y_size_); map_ = std::make_shared(map_resolution_,map_size); map_->setInflateRadius(0.5); Eigen::Vector2i map_size_2(500,500); planner_->initGridMap(map_,map_size_2); }
这里的map_size_2(500,500);中500表示起点和终点的范围是50米,也就说,它最大只能规划50米长的路径,一般来说,针对于局部规划的自主绕障够用了。
#include "path_searching/dyn_a_star.h"using namespace std;using namespace Eigen;AStar::~AStar(){ if (GridNodeMap_ == nullptr) return; for (int i = 0; i POOL_SIZE_(0); ++i) { if (GridNodeMap_[i] == nullptr) continue; for (int j = 0; j POOL_SIZE_(1); ++j) { delete GridNodeMap_[i][j]; } delete[] GridNodeMap_[i]; } delete[] GridNodeMap_; GridNodeMap_ = nullptr;}void AStar::initGridMap(shared_ptr occ_map, const Eigen::Vector2i pool_size){ POOL_SIZE_ = pool_size; CENTER_IDX_ = pool_size / 2; GridNodeMap_ = new GridNodePtr*[POOL_SIZE_(0)]; for (int i = 0; i POOL_SIZE_(0); ++i) { GridNodeMap_[i] = new GridNodePtr[POOL_SIZE_(1)]; for (int j = 0; j POOL_SIZE_(1); ++j) { GridNodeMap_[i][j] = new GridNode; } } grid_map_ = occ_map;}double AStar::getDiagHeu(GridNodePtr node1, GridNodePtr node2){ double dx = abs(node1->index(0) - node2->index(0)); double dy = abs(node1->index(1) - node2->index(1)); int diag = min(dx, dy); double remaining = abs(dx - dy); return diag * sqrt(2.0) + remaining * 1.0; }double AStar::getManhHeu(GridNodePtr node1, GridNodePtr node2){ double dx = abs(node1->index(0) - node2->index(0)); double dy = abs(node1->index(1
) - node2->index(1)); return dx + dy; }double AStar::getEuclHeu(GridNodePtr node1, GridNodePtr node2){ return (node2->index - node1->index).head(2).norm();}vector AStar::retrievePath(GridNodePtr current){ vector path; path.push_back(current); while (current->cameFrom != nullptr) { current = current->cameFrom; path.push_back(current); } return path;}bool AStar::ConvertToIndexAndAdjustStartEndPoints(Vector2d start_pt, Vector2d end_pt, Vector2i &start_idx, Vector2i &end_idx){ if (!Coord2Index(start_pt, start_idx) || !Coord2Index(end_pt, end_idx)) return false; std::cout << start_pt return true;}bool AStar::AstarSearch(const double step_size, Vector2d start_pt, Vector2d end_pt){ ++rounds_; step_size_ = step_size; inv_step_size_ = 1.0 / step_size; center_ = (start_pt + end_pt) / 2.0; Vector2i start_idx, end_idx; if (!ConvertToIndexAndAdjustStartEndPoints(start_pt, end_pt, start_idx, end_idx)) { printf("无法处理起点或终点,强制返回!\n"); return false; } GridNodePtr startPtr = GridNodeMap_[start_idx(0)][start_idx(1)]; GridNodePtr endPtr = GridNodeMap_[end_idx(0)][end_idx(1)]; std::priority_queue, NodeComparator> empty; openSet_.swap(empty); GridNodePtr neighborPtr = nullptr; GridNodePtr current = nullptr; startPtr->index = start_idx;
startPtr->rounds = rounds_; startPtr->gScore = 0.0; startPtr->fScore = getManhHeu(startPtr, endPtr); startPtr->state = GridNode::OPENSET; startPtr->cameFrom = nullptr; openSet_.push(startPtr); endPtr->index = end_idx; double tentative_gScore; int num_iter = 0; while (!openSet_.empty()) { ++num_iter; current = openSet_.top(); openSet_.pop(); if (current->index(0) == endPtr->index(0) && current->index(1) == endPtr->index(1)) { gridPath_ = retrievePath(current); return true; } current->state = GridNode::CLOSEDSET; for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { if (dx == 0 && dy == 0) continue; Vector2i neighborIdx; neighborIdx(0) = current->index(0) + dx; neighborIdx(1) = current->index(1) + dy; if (neighborIdx(0) 1 || neighborIdx(0) >= POOL_SIZE_(0) - 1 || neighborIdx(1) 1 || neighborIdx(1) >= POOL_SIZE_(1) - 1) { continue; } neighborPtr = GridNodeMap_[neighborIdx(0)][neighborIdx(1)]; neighborPtr->index = neighborIdx; bool flag_explored = (neighborPtr->rounds == rounds_); if (flag_explored && neighborPtr->state == GridNode::CLOSEDSET) continue; neighborPtr->rounds = rounds_; if (checkOccupancy(Index2Coord(neighborPtr->index))) continue; double static_cost = sqrt(dx*dx + dy*dy); tentative_gScore = current->gScore + static_cost; if (!flag_explored) { neighborPtr->state = GridNode::OPENSET; neighborPtr->cameFrom = current; neighborPtr->gScore = tentative_gScore; neighborPtr->fScore = tentative_gScore + getHeu(neighborPtr, endPtr); openSet_.push(neighborPtr); } else if (tentative_gScore < neighborPtr->gScore) { neighborPtr->cameFrom = current; neighborPtr->gScore = tentative_gScore; neighborPtr->fScore = tentative_gScore + getHeu(neighborPtr, endPtr); } } } } return false;}
vector AStar::getPath(){ vector path; for (auto ptr : gridPath_) { Vector2d coord = Index2Coord(ptr->index); path.push_back(coord); } reverse(path.begin(), path.end()); return path;}
1. 内存管理与初始化
- 析构函数 (
~AStar): 使用了手动内存管理。由于 GridNodeMap_ 是一个动态分配的二维指针数组(GridNodePtr**),析构函数通过嵌套循环逐层释放:GridNode 对象 -> y方向数组 -> x方向数组。这确保了在对象销毁时没有内存泄漏。 - 初始化 (
initGridMap)
: 根据传入的 pool_size 分配二维网格。每个网格单元都预先分配了一个 GridNode 结构体。这种预分配策略在实时系统(如无人机、机器人)中非常常见,可以避免在搜索过程中频繁调用 new 带来的开销。
2. 启发函数 (Heuristics)
代码提供了三种经典的启发式估算方法,用于计算当前节点到终点的预期代价 :
- 对角线距离 (
getDiagHeu): 适用于允许 8 连通移动(横、竖、对角线)的网格。它计算对角线移动(代价 )和剩余直线移动(代价 1.0)的和。 -
曼哈顿距离 (
getManhHeu): 适用于只允许 4 连通移动(上下左右)的情况。公式为 。 - 欧式距离 (
getEuclHeu): 两点间的直线距离。虽然最精确,但计算开销(平方根)稍大。
3. 核心算法流程 (AstarSearch)
这是 A* 的核心逻辑,遵循以下步骤:
- 轮次管理 (
rounds_): 这是一个优化的技巧。通过比较 node->rounds 和全局的 rounds_,算法可以判断该节点在当前搜索任务中是否被访问过。这样就不需要在每次搜索前手动清空数万个网格的状态,只需 ++rounds_ 即可,大幅提高了频繁调用的效率。 - 起点终点处理: 调用
ConvertToIndexAndAdjustStartEndPoints 将世界坐标转换为网格索引。 - **开放列表 (Open Set)**: 使用优先级队列(
priority_queue),并配合自定义的比较器 NodeComparator,确保每次弹出的都是
最小的节点。 - 邻居扩展 (8-Connectivity): 通过两个嵌套循环
dx, dy \in [-1, 0, 1] 检查当前节点周围的 8 个邻居。
-
障碍物检查:调用
checkOccupancy(通常由外部地图 GridMap2D 提供实现)。 - 代价更新:计算从起点到邻居的新路径代价
tentative_gScore。如果是更短的路径,则更新节点的父节点指针(cameFrom)并放入 Open Set。
4. 路径回溯与转换
- 回溯 (
retrievePath): 从终点开始,沿着 cameFrom 指针一路找回起点,生成一个节点指针序列。 -
坐标转换 (
getPath): 将网格索引反转回世界坐标(Vector2d),并使用 std::reverse 将其调整为从起点到终点的顺序。
#include "grid_map.h"#include #include #include #include GridMap2D::GridMap2D(double resolution, const Eigen::Vector2i& world_size) : resolution_(resolution), inflate_radius_(0.5) { if (resolution <= 0) { throw std::invalid_argument("地图分辨率必须为正数(当前:" + std::to_string(resolution) + ")"); } if (world_size.x() <= 0 || world_size.y() <= 0) { throw
std::invalid_argument("世界尺寸(x,y)必须为正数(当前:(" + std::to_string(world_size.x()) + "," + std::to_string(world_size.y()) + "))"); } int grid_cols = static_cast<int>(std::ceil(static_cast<double>(world_size.x()) / resolution)); int grid_rows = static_cast<int>(std::ceil(static_cast<double>(world_size.y()) / resolution)); map_size_.x() = grid_cols; map_size_.y() = grid_rows; origin_.x() = -static_cast<double>(world_size.x()) / 2.0; origin_.y() = -static_cast<double>(world_size.y()) / 2.0; original_grid_.resize(grid_rows, std::vector<bool>(grid_cols, false)); grid_.resize(grid_rows, std::vector<bool>(grid_cols, false)); std::cout << std::endl; std::cout << world_size.x() y() std::cout << resolution std::cout << grid_cols std::cout << std::fixed << std::setprecision(2) << origin_.x() << origin_.x() + world_size.x() << origin_.y() << origin_.y() + world_size.y() << std::endl; std::cout << inflate_radius_ }void GridMap2D::setObstacle(const Eigen::Vector2i& grid_index, bool is_obstacle) { if (!isIndexValid(grid_index)) { std::cerr << grid_index.x() y() << map_size_.x()-1 y()-1 return; } original_grid_[grid_index.y()][grid_index.x()] = is_obstacle; grid_[grid_index.y()][grid_index.x()] = is_obstacle; std::cout << grid_index.x() y() << (is_obstacle ? "障碍物" : "自由空间") << std::endl;}bool GridMap2D::isObstacle(const Eigen::Vector2d& world_pos) const { Eigen::Vector2i grid_index = worldToGrid(world_pos); if (!isIndexValid(grid_index)) { return true; } bool is_obs = original_grid_[grid_index.y()][grid_index.x()]; return is_obs;
}bool GridMap2D::getInflateOccupancy(const Eigen::Vector2d& world_pos) const { Eigen::Vector2i grid_index = worldToGrid(world_pos); if (!isIndexValid(grid_index)) { return true; } bool is_occupied = grid_[grid_index.y()][grid_index.x()]; return is_occupied;}void GridMap2D::inflateObstacles(double radius) { if (radius <= 0) { std::cerr << radius return; } std::cout << radius int inflate_grid_num = static_cast<int>(std::ceil(radius / resolution_)); std::cout << inflate_grid_num std::vector<:vector2i> original_obstacle_indices; for (int row = 0; row < map_size_.y(); ++row) { for (int col = 0; col < map_size_.x(); ++col) { if (original_grid_[row][col]) { original_obstacle_indices.emplace_back(col, row); } } } if (original_obstacle_indices.empty()) { std::cout << std::endl; return; } std::cout << original_obstacle_indices.size() grid_ = original_grid_; int inflated_count = 0; for (const auto& obs_idx : original_obstacle_indices) { int obs_col = obs_idx.x(); int obs_row = obs_idx.y(); for (int dr = -inflate_grid_num; dr <= inflate_grid_num; ++dr) { for (int dc = -inflate_grid_num; dc <= inflate_grid_num; ++dc) { int curr_row = obs_row + dr; int curr_col = obs_col + dc; Eigen::Vector2i curr_idx(curr_col, curr_row); if (!isIndexValid(curr_idx)) { continue; } if (grid_[curr_row][curr_col]) { continue; } double dist_grid = std::sqrt(static_cast<double>(dr*dr + dc*dc)); if (dist_grid <= inflate_grid_num) { grid_[curr_row][curr_col] = true; inflated_count++; } } } } std::cout << std::endl; std::cout << inflated_count std::cout << (original_obstacle_indices.size() + inflated_count) }void GridMap2D::setInflateRadius(double radius) { if (radius <= 0) { std::cerr << radius return; } inflate_radius_ = radius; std::cout << radius }void GridMap2D::inflate() { std::cout << std::endl; inflateObstacles(inflate_radius_); }Eigen::Vector2i GridMap2D::worldToGrid(const Eigen::Vector2d& world_pos) const { Eigen::Vector2i grid_index; grid_index.x() = static_cast<int>(std::round((world_pos.x() - origin_.x()) / resolution_)); grid_index.y() = static_cast<int>(std::round((world_pos.y() - origin_.y()) / resolution_)); return grid_index;}Eigen::Vector2d GridMap2D::gridToWorld(const Eigen::Vector2i& grid_index) const { if (!isIndexValid(grid_index)) { std::cerr << grid_index.x() y() << std::endl; return origin_; } Eigen::Vector2d world_pos; world_pos.x() = origin_.x() + grid_index.x() * resolution_; world_pos.y() = origin_.y() + grid_index.y() * resolution_; return world_pos;}bool GridMap2D::isIndexValid(const Eigen::Vector2i& grid_index) const { bool valid = (grid_index.x() >= 0 && grid_index.x() < map_size_.x()) && (grid_index.y() >= 0 && grid_index.y() < map_size_.y()); if (!valid) { std::cerr << grid_index.x() y() << map_size_.x()-1 y()-1 } return valid;}
std::vector<:vector2d> GridMap2D::getObstaclePointCloud(bool return_inflated_map) const { std::vector<:vector2d> point_cloud; for (int row = 0; row < map_size_.y(); ++row) { for (int col = 0; col < map_size_.x(); ++col) {
bool is_obs = return_inflated_map ? grid_[row][col] : original_grid_[row][col]; if (is_obs) { double world_x = origin_.x() + col * resolution_; double world_y = origin_.y() + row * resolution_; point_cloud.emplace_back(world_x, world_y); } } } return point_cloud;}double GridMap2D::resolution() const { return resolution_;}const Eigen::Vector2i& GridMap2D::mapSize() const { return map_size_;}const Eigen::Vector2d& GridMap2D::origin() const { return origin_;}
1. 地图结构与双层网格
该类维护了两个关键的二维布尔数组(
std::vector<:vector>>):
original_grid_ (原始网格):只记录传感器或人工设置的原始障碍物。grid_ (工作/膨胀网格):存储原始障碍物加上经过“膨胀”处理后的区域。- 意义:保留原始数据是为了在动态调整膨胀半径时,不需要重新导入障碍物,只需基于原始数据重新计算即可。
2. 坐标系对齐 (World Grid)
代码实现了世界坐标(米)与栅格索引(行列)的相互转换:
- 中心对齐:构造函数中
origin_ 的计算方式
world_size / 2.0 表明,该地图将世界坐标的原点 放在了地图的几何中心。 worldToGrid:采用 std::round(四舍五入)确保坐标转换到最接近的栅格。gridToWorld:计算该栅格对应的世界坐标位置。
3. 障碍物膨胀机制 (inflateObstacles)
这是机器人导航中的核心功能。为了防止机器人撞到障碍物边缘,通常需要将障碍物“扩充”一圈,代表机器人的半径。
-
扫描:首先遍历
original_grid_ 找到所有原始障碍物。 - 局部更新:对于每个障碍物,在它周围 的矩形范围内进行搜索。
- 欧式判定:计算周围点到障碍物中心的栅格距离 。如果距离小于等于膨胀栅格数,则标记为占用。
鲁棒性:代码通过 isIndexValid 检查,确保膨胀操作不会越出数组边界。4. 碰撞检查策略
代码提供了两个查询接口:
getInflateOccupancy
:检查膨胀后的占用情况(路径规划算法 A 应该调用这个接口*,以确保生成的路径考虑了安全间距)。- 越界处理:如果查询的点在地图范围外,这两个函数都返回
true(视为障碍物)。这是一种保守安全策略,防止机器人规划出地图边界外的未知路径。
5. 其他辅助功能
getObstaclePointCloud:将地图中的所有障碍物点导出为世界坐标集合。这在可视化(在 ROS2 Rviz 中显示点云)或者与激光雷达数据对比时非常有用。- 动态调整:支持通过
setInflateRadius 修改半径并重新调用 inflate() 刷新地图。