ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RRT算法在ROS中的实现:从功能包到路径规划落地

RRT算法在ROS中的实现:从功能包到路径规划落地 简介面向ROS与移动机器人开发者这是一份RRT路径规划算法在ROS Melodic环境下的工程实现并以Turtlebot3小车作为仿真验证对象。内容围绕RRT算法核心流程展开从状态表示、随机采样、近邻搜索、树扩展到目标检测与路径平滑完整覆盖路径规划关键环节同时提供Turtlebot3模型、激光雷达地图、栅格地图处理等配套文件帮助读者在Ubuntu 18.04上快速搭建可运行的规划实验。压缩包共204个文件以Gazebo模型、rviz可视化配置、C源码、地图文件及launch启动脚本为主整体约2.6MB目录结构清晰便于按需取用。已有3403人学习下载适合进阶ROS学习者研究算法与机器人交互的联动机制读者可自行调整采样步长、目标偏置等参数在仿真中直观对比路径差异加深对随机路径规划思想的理解也能进一步掌握ROS、Gazebo与RViz的联合使用方法。 如果你和我一样习惯先从网上下载现成代码跑通再做改造那你大概率会拿到类似“RRT算法在ROS中的实现.zip”这样的压缩包。解压之后看到的不是单一源码而是一个完整的 ROS 功能包src、launch、rviz、maps可能还有 README。很多人第一步就卡在这里直接把整个文件夹丢到catkin_ws/src下结果catkin_make报错一大堆甚至提示找不到这个包。这篇文章我就以这类实现压缩包为切入把 RRT 算法在 ROS 里的落地方式、文件结构、核心逻辑、常见坑和改造方向一次说清楚。无论你是刚接触 ROS 的初学者还是想在仿真环境里快速验证路径规划方案的工程师都能从这里面拿到直接能用的东西。1. 解压之后先别急着编译看清文件结构再动手1.1 一个标准 RRT 功能包通常长什么样拿到压缩包第一步一定是把整个目录展开先看清楚里面有哪些东西。一个合格实现包一般会包含这些部分scripts/或src/核心算法源码可能是 Python 的rrt_planner.py也可能是 C 的rrt_planner.cpplaunch/启动文件用来同时拉起规划节点、RViz、地图服务等rviz/RViz 界面配置保存了视角、话题订阅、Marker 显示设置maps/仿真用的地图通常是.pgm图片加一个.yaml描述文件config/参数文件比如 YAML 格式的规划参数package.xml和CMakeLists.txtROS 功能包的身份证明和编译规则很多人习惯把压缩包直接解压到src下就开始编译结果报“找不到包”。原因往往是包的外层还套了一层文件夹导致catkin_make识别不到package.xml。所以解压后先看一眼目录层级确保package.xml在~/catkin_ws/src/的直接子目录下而不是在src/xxx/xxx/这种嵌套深处。这一步虽然简单但确实是我见过最多人踩的入门坑。1.2 环境匹配ROS 版本和依赖比代码本身更容易卡人RRT 算法本身不依赖什么重型库核心就是随机数、几何计算和地图数据解析。但 ROS 环境的版本匹配会先卡掉一半的人。如果这个包是 ROS Noetic 时代写的你直接拿到 ROS 2 Humble 上编译基本不可能一次跑通因为rospy、rclpy、catkin、colcon这些 API 和构建体系完全不一样。先确认你的环境echo $ROS_DISTRO printenv | grep ROS如果输出是noetic说明是 ROS 1如果是humble或foxy说明是 ROS 2。再看包里package.xml里的依赖声明。Python 实现通常依赖rospy、numpy、nav_msgs、visualization_msgs这些在 ROS 1 下一般已经装好。如果缺了某个消息包编译时会报No module named ...不需要慌用sudo apt install ros-${ROS_DISTRO}-xxx补上就行。ROS 环境安装本身也简单如果你还在手动配源、逐个装包强烈建议先用社区里成熟的一键安装脚本几分钟就能装好完整环境省下的时间足够你把算法多跑好几遍。1.3 从编译到启动我建议用这套固定流程我复现这类包时的操作基本是这样cd ~/catkin_ws/src unzip RRT算法在ROS中的实现.zip cd ~/catkin_ws catkin_make source devel/setup.bash roslaunch rrt_planner rrt_planner.launch如果catkin_make提示找不到包先检查目录层级如果提示脚本没有执行权限执行chmod x scripts/*.py。这一点特别容易漏因为从 Windows 解压出来的文件默认不一定带执行位而roslaunch在启动 Python 节点时会直接调用脚本没有权限就起不来。启动之后如果 RViz 打开了但地图是空的检查 launch 文件里是否调用了map_server并且 map 的 YAML 路径是否正确。很多人死磕算法代码最后发现问题出在 launch 文件里的相对路径写错了。建议所有资源路径尽量写绝对路径或者用$(find pkg_name)/maps/xxx.yaml这种 ROS 原生格式。2. RRT 算法核心逻辑它为什么能找得到路2.1 随机采样加最近邻扩展本质就是“摸着石头过河”RRT 的原理其实不复杂在地图上随机撒一个点在已经长出来的树里面找到离它最近的节点然后从这个最近节点向随机点方向走一个固定步长。如果这一段不撞障碍就把新节点加入树中。反复迭代直到树的某个节点离目标点足够近就认为找到了一条路径。你可以这样理解在一个完全陌生的大楼里找安全出口每次朝一个不确定的方向走一小段碰到墙就退回之前的岔路口换个方向时间足够长的话总能摸到出口。RRT 里的随机点就是那个不确定方向碰撞检测就是摸墙的动作。它不追求第一次就找到最优路线只追求“能够在复杂环境里找到一条可行路线”。正因为它不需要建模整个空间RRT 在高维空间和复杂约束下反而比栅格搜索更灵活。2.2 步长、迭代次数和目标偏置直接决定算法能不能用真正决定这个算法能不能跑出像样路径的是几个核心参数。参数常见取值说明step_size0.1 ~ 0.5 米每次生长的距离太大容易穿过障碍物太小则速度慢max_iterations3000 ~ 10000最大采样次数防止找不到目标时无限循环goal_bias0.05 ~ 0.2每次随机采样有多少概率直接选择目标点goal_tolerance0.1 ~ 0.3 米达到目标点的判定距离step_size是最关键的一项。如果地图分辨率是 0.05 米/像素那么步长至少应该是分辨率的 3 到 5 倍以上否则每一个新节点都缩在旧节点旁边树生长速度极慢。如果步长太大比如超过狭窄通道宽度就会频繁穿墙碰撞检测一直失败。goal_bias的作用是让树不要完全“瞎长”每次有 10% 的概率直接朝目标点伸一下这样可以明显加快收敛速度但也不能设太高否则树会失去探索能力容易被局部障碍困住。简单实现的核心代码就长这个样子for i in range(max_iterations): if random.random() goal_bias: sample goal else: sample random_point(map_width, map_height) nearest_node nearest(tree, sample) new_node steer(nearest_node, sample, step_size) if not collision(nearest_node, new_node, grid_map): tree.append(new_node) if distance(new_node, goal) goal_tolerance: return build_path(tree, new_node)这段逻辑看起来简单但工程实现里每一步都可能有陷阱尤其是collision函数怎么写后面我会专门讲。2.3 有些压缩包里装的是 RRT*别把两个版本混为一谈不少实现包会同时提供 RRT 和 RRT* 两套代码分别放在rrt.py和rrt_star.py。RRT 原版只求找到路径路径质量通常很差折线多、绕路多。RRT* 在加入新节点之后多做了两个操作重新选择父节点和重布线。简单说就是每次长出一个新节点都检查一下周围已有的节点看看能不能把新节点连接到更合理的父节点上从而让整棵树逐步向着最优路径收敛。如果你只是做算法演示或者跑通流程先看 RRT 的代码就够了主流程清晰容易调通。如果你后续要做实车路径规划那直接用 RRT* 会更合适因为它生成的路径更短也更平滑。Informed RRT* 则是进一步优化采样区域在找到第一条路径后把采样限制在包含起终点的一个椭圆范围内收敛速度更快。这类实现一般会多一个informed_rrt_star.py参数和 RRT* 类似可以后面再研究。3. 在 ROS 里组织算法节点、话题和坐标才是真正花时间的地方3.1 规划器节点应该对外暴露哪些接口算法本身写完只是第一步在 ROS 里真正的工作量在接口设计。一个规范到可以直接用的 RRT 规划节点至少应该输出这几个话题订阅/map类型nav_msgs/OccupancyGrid用来获取二维栅格地图订阅/goal类型geometry_msgs/PoseStamped用来接收导航目标点发布/path类型nav_msgs/Path用来输出规划好的路径发布/tree类型visualization_msgs/MarkerArray用来在 RViz 里实时显示树生长过程这种设计把算法和界面完全解耦。算法节点只负责“拿到地图和目标点计算出路径”可视化只是额外发布一份数据。这样做的最大好处是调试方便你可以单独向/goal发一个消息看规划节点是否有响应而不需要依赖完整导航流程。用命令行测试接口也很简单rostopic pub -1 /goal geometry_msgs/PoseStamped \ {header: {frame_id: map}, pose: {position: {x: 5.0, y: 5.0, z: 0.0}, orientation: {w: 1.0}}}3.2 让树在 RViz 里显示出来的正确姿势RViz 里显示路径和树节点一般用MarkerArray而不是直接显示Path。原因是树在生长过程中有大量节点和连线Marker的LINE_STRIP和POINTS类型能灵活控制颜色、尺寸和生命周期。核心代码大致是这样marker Marker() marker.header.frame_id map marker.type Marker.LINE_STRIP marker.action Marker.ADD marker.scale.x 0.05 marker.color.a 1.0 marker.color.g 1.0 marker.points [Point(xn.x, yn.y, z0) for n in tree] tree_pub.publish(marker)这里最容易踩的坑是scale.x忘设置或者设得太小。LINE_STRIP的线宽靠scale.x控制默认是 0你会看到 RViz 里什么都显示不出来但节点又不报错。另一个坑是frame_id必须和地图一致如果地图是map你把 Marker 的 header 设成base_link同样不显示。3.3 栅格坐标换算至少一半的 bug 都出在这nav_msgs/OccupancyGrid的本质是一个一维数组数组长度是width * height每个元素代表一个栅格。要判断机器人在某个位置有没有碰撞必须把世界坐标系的点换算成栅格索引。正确的换算方式是这样def world_to_grid(x, y, grid): gx int((x - grid.info.origin.position.x) / grid.info.resolution) gy int((y - grid.info.origin.position.y) / grid.info.resolution) if 0 gx grid.info.width and 0 gy grid.info.height: return gy * grid.info.width gx return -1特别要注意的是grid.info.origin不是 00。地图的起点可能在任何位置所以换算时一定要减去 origin再除以分辨率。很多初学者直接把x, y当成像素坐标在原点为 0,0 的测试地图上碰巧能跑通一旦换成真实地图就各种撞墙。这个问题排查起来很隐蔽因为程序不报错只是路径明显不对。树里的节点存储的是世界坐标碰撞检测的时候再转成栅格索引路径输出也是世界坐标。只要这两个方向都能正确转换整个数据流就通了。4. 复现时踩过的坑按这个顺序排查最省时间4.1 RViz 里完全没有树的影子如果你启动节点后RViz 里既看不到树也看不到路径第一反应不应该是怀疑代码有问题而是先确认数据是否正常发布。按下面这个顺序排查基本十分钟内能定位rosnode list rostopic list rostopic hz /tree rostopic hz /path如果/tree的发布频率为 0说明规划节点没有在运行或者没有收到地图。如果/path有数据但 RViz 不显示检查 RViz 左侧是否添加了MarkerArray显示项并且 Global Options 里的 Fixed Frame 是否为map。还有一个我栽过跟头的问题可视化 Marker 的scale.x设成了 0.01但 RViz 默认视角下根本看不见。后来我把线宽调到 0.05再把 Marker 的color.a设为 1问题立刻解决。这些看起来不是算法问题但在实际调试中占掉的时间比算法本身更多。4.2 树长得很慢或者老往障碍物里钻树长得很慢通常是step_size太小。你可以在 RViz 里看到树一点点蠕动半天生长不到目标点附近。解决办法是适度加大步长同时把goal_bias提到 0.1 以上。树往障碍物里钻则大概率是碰撞检测做得不够细。很多人只检查新节点本身是否落在障碍栅格里忽略了从最近节点到新节点之间的线段。如果步长跨越了一个障碍格而线段中间的采样点没有被检查路径就会穿墙。安全做法是沿着线段每隔一个栅格地球取一个点逐一检查占用状态def is_collision(p1, p2, grid): steps int(math.hypot(p2.x - p1.x, p2.y - p1.y) / grid.info.resolution) for i in range(steps 1): t i / steps x p1.x t * (p2.x - p1.x) y p1.y t * (p2.y - p1.y) idx world_to_grid(x, y, grid) if grid.data[idx] 50: return True return Falsesteps至少要按地图分辨率算确保每个栅格都被覆盖到。很多人调了一天算法最后发现是这里少了循环。4.3 目标点明明在地图上却一直规划失败这种情况主要有三个原因。一是目标点的frame_id和地图不一致比如你用 RViz 的 “2D Nav Goal” 按钮给目标点时有时候会发到map以外的坐标系导致目标点实际上跑到地图外面去了。二是判断到达的条件太苛刻如果goal_tolerance设成了 0.01而导航目标本身存在误差那很可能永远达不到。三是检查目标点是否在膨胀层或障碍物内部如果目标点紧贴墙壁RRT 就算找到最近节点也满足不了无碰撞条件。调试时可以单独打印目标和最终节点的坐标rospy.loginfo(goal: %.2f, %.2f, goal.x, goal.y) rospy.loginfo(nearest: %.2f, %.2f, nearest.x, nearest.y)从日志里很快能看出坐标是否合理。如果坐标没错再降低goal_tolerance的值比如从 0.2 开始试基本都能解决。5. 从“能跑”到“能用”往这几个方向改造更有价值5.1 路径平滑不要直接拿折线去做控制RRT 生成出来的路径本质上是一堆折线转折处非常生硬如果直接把这条路径发给底盘机器人会走走停停姿态抖动严重。我的做法是先对路径做简化去掉那些在一条直线上多余的点然后用三次样条插值做平滑。更简单的方案是直接用均匀采样加梯度下降把折线“熨平”。在 ROS 里你只需要把平滑后的点重新写进nav_msgs/Path发布出去就行。路径平滑本身不是必须项但如果你做的是实车或者带运动学模型的仿真这一步直接决定路径能不能执行得下去。5.2 把静态地图换成 costmap机器人半径必须考虑很多 RRT 实现包默认订阅/map也就是静态地图。静态地图里障碍物是二值的有障碍就是占用的没有就是空闲的。但机器人有宽度不可能贴着墙走。不处理这问题的话路径规划出来看起来没问题实际执行时机器人会蹭墙甚至卡死。最简单的改造方法是在读地图时做一次膨胀把障碍物周围的栅格都标记成“不可通行”膨胀半径至少等于机器人半径。更正规的做法是接入costmap_2d直接订阅/move_base/global_costmap/costmap让膨胀半径由 costmap 的 inflation 层统一管理。这样 RRT 的碰撞检测就能直接使用包含膨胀信息的地图路径自然会远离墙壁。5.3 全局 RRT 和局部规划器配合使用才是完整方案如果你只是想演示算法单独跑 RRT 已经够了。但如果你想把它用在实际导航系统里建议全局规划用 RRT 或 RRT*局部规划用 TEB 或 DWA。RRT 负责在全局地图上找一条从起点到目标点的粗路径局部规划器再基于实时传感器数据在靠近障碍物时做平滑避障。两条规划链路是叠加关系不是替代关系。我自己的习惯是先用静态地图把 RRT 全局路径跑通确认没有明显穿墙问题再叠加局部规划器。等整体跑顺之后再把 costmap 和传感器数据加进来一步一步过渡到真机。别一上来就在实车上调试那样你根本分不清是全局规划的问题还是局部避障的问题。先让树在地图上长出来看到一条合理路径再谈后面的控制。最后再分享一个小技巧每次调参之前把max_iterations设大一些比如 10000同时打开可视化 Marker观察树的生长过程。RRT 这个算法最大的优势就是每一步都能看到树在长大这种实时反馈比任何日志都好用。等你能直观地看到树绕开障碍、逼近目标你对这个算法的理解才算真正到位了。本文还有配套的精品资源点击获取
返回列表