1. 项目概述与核心价值
在机器人操作系统(ROS)的生态里,路径规划是让机器人从A点移动到B点的“大脑”。我们熟知的A*、Dijkstra这类基于栅格地图的算法,就像是给机器人一张精细的网格纸,让它一格一格地找路。这在室内、结构化环境中非常有效。但当你把机器人放到一个大型的、结构复杂的仓库,或者一片广阔的野外区域时,这种“网格思维”就会遇到麻烦:计算量爆炸、路径拐弯抹角不自然,而且对地图分辨率极度敏感。
这时,拓扑(Topo)规划算法的价值就凸显出来了。它不再纠结于每一个像素点,而是像人类看地图一样,关注关键的“地点”(节点,如走廊交叉口、房间门口)和连接它们的“道路”(边)。这就是拓扑地图的核心——一种用图(Graph)来抽象表达环境连通性的方法。ROS环境下Topo算法的C++实现这个项目,正是要搭建一座桥梁,将这种高效的抽象规划能力,融入到ROS这个机器人开发的“标准车间”里。
简单来说,这个项目的目标是:在ROS中,用C++实现一套完整的拓扑路径规划器。它能够读取或生成环境的拓扑地图,接收ROS标准的导航目标,然后快速计算出基于拓扑节点序列的最优路径,并输出给下游的局部规划器或控制器去执行。这对于仓储物流机器人、园区巡检车、甚至家用服务机器人在多房间场景下的高效导航,有着实实在在的意义。如果你正在为机器人在大范围场景下的导航效率发愁,或者想深入理解规划算法如何与ROS框架深度融合,那这个实现过程会给你带来不少启发。
2. 拓扑路径规划的核心思想与ROS适配分析
在开始敲代码之前,我们必须把Topo算法的“心法”和ROS的“招式”理解透彻,这样才能让它们完美配合。
2.1 拓扑地图:从像素点到抽象图
拓扑地图的核心是降维和抽象。假设我们有一个办公室地图,里面有前台、办公区A、办公区B和会议室。
- 栅格表示:一个1000x1000像素的二值图像,黑色是障碍物,白色是可通行区域。机器人需要在上百万个点中搜索。
- 拓扑表示:我们只定义4个关键节点(Node):
N_front_desk,N_office_A,N_office_B,N_meeting_room。然后定义连接它们的边(Edge):(N_front_desk, N_office_A),(N_front_desk, N_office_B),(N_office_A, N_meeting_room)。每条边可以有权重,比如实际距离或通行代价。
当机器人需要从前台去会议室时,拓扑规划器不再搜索栅格,而是在这个小小的图上运行图搜索算法(如Dijkstra或A*),瞬间得到路径:N_front_desk -> N_office_A -> N_meeting_room。这个节点序列就是高层指令。
2.2 为何在ROS中用C++实现?
ROS支持多种语言,但C++依然是性能敏感模块的首选,尤其是路径规划这种需要频繁计算的核心组件。
- 性能优势:C++的零成本抽象和对内存的直接控制,能让图搜索、代价计算等循环密集型操作达到最高效率。
- 与现有生态无缝集成:ROS Navigation Stack的核心组件(如
global_planner、move_base)本身就是C++写的。用C++实现可以更方便地以插件(plugin)形式集成,复用其消息接口(如nav_msgs::Path、geometry_msgs::PoseStamped)。 - 工程化与稳定性:对于需要部署到实际机器人上的系统,C++在资源管理和跨平台兼容性上更成熟可靠。
2.3 ROS导航框架下的定位
在标准的ROS导航堆栈中,move_base节点协调全局规划器(Global Planner)和局部规划器(Local Planner)。我们的Topo规划器目标就是成为一个全局规划器插件。
- 输入:
costmap_2d::Costmap2DROS提供的代价地图(用于拓扑地图的构建或验证)、目标位姿。 - 输出:一个由世界坐标系下位姿点组成的
nav_msgs::Path消息。虽然路径由拓扑节点序列决定,但最终输出需要转换成连续的位姿点,以便局部规划器跟踪。 - 核心任务:实现
nav_core::BaseGlobalPlanner接口。这是ROS为全局规划器定义的“契约”,只要实现了它规定的几个关键函数(特别是makePlan),我们的规划器就能被move_base直接调用。
3. 系统架构设计与模块分解
一个健壮的Topo规划器不能只是一个算法函数,它需要一套可维护、可扩展的架构。这里我设计了一个四层模块化结构,这也是我在实际项目中反复迭代后的经验总结。
3.1 整体架构图(概念层)
[ROS Master] | | (Topic/Service) [Topo Planner Node] | ---------------------------------------- | | | [Topo Map Manager] [Planner Core] [ROS Interface] | | | [Graph Data] [Search Algorithm] [Config Server]3.2 核心模块详解
3.2.1 拓扑地图管理器 (TopoMapManager)
这是项目的基石,负责拓扑地图的生命周期。它必须解决地图从哪里来的问题。
- 功能:加载、保存、访问、更新拓扑地图。
- 数据结构设计:
struct TopoNode { int id; std::string name; geometry_msgs::Pose pose; // 节点在世界坐标系中的位置 std::vector<int> connected_edge_ids; // 关联的边 // 可扩展属性:节点类型(门、电梯)、通行约束等 }; struct TopoEdge { int id; int from_node_id; int to_node_id; double cost; // 权重,可以是欧氏距离、固定代价或动态代价 // 可扩展属性:宽度、方向性(单向/双向)、最大速度等 }; class TopoMap { private: std::map<int, TopoNode> nodes_; std::map<int, TopoEdge> edges_; // 使用map便于通过ID快速查找,也可用vector+索引优化内存。 public: bool loadFromYAML(const std::string& file_path); bool saveToYAML(const std::string& file_path); const TopoNode* getNode(int id) const; std::vector<int> getNeighborNodeIds(int node_id) const; // ... 其他方法 }; - 地图来源实践:
- 手动标注:对于已知的、结构稳定的环境,用RViz的
Publish Point工具点击获取关键点坐标,然后编写YAML文件定义连接关系。这是最直接、可控的方式。 - 自动提取:这是一个更有挑战性但也更自动化的方向。可以从高精度栅格地图或点云地图中,使用图像处理(如骨架化、关键点检测)或机器学习方法来识别走廊、路口、房间,并自动生成拓扑图。初期建议从手动标注开始,确保算法核心正确,再考虑自动化。
- 手动标注:对于已知的、结构稳定的环境,用RViz的
3.2.2 规划器核心 (PlannerCore)
这是算法灵魂所在,封装了在图上的搜索逻辑。
- 核心接口:
class TopoPlannerCore { public: // 核心规划函数 bool makePlan(const TopoMap& map, int start_node_id, int goal_node_id, std::vector<int>& node_path); // 输出节点ID序列 // 设置搜索算法(策略模式) void setSearchAlgorithm(const std::string& algo); private: std::unique_ptr<SearchAlgorithm> search_algo_; }; - 搜索算法选型与实现:
- Dijkstra算法:经典的最短路径算法,保证找到全局最优解(代价最小)。在节点数不多(几百个)的拓扑图中,它的性能完全足够,且实现简单可靠。对于大多数室内/园区场景,我首推Dijkstra,它的稳定性比那一点可能的性能提升更重要。
- A算法*:如果拓扑图很大,可以考虑A*。关键在于设计一个合理的启发式函数(Heuristic)。对于拓扑图,一个简单有效的启发式函数是节点间的欧几里得距离。这需要我们在
TopoNode中存储位置信息。A*可以更快地导向目标,减少搜索范围。 - 实现要点:需要维护
open list和closed list。C++中可以使用std::priority_queue(优先队列)来实现open list,效率很高。记得为队列元素设计一个包含节点ID、到达代价g和预估总代价f的结构体,并重载比较运算符。
3.2.3 ROS接口层 (ROSInterface)
此模块负责与ROS世界通信,是规划器能“干活”的对外窗口。
- 核心类:继承自
nav_core::BaseGlobalPlanner。#include <nav_core/base_global_planner.h> #include <ros/ros.h> class TopoGlobalPlanner : public nav_core::BaseGlobalPlanner { public: TopoGlobalPlanner(); virtual void initialize(std::string name, costmap_2d::Costmap2DROS* costmap_ros); virtual bool makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, std::vector<geometry_msgs::PoseStamped>& plan); // ... 析构等其他方法 private: costmap_2d::Costmap2DROS* costmap_ros_; // 代价地图指针,可能用于验证节点可达性 std::shared_ptr<TopoMapManager> map_manager_; std::shared_ptr<TopoPlannerCore> planner_core_; ros::NodeHandle nh_; ros::NodeHandle private_nh_; // 用于读取私有参数 }; - 关键任务:
- 初始化 (
initialize):在这里加载参数(如拓扑地图文件路径),初始化TopoMapManager和PlannerCore。 - 坐标转换:
makePlan接收到的起点和目标点是世界坐标下的位姿。我们需要一个关键函数:findNearestTopoNode。这个函数负责将真实的(x, y)坐标匹配到拓扑地图中最近的可通行节点上。匹配精度直接影响了规划的可用性。 - 路径平滑与填充:规划核心返回的是节点ID序列。但
move_base和局部规划器期望的是一条由密集位姿点组成的连续路径。因此,我们需要将节点序列“翻译”成路径。简单做法是在相邻两个节点的位姿之间进行线性插值,生成一系列中间点。更高级的做法可以考虑用贝塞尔曲线或样条曲线进行平滑,使路径更符合机器人的运动学。
- 初始化 (
3.2.4 工具与配置模块
- 动态参数配置:使用
dynamic_reconfigure让用户在运行时调整参数,如切换搜索算法、设置路径插值密度、调整节点匹配距离阈值等,无需重新编译。 - RViz可视化插件:编写一个RViz插件,用于显示拓扑地图(节点和边),并高亮显示当前计算出的路径。这对于调试和演示至关重要。可以发布
visualization_msgs::MarkerArray消息来在RViz中绘制图形。
4. 关键实现细节与C++编程实践
理论架构清晰后,我们深入到代码层面,看看几个最容易出问题的地方该如何实现。
4.1 拓扑地图的存储与加载:YAML vs. 代码定义
虽然可以在代码里硬编码节点和边,但这极度不灵活。强烈推荐使用YAML文件。
# topo_map.yaml nodes: - id: 0 name: entrance pose: x: 1.0 y: 2.0 yaw: 0.0 - id: 1 name: corridor_junction pose: x: 5.0 y: 2.0 yaw: 0.0 edges: - id: 0 from: 0 to: 1 cost: 4.5 # 计算出的欧氏距离或自定义代价使用yaml-cpp库可以轻松解析。在TopoMapManager::loadFromYAML中,遍历nodes和edges列表,填充到std::map中。
注意:节点ID是整型且应唯一,它在内部作为查找键值。
name字段是为了人类可读。yaw可以用来指示机器人在到达该节点时的推荐朝向,这对后续控制有好处。
4.2 最近节点查找:规划可靠性的第一道关
findNearestTopoNode函数是连接连续世界和离散拓扑图的关键。
int TopoMapManager::findNearestTopoNode(const geometry_msgs::PoseStamped& pose, double max_distance) { int nearest_id = -1; double min_dist_sq = max_distance * max_distance; // 使用平方比较,避免开方运算 for (const auto& pair : nodes_) { const TopoNode& node = pair.second; double dx = pose.pose.position.x - node.pose.position.x; double dy = pose.pose.position.y - node.pose.position.y; double dist_sq = dx*dx + dy*dy; if (dist_sq < min_dist_sq) { min_dist_sq = dist_sq; nearest_id = node.id; } } return nearest_id; // 如果没找到,返回-1 }避坑指南:
max_distance参数非常重要。设置太小,机器人稍微偏离节点就规划失败;设置太大,可能匹配到错误的节点。需要根据环境节点密度来调整,通常设为节点间平均距离的1/2到2/3。- 可以考虑更复杂的匹配策略,比如不仅看距离,还看当前机器人的朝向与节点
yaw的夹角。
4.3 Dijkstra算法的C++高效实现
这里给出一个基于标准库的简洁实现,重点在于数据结构和流程。
std::vector<int> DijkstraSearch::search(const TopoMap& map, int start_id, int goal_id) { struct NodeCost { int node_id; double cost; bool operator>(const NodeCost& other) const { return cost > other.cost; } }; std::priority_queue<NodeCost, std::vector<NodeCost>, std::greater<NodeCost>> open_set; std::unordered_map<int, double> g_score; // 从起点到当前节点的实际代价 std::unordered_map<int, int> came_from; // 记录父节点,用于回溯路径 g_score[start_id] = 0.0; open_set.push({start_id, 0.0}); while (!open_set.empty()) { NodeCost current = open_set.top(); open_set.pop(); if (current.node_id == goal_id) { // 路径找到,回溯 return reconstructPath(came_from, current.node_id); } // 遍历当前节点的所有邻居 for (int neighbor_id : map.getNeighborNodeIds(current.node_id)) { const TopoEdge* edge = map.getEdge(current.node_id, neighbor_id); // 需要实现根据两端节点找边的函数 if (!edge) continue; double tentative_g_score = g_score[current.node_id] + edge->cost; if (g_score.find(neighbor_id) == g_score.end() || tentative_g_score < g_score[neighbor_id]) { // 找到更优路径 came_from[neighbor_id] = current.node_id; g_score[neighbor_id] = tentative_g_score; open_set.push({neighbor_id, tentative_g_score}); } } } // 开放集为空仍未找到目标 return std::vector<int>(); // 返回空路径 }性能与技巧:
- 使用
std::unordered_map来存储g_score和came_from,平均查找时间复杂度O(1)。 open_set(优先队列)中可能包含同一个节点的多个不同代价的副本。当我们从队列中取出一个节点时,需要检查其代价是否与当前g_score中记录的一致,如果不一致(说明这个节点已经被以更低的代价访问过了),则直接跳过。上述简化代码省略了这一步检查,在节点数不多时问题不大,但在大图中为了严谨和效率应该加上。reconstructPath函数就是一个简单的从目标节点沿came_from映射反向追溯到起点的过程。
4.4 从节点序列到连续路径:插值与平滑
获得节点ID序列[A, B, C]后,需要生成nav_msgs::Path。
- 线性插值:最简单的方法。在节点A和B的位姿之间,等距离插入N个中间点。
N = ceil(两点距离 / 分辨率)。分辨率通常设置为局部规划器或控制器所需的分辨率(如0.05米)。geometry_msgs::PoseStamped interpolate(const geometry_msgs::Pose& start, const geometry_msgs::Pose& end, double ratio) { geometry_msgs::PoseStamped pose; pose.pose.position.x = start.position.x + ratio * (end.position.x - start.position.x); pose.pose.position.y = start.position.y + ratio * (end.position.y - start.position.y); // 朝向可以用四元数球面线性插值(SLERP),简单情况也可以用线性插值yaw角。 // ... return pose; } - 曲线平滑:线性插值路径会有尖角。可以使用二次贝塞尔曲线(三个控制点:前节点、中间点、后节点)或三次样条曲线进行平滑。ROS的
navfn包中的全局规划器就使用了梯度下降法进行路径平滑。引入平滑后,路径更优,但对计算有一定要求。
实操心得:在项目初期,强烈建议先实现线性插值,确保整个规划-输出流程跑通。平滑优化可以作为一个后续的增强功能。过早引入复杂性会大大增加调试难度。
5. ROS集成、测试与调试全流程
算法模块写好之后,如何让它成为一个真正的ROS节点并工作起来?这是从代码到系统的一步。
5.1 创建ROS功能包与配置
catkin_create_pkg topo_planner roscpp nav_core costmap_2d tf geometry_msgs visualization_msgs dynamic_reconfigureCMakeLists.txt关键配置:确保正确链接库,并安装插件描述文件。add_library(topo_global_planner_lib src/topo_global_planner.cpp src/topo_map_manager.cpp ...) target_link_libraries(topo_global_planner_lib ${catkin_LIBRARIES}) # 注册为全局规划器插件 catkin_package( LIBRARIES topo_global_planner_lib CATKIN_DEPENDS roscpp nav_core costmap_2d tf )- 插件描述文件:在功能包根目录创建
global_planner_plugin.xml。<library path="lib/libtopo_global_planner_lib"> <class name="topo_planner/TopoGlobalPlanner" type="topo_planner::TopoGlobalPlanner" base_class_type="nav_core::BaseGlobalPlanner"> <description> A topological global planner plugin for ROS navigation. </description> </class> </library> package.xml:添加<export>标签声明插件。<export> <nav_core plugin="${prefix}/global_planner_plugin.xml" /> </export>
5.2 编写Launch文件与参数配置
创建一个launch文件来启动测试节点或集成到move_base。
<!-- test_topo_planner.launch --> <launch> <!-- 启动一个静态地图服务器(如果有静态地图的话) --> <node name="map_server" pkg="map_server" type="map_server" args="$(find your_map_pkg)/map.yaml"/> <!-- 启动代价地图 --> <node name="costmap_node" pkg="costmap_2d" type="costmap_2d_node"> <rosparam file="$(find topo_planner)/params/costmap_common_params.yaml" command="load" ns="global_costmap"/> <!-- ... 其他参数 --> </node> <!-- 启动move_base,并指定我们的全局规划器 --> <node name="move_base" pkg="move_base" type="move_base" output="screen"> <param name="base_global_planner" value="topo_planner/TopoGlobalPlanner"/> <rosparam file="$(find topo_planner)/params/topo_planner_params.yaml" command="load"/> <!-- ... 其他move_base参数 --> </node> <!-- 启动RViz进行可视化 --> <node name="rviz" pkg="rviz" type="rviz" args="-d $(find topo_planner)/rviz/topo_planner.rviz"/> </launch>topo_planner_params.yaml文件包含了规划器自身的参数:
TopoGlobalPlanner: topo_map_file: "$(find topo_planner)/maps/office_topo.yaml" search_algorithm: "dijkstra" # 或 "astar" node_match_threshold: 1.0 # 匹配节点的最大距离(米) path_interpolation_resolution: 0.05 # 路径插值分辨率(米)5.3 分阶段测试策略
不要试图一次性把所有功能集成测试。分阶段进行,步步为营。
- 单元测试(脱离ROS):使用
gtest为TopoMapManager、PlannerCore等核心类编写测试。测试地图加载是否正确、Dijkstra算法在简单图上是否能算出预期路径、最近节点查找函数是否准确。这是保证代码质量的基础,能节省大量集成调试时间。 - 独立节点测试:写一个简单的ROS节点,手动发布起点和目标点,调用规划器的
makePlan服务或直接调用其函数,并将计算出的路径用visualization_msgs::Marker或nav_msgs::Path发布出来,在RViz中查看。此时先不集成move_base,专注于验证规划逻辑本身。 - RViz交互测试:使用RViz的
2D Nav Goal工具指定目标。在RViz中清晰地显示拓扑节点(用球形Marker)、边(用线条Marker)和规划出的路径(用带箭头的线条)。观察路径是否合理,节点匹配是否准确。 - 集成到move_base:修改move_base的参数,将全局规划器替换为我们的插件。使用
rosrun rqt_reconfigure rqt_reconfigure动态调整参数,测试规划器在完整导航栈中的表现。关注/move_base/global_plan话题发布的路径。
5.4 常见问题与排查实录
在实际集成中,你几乎一定会遇到下面这些问题。这里是我的“踩坑”记录和解决方案。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 规划器插件无法加载 | 1. 插件描述文件路径错误或格式不对。 2. 库文件未正确编译或链接。 3. package.xml中<export>标签缺失。 | 1. 检查global_planner_plugin.xml文件是否存在,类名和命名空间是否正确。2. 运行 rospack plugins --attrib=plugin nav_core,查看插件是否在列表中。3. 使用 ldd命令检查规划器库文件的依赖是否满足。 |
makePlan返回false,路径为空 | 1. 拓扑地图未成功加载。 2. 起点/目标点无法匹配到任何拓扑节点。 3. 起点和目标点在图中的同一连通分量,但搜索算法bug。 | 1. 在initialize和makePlan函数中加入ROS_INFO日志,打印地图节点数、加载状态。2. 打印起点/目标点坐标,并打印 findNearestTopoNode的结果,检查匹配阈值max_distance是否合理。3. 单独写一个测试程序,用最小的拓扑图(如两个节点一条边)测试 PlannerCore。 |
| 规划出的路径在RViz中显示跳跃或不在节点上 | 1. 节点序列到路径的插值逻辑错误。 2. 拓扑节点自身的位姿数据有误。 3. 坐标系(frame_id)不匹配。 | 1. 检查插值函数,确保插值比例计算正确,生成的路径点坐标在两点连线上。 2. 在RViz中同时发布节点Marker,看其位置是否与预期一致。 3.重中之重:确保 nav_msgs::Path消息的header.frame_id与RViz中显示的世界坐标系(通常是map)一致。规划器内部计算可能是在map坐标系下,但发布时写错了frame_id。 |
| 路径有尖角,机器人转弯不流畅 | 使用了简单的线性插值,在节点处方向突变。 | 1. 这是预期行为,证明你的基础功能是工作的! 2. 实现路径平滑算法。一个快速的改进是:在输出路径前,对节点位姿的朝向进行平滑处理,例如,让机器人在接近节点时就开始转向下一个节点的方向。 |
| 动态障碍物出现时规划失效 | 拓扑规划器本身不考虑动态障碍物,它只处理静态连通性。 | 这是拓扑规划器的固有局限。解决方案是分层规划:Topo规划器负责高层粗规划(节点序列),局部规划器(如DWA、TEB)负责底层细规划,并利用局部代价地图规避动态障碍物。确保你的拓扑路径为局部规划器提供了合理的参考。 |
一个关键的调试技巧:大量使用ROS_INFO_STREAM或ROS_DEBUG_STREAM输出中间变量。例如,在findNearestTopoNode函数中输出所有节点的距离;在搜索算法中输出open_set的大小和当前处理的节点。配合rqt_console查看日志,可以清晰地看到程序的执行流程和数据状态。
6. 性能优化与高级功能拓展
当基础功能稳定后,可以考虑以下方向来提升规划器的实用性和鲁棒性。
6.1 引入代价地图进行动态验证
纯粹的拓扑规划不知道两点之间是否有新出现的障碍物。我们可以利用ROS提供的costmap_2d::Costmap2DROS来增强。
- 思路:在
makePlan中,获得节点序列后,在输出最终路径前,对相邻节点连成的线段进行“射线检查”。使用代价地图的worldToMap和getCost函数,采样线段上的点,检查其代价值是否超过障碍物阈值(如costmap_2d::LETHAL_OBSTACLE)。 - 实现:如果发现某条边被阻断,有两种策略:(1) 从拓扑图中临时移除这条边,重新规划;(2) 在规划前就根据代价地图信息动态更新边的代价(如设置为无穷大)。这使拓扑规划器具备了初步的动态环境适应能力。
6.2 支持多层级拓扑地图
对于多层建筑(如带电梯的办公楼),可以扩展拓扑地图结构。
- 数据结构:为
TopoNode增加level或floor属性。为TopoEdge增加type属性(如STAIR,ELEVATOR,CORRIDOR)。 - 搜索算法:在搜索时,只有当边类型允许且节点层级符合条件时,才将其视为连通。例如,一个“电梯”边只能连接不同楼层的特定电梯口节点。
- 路径描述:输出的路径序列中,可以包含层间切换的动作描述,这对机器人高层控制有指导意义。
6.3 与语义信息结合
这是拓扑规划非常前沿且实用的拓展。为节点和边赋予语义标签。
- 节点语义:
ROOM,DOOR,ELEVATOR_LOBBY,CHARGING_STATION。 - 边语义:
HALLWAY,DOORWAY,NARROW_PASSAGE。 - 应用:
- 人性化指令:规划结果不仅是“去坐标(x,y)”,而是“穿过走廊,进入301会议室”。
- 约束性规划:可以指定“避开狭窄通道”或“必须经过充电站”。
- 行为触发:当路径包含
DOOR节点时,触发“开门”行为。
实现上,需要在搜索算法的代价函数中考虑语义代价。例如,狭窄通道的边权重可以设置得更高,让规划器倾向于选择宽阔的走廊。
6.4 使用更高效的数据结构与算法
当拓扑图变得非常大(例如用于整个城市街区)时,需要考虑性能。
- 空间索引:对于
findNearestTopoNode,线性遍历所有节点效率是O(n)。可以使用空间索引数据结构,如四叉树(Quadtree)或KD-Tree,将节点组织起来,实现O(log n)级别的近邻搜索。PCL库或FLANN库提供了现成的实现。 - 启发式搜索优化:如果使用A*算法,一个更好的启发式函数可以大幅提升搜索速度。除了欧氏距离,可以考虑使用预计算的路径距离(如Landmark法)或学习得到的启发函数。
从手动标注一个简单办公室的拓扑图开始,到实现一个能处理多层楼、语义丰富、并能与动态环境交互的规划器,这个过程充满了挑战,但也正是机器人软件开发的魅力所在。这个C++实现不仅是一个可用的工具,更是一个理解ROS插件机制、图搜索算法和机器人导航架构的绝佳载体。