ARTICLE DETAIL

资讯详情

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

AGV激光SLAM导航实战:ROS+Python从建图到避障全流程解析

AGV激光SLAM导航实战:ROS+Python从建图到避障全流程解析 1. 写在动手之前从建图成功到稳定跑的差距在哪很多搞AGV的朋友应该都有过这种经历建图跑得挺顺地图看着也漂亮结果一切换到自动导航小车就开始各种撞墙、原地转圈、莫名其妙急停。我最早接触AGV路径规划那阵子也是这样后来把PythonROS这套激光SLAM导航链路完整拆开调了一遍才算真正弄明白问题出在哪。这篇文章就围绕AGV路径规划这个核心把我用PythonROS实现激光SLAM导航的完整过程包括环境搭建、建图、导航框架、避障代码以及调参踩坑记录一次性讲清楚。这篇内容适合正在做AGV或者移动机器人项目的工程师也适合准备入门ROS导航的朋友。你不需要有很深的路径规划算法功底但最好对Linux基本命令和Python语法不陌生。我会把每一步都写明白为什么这么做、不这么做会踩什么坑、出现故障怎么排查。整套方案我实机验证过不是纸上谈兵照着做能省掉不少弯路。1.1 一个典型的AGV导航调试现场我先描述一个场景一台差速底盘AGV头顶装一个单线激光雷达工控机里跑着Ubuntu和ROS任务是让它在仓库里从A点自主走到B点避开托盘和货架。很多人拿到这个需求后的第一反应是“SLAM建个图然后move_base发个目标点就行了”。理论上没错但实际跑起来会发现建好的地图在Rviz里看没问题导航时定位却时不时跳一下车就开始画龙。明明激光雷达已经扫到障碍物了车还是硬往前顶碰到之后才急停。全局路径规划出来的轨迹绕远路局部规划又频繁重新规划车走走停停效率很低。这些问题的根源往往不在某一个单独环节而是整条链路——从激光数据处理、TF坐标变换、定位精度到代价地图参数、规划器配置——没有协同工作。所以我在后面的章节里会按正常做项目时的推进顺序来讲先搭环境再建图再搭导航框架再补避障代码最后集中调参。1.2 这套方案的技术选型概览在开始之前我先交代一下这套方案的整体选型后面所有步骤都基于这个组合模块我用的方案备选方案系统Ubuntu 20.04 ROS NoeticUbuntu 22.04 ROS 2 Humble激光SLAMgmappingcartographer / slam_toolbox定位AMCL无手动定位全局路径规划navfn / global_plannerA*Dijkstra局部路径规划DWATEB阿克曼底盘推荐底盘接口差速驱动 里程计全向轮 / 阿克曼为什么用Noetic而不是Humble如果你是第一次搭这套系统Noetic的社区资料最多网上随便一搜就是现成的报错解决方案对入门非常友好。ROS 2 Humble的实时性和通信机制确实更好但很多传统AGV厂家的上层软件还是ROS 1的老接口团队协作时版本壁垒很现实。我的建议是学习阶段直接用Noetic等整个导航链路跑通了再迁到Humble不迟。2. 环境准备最容易翻车的三个环节ROS版本、Python环境与仿真平台环境搭建是劝退新手最多的地方。我见过太多人卡在安装这一步还没开始写代码就放弃了。所以这一节我把最容易翻车的环节单独拎出来按顺序处理完后面就顺利了。2.1 ROS版本选择与系统安装如果你用的是Ubuntu 20.04ROS对应版本是NoeticUbuntu 22.04对应HumbleROS 2。这里有个关键点很多人会搞混Ubuntu 18.04对应的是ROS Melodic它的Python默认是2.7跟现在大量Python 3的开源库不兼容。所以我现在的建议是别用Melodic做新项目除非你维护的是存量老车。系统装好后先把软件源和基础工具补齐sudo apt update sudo apt install -y git curl vim net-tools然后设置ROS软件源并安装ROS Noetic完整版桌面版。这一步如果你挨个敲API key和源地址很容易因为网络问题卡住。我这边实测省心的方法是用网络上常见的ROS安装脚本比如鱼香ROS的一键安装脚本。它其实就是个bash脚本会自动识别你的Ubuntu版本、配置源、安装ROS和常用依赖运行方式wget http://fishros.com/install -O fishros . fishros脚本运行后会让你选择安装内容比如ROS Noetic完整版、ROS 2 Humble、或者Gazebo仿真按数字键选择就行。这个脚本的价值在于它把源配置、公钥导入、rosdep初始化这些琐碎但容易出错的步骤全部封装了。我第一次手动配置时卡在公钥导入上用这个脚本十分钟就装完了。装完之后记得初始化rosdepsudo rosdep init rosdep updaterosdep的作用是编译工作空间时自动解析依赖包这一步不执行后面catkin_make的时候会报各种找不到依赖的错误。2.2 Python环境配置ROS Noetic的Python版本是Python 3.8系统自带的python3就能满足大部分需求。我在项目里习惯用venv给机器人代码建独立虚拟环境避免跟系统Python装包时互相冲突。但注意一点ROS节点的执行依赖系统Python环境里的rospy包所以你不能直接在虚拟环境里跑ROS节点除非在创建虚拟环境时加--system-site-packages参数sudo apt install -y python3-venv python3-pip python3 -m venv --system-site-packages ~/agv_env source ~/agv_env/bin/activate这样虚拟环境既能用系统装好的rospy又能用pip安装numpy、matplotlib这些做路径规划算法验证的库。如果你用VSCode做开发记得在.vscode/settings.json里把Python解释器路径指向~/agv_env/bin/python不然调试时import rospy会报错。另外我强烈建议装上rospy-tutorials和tf2-tools这两个包调试TF坐标变换时非常有用sudo apt install -y ros-noetic-rospy-tutorials ros-noetic-tf2-tools2.3 建一个工作空间并准备Gazebo仿真环境没有真车的时候先在Gazebo里建一个仿真AGV模型调通整条链路是最稳妥的做法。先创建工作空间mkdir -p ~/agv_ws/src cd ~/agv_ws catkin_make source devel/setup.bash echo source ~/agv_ws/devel/setup.bash ~/.bashrcGazebo和导航相关依赖包一次装齐sudo apt install -y ros-noetic-gazebo-ros ros-noetic-gazebo-plugins ros-noetic-navigation ros-noetic-slam-gmapping ros-noetic-amcl ros-noetic-map-server ros-noetic-move-base ros-noetic-dwa-local-planner这里解释一下为什么要装navigation这个元包。它会一并安装move_base、amcl、map_server、costmap_2d这些导航核心组件省得一个一个找。Gazebo里跑仿真时激光雷达模型通常给的是sensor_msgs/LaserScan话题这个格式跟后面避障代码直接匹配所以在仿真环境里把逻辑调通换真机时只需要改驱动话题名和几个参数就行。3. 激光SLAM建图的完整链路从激光驱动到地图文件SLAM建图是整个导航系统的地基。地图不准后面AMCL定位神仙难救。所以这一章我会把选型、实操、以及容易忽略的坐标系检查都写明白。3.1 激光SLAM选型gmapping、cartographer、slam_toolbox怎么选激光SLAM算法选型是很多人会纠结的地方。我的经验是AGV运行场景基本是结构化环境墙壁规则、通道固定用gmapping就足够了。它依赖里程计计算量小在低配工控机上也能跑30Hz以上。Cartographer精度确实更高能处理长廊等退化场景但CPU占用大参数多调起来费时间。slam_toolbox的优势是支持建图后的地图修正和重定位适合二次开发。算法地图精度CPU占用对里程计依赖典型场景gmapping中等低高室内结构化环境AGV仓储cartographer高高中复杂环境、长廊、动态场景slam_toolbox高中中需要后期地图编辑的场景我给的结论是第一套方案用gmapping把流程跑通遇到长廊建图漂移再切cartographer。这跟我做项目的习惯一致——先用最简方案验证系统再针对具体痛点升级。启动gmapping的方式很简单前提是你的激光雷达驱动已经正常发布/scan话题。一个可以直接用的launch文件launch node nameslam_gmapping pkggmapping typeslam_gmapping param namebase_frame valuebase_footprint/ param nameodom_frame valueodom/ param namemap_frame valuemap/ param namemap_update_interval value5.0/ param namemaxUrange value6.0/ param namemaxRange value8.0/ param nameminimumScore value50/ param namelinearUpdate value0.1/ param nameangularUpdate value0.1/ remap fromscan toscan/ /node /launch注意参数里base_frame要跟你机器人的TF树里的base坐标系名称一致。如果你用的模型是base_link而不是base_footprint这里不对应的话gmapping会一直报“Couldnt get base frame”之类的错误地图完全建不起来。3.2 建图实操流程与命令建图过程分三步启动雷达驱动、启动SLAM算法、遥控小车扫描环境。以仿真环境为例# 终端1启动Gazebo仿真环境和AGV模型 roslaunch agv_gazebo agv_world.launch # 终端2启动gmapping roslaunch agv_slam gmapping.launch # 终端3启动键盘遥控 rosrun teleop_twist_keyboard teleop_twist_keyboard.py操作手法上有讲究不要只沿着墙根转一圈就完事。正确做法是先在场地中央转一圈让地图初始化然后沿着场地边界慢速走一圈再以S形路线扫内部遇到柱子之类的障碍物要绕一圈让它形成闭合轮廓。建图速度控制在0.3m/s以内转弯时尽量原地转别大幅画弧——弧线会让gmapping的扫描匹配误差累积导致地图重影。我建图时习惯打开Rviz实时看地图和激光点云叠合情况rosrun rviz rviz -d $(rospack find agv_slam)/rviz/slam.rviz如果激光点云和地图边缘出现明显错位说明里程计标定不准或者雷达安装位置描述有误先别急着继续扫停下来检查机器人底盘的/odom话题频率和TF树。3.3 保存地图与坐标系检查建图完成后保存地图rosrun map_server map_saver -f ~/agv_ws/src/agv_nav/maps/warehouse这会生成两个文件warehouse.yaml和warehouse.pgm。别忘了看一眼yaml里的resolution和origin参数这两个值在后续导航配置里会被map_server读取写错了地图加载后位置会偏。保存完地图务必做一件事检查TF树。用自带的tf工具rosrun tf view_frames evince frames.pdf正常情况TF树应该是一条完整的链map - odom - base_footprint - base_link - laser。很多奇怪问题——比如车定位漂移、导航路径偏斜——最后查下来都是因为laser坐标系到base_link的静态坐标变换写错了。雷达装在车体中心正上方10cm处那laser到base_link的xyz变换就应该是(0, 0, 0.10)yaw偏角差0.01弧度在10米外就是10厘米的位置误差足够让车撞上货架。所以建图之前静态坐标变换必须反复确认。4. 导航框架拆解move_base、AMCL与代价地图的分工逻辑建好图之后接下来就是把导航框架跑起来。很多新手觉得导航就是发一个目标点车自己会走。真实系统里一套标准导航链路涉及多个节点协同工作理解它们各自干什么排查问题才能有的放矢。4.1 导航系统里每个节点在干什么我把一条完整的导航数据流拆成五个角色map_server加载建好的地图持续发布/map话题。AMCL负责定位。它接收激光雷达数据、里程计数据和地图通过粒子滤波估计机器人在地图中的位姿发布/amcl_pose和map-odom的TF变换。move_base导航总指挥。它订阅目标点/move_base_simple/goal内部由全局规划器和局部规划器协作最终输出速度指令到/cmd_vel。costmap_2d代价地图生成器。它把地图数据和激光雷达实时数据融合成“障碍物栅格图”供规划器搜索路径时使用。RViz可视化调试工具相当于导航系统的“仪表盘”。很多人会问AMCL都定位了为什么还要维护map-odom这个变换原因是里程计会有累积误差AMCL每次用激光匹配修正一次位置这个修正量就体现在map-odom变换里。如果这个变换跳变剧烈就说明定位不稳定需要检查AMCL参数或者激光数据质量。启动这一整套的launch文件结构建议拆成两个amcl.launch和move_base.launch方便单独重启调试。4.2 代价地图参数与障碍物膨胀costmap是路径规划里非常核心但又容易被忽视的部分。它分全局代价地图/global_costmap和局部代价地图/local_costmap各有各的坐标系和更新频率。下面是我常用的一套costmap配置直接放到config/costmap_common.yaml里robot_base_frame: base_footprint update_frequency: 5.0 publish_frequency: 2.0 transform_tolerance: 0.5 static_map: false rolling_window: true width: 6.0 height: 6.0 resolution: 0.05 obstacle_range: 3.0 raytrace_range: 3.5 inflation_radius: 0.35 cost_scaling_factor: 3.0 observation_sources: laser laser: data_type: LaserScan topic: /scan marking: true clearing: true这里有两个参数直接影响避障效果我解释一下背后逻辑inflation_radius障碍物的“膨胀半径”。数值越大AGV离障碍物越远路径更安全但更绕。对于1米宽的AGV我给0.35米起步实际调试时再根据货架间距调整。cost_scaling_factor代价衰减速度。值越小代价衰减越慢也就是说靠近障碍物的区域代价更高。调到3.0是我的经验值既能保持一定通行距离又不会让狭窄通道完全不可通行。还有clearing: true这个配置很关键它让激光雷达的每个扫描点不仅把障碍物标记进地图还把障碍物后方的“空白区域”清除掉。代价地图默认会清除动态障碍物离开后的痕迹clearing就是干这个的。如果你发现小车在经过一个地方后路线上保留着“虚拟障碍物”导致后续路径绕路多半就是这个参数没配对。4.3 全局与局部路径规划器的选型move_base默认的全局规划器是navfn它实现的是Dijkstra算法也可以切换到global_planner插件使用A*算法。两者都能找出一条从起点到目标点的路径区别在于Dijkstra从起点出发向外扩展遍历整个地图直到找到终点一定能找最优路径但搜索范围大、耗时长。A*在Dijkstra基础上加入启发式函数相当于“朝着目标方向优先搜索”速度快很多适合仓储AGV这种需要频繁规划路径的场景。我现在的做法是在move_base参数里显式指定使用A*GlobalPlanner: use_grid_path: false use_quadratic: true use_dijkstra: false use_astar: true default_tolerance: 0.0局部规划器我选了DWA。它的原理其实很直白在机器人当前速度附近采样一组候选速度用运动学模型模拟一小段时间内的轨迹然后根据“离全局路径多远、是否撞障碍物、是否朝目标”这几个指标打分选分数最高的速度执行。DWA适合差速和全向底盘参数少调起来快。TEB时间弹性带的避障能力更强生成的轨迹更平滑但它会时刻优化时间最优性调不好容易出现抖动。如果你是阿克曼底盘的AGV建议用TEB差速底盘用DWA就够没必要一开始就上TEB给自己增加调参负担。5. 避障代码核心实现从激光数据到速度指令标题里说要附避障代码这一段就上干货。我先说明设计思路不自己重写一套路径规划而是做一个“安全控制器”串在move_base和底盘之间。也就是说move_base算出速度指令这个节点先检查激光数据确认前方安全再放行有碰撞风险就拦截、减速或转向。这样设计的好处是既不影响move_base的完整规划能力又能在传感器数据异常或规划器漏判时兜底工程上更稳。5.1 代码逻辑与数据结构这个节点的核心是维护两个数据源/move_base/cmd_velgeometry_msgs/Twistmove_base规划好之后发出来的目标速度。/scansensor_msgs/LaserScan激光雷达的实时扫描数据。收到目标速度后先看机器人前进方向附近有没有障碍物。具体做法是把激光点云按角度分成三个扇区前方-30°到30°用于判断是否能继续前进。左侧30°到90°用于左转避让时判断左侧空间。右侧-90°到-30°用于右转避让时判断右侧空间。每个扇区取最小距离作为“该方向的安全距离”。当前方距离小于安全阈值时就把move_base发给底盘的线速度压到0同时根据左右哪边空间更宽裕给一个小的转向角速度。这样既实现了避障也保持了继续绕障碍物走的能力而不是傻站在那。5.2 可运行的Python避障节点源码下面是我实机上跑过的版本完整可以放到scripts/obstacle_avoidance.py里直接运行#!/usr/bin/env python3 import rospy import math from sensor_msgs.msg import LaserScan from geometry_msgs.msg import Twist class ObstacleAvoidance: def __init__(self): rospy.init_node(obstacle_avoidance, anonymousTrue) self.cmd_pub rospy.Publisher(/cmd_vel, Twist, queue_size1) rospy.Subscriber(/move_base/cmd_vel, Twist, self.cmd_callback) rospy.Subscriber(/scan, LaserScan, self.scan_callback) self.latest_scan None self.safety_dist rospy.get_param(~safety_dist, 0.35) self.turn_speed rospy.get_param(~turn_speed, 0.5) self.emergency_stop_dist rospy.get_param(~emergency_stop_dist, 0.20) self.lateral_dist rospy.get_param(~lateral_dist, 0.30) self.rate rospy.Rate(20) rospy.loginfo(Obstacle avoidance node started.) def scan_callback(self, msg): self.latest_scan msg def get_sector_distance(self, angle_min, angle_max): if self.latest_scan is None: return float(inf) ranges self.latest_scan.ranges angle_min max(angle_min, self.latest_scan.angle_min) angle_max min(angle_max, self.latest_scan.angle_max) idx_min max(0, int((angle_min - self.latest_scan.angle_min) / self.latest_scan.angle_increment)) idx_max min(len(ranges) - 1, int((angle_max - self.latest_scan.angle_min) / self.latest_scan.angle_increment)) valid [r for r in ranges[idx_min:idx_max 1] if r 0.1] if not valid: return float(inf) return min(valid) def cmd_callback(self, msg): if self.latest_scan is None: self.cmd_pub.publish(msg) return front_dist self.get_sector_distance(-math.radians(30), math.radians(30)) left_dist self.get_sector_distance(math.radians(30), math.radians(90)) right_dist self.get_sector_distance(-math.radians(90), -math.radians(30)) if front_dist self.emergency_stop_dist: # 紧急停车线速度为0角速度为0 stop Twist() self.cmd_pub.publish(stop) rospy.logwarn_throttle(1.0, Emergency stop! front_dist%.2f, front_dist) return if front_dist self.safety_dist and msg.linear.x 0: # 前方太近需要减速并转向 avoid Twist() avoid.linear.x 0.0 if left_dist right_dist: avoid.angular.z self.turn_speed else: avoid.angular.z -self.turn_speed self.cmd_pub.publish(avoid) rospy.logwarn_throttle(1.0, Avoiding obstacle, front%.2f, front_dist) return # 安全透传move_base的指令 self.cmd_pub.publish(msg) def spin(self): r rospy.Rate(20) while not rospy.is_shutdown(): r.sleep() if __name__ __main__: try: node ObstacleAvoidance() node.spin() except rospy.ROSInterruptException: pass解释几个关键设计为什么用rospy.logwarn_throttle而不是直接rospy.logwarn因为避障触发时这个回调函数以20Hz触发直接打印会把终端刷爆throttle让它每秒最多打印一次既能看到日志又不影响实时性。为什么在cmd_callback里发布指令而不另开定时器因为避障节点本质是个“阀门”只有在move_base有速度指令进来时才需要响应没有目标时就不该有任何输出。扇区距离计算那里有个细节r 0.1这个过滤不能省。激光雷达在检测到过近物体或者没有回波时会用0或者inf填充直接参与min计算会得到错误结果。5.3 如何与move_base配合工作把这个节点接到系统里核心是话题重映射。默认情况下move_base发布到/cmd_vel我们改成让它发布到/move_base/cmd_vel然后由避障节点过滤后输出到真实的/cmd_velroslaunch move_base move_base.launch cmd_vel_topic:/move_base/cmd_vel rosrun agv_nav obstacle_avoidance.py如果你不想改move_base的启动参数也可以用rosrun topic_tools relay做话题转发但那样控制粒度不够避障节点就没法拦截了。我建议还是用重映射的方式。这套代码的调试技巧先在Gazebo仿真里跑故意把障碍物放在路径中间观察小车是否能停下并绕行。仿真里验证通过后真机上把safety_dist从0.5开始往下调逐步逼近AGV的最小通过能力。比如你的AGV底盘宽度0.6米货架通道宽度0.8米那safety_dist设0.15就够了设太大反而会让小车在窄通道里进退两难。6. 实测调参与避坑记录最后一章我把做AGV导航项目以来遇到的高频问题和调试经验汇总一下。这些问题在文档里往往看不到但实际项目中几乎每个都会碰到。6.1 激光雷达安装位置与TF坐标系激光雷达的安装位置对导航稳定性影响极大。我吃过一次大亏一台AGV的雷达装在车体正前方结果导航时一接近货架边缘AMCL定位就疯狂跳变。后来排查发现雷达视角朝前侧面货架进入视野的角度太小匹配特征不足粒子滤波就乱猜。正确做法是雷达尽量装在车体旋转中心的正上方这样才能保证雷达坐标系到base_link的变换里只有xyz平移没有额外的旋转耦合。装好之后用下面这条命令验证静态变换rosrun tf2_ros static_transform_publisher 0 0 0.10 0 0 0 base_link laseryaw角的偏差即使只有几度也会导致建图时墙壁倾斜、导航时路径偏移。真机调试时先在RViz里观察激光点云是否跟小车模型的外轮廓对齐这是最直观的检查方式。6.2 导航跑飞、原地打转、突然急停的三类常见问题我把实际调试中遇到的三类问题整理成了排查表遇到类似情况可以直接对照检查现象可能原因排查路径导航跑飞车冲到目标点之外AMCL初始位姿不准在RViz里用“2D Pose Estimate”重新给出初始位姿地图与激光点云重叠时出现错位odom模型参数错误检查/odom话题频率校准轮径和轮距到达目标点附近却疯狂原地打转局部costmap里残留障碍物检查clearing: true调大update_frequency距离障碍物还有一段距离就急停inflation_radius设得过大逐步缩小0.05观察变化导航过程中转速一顿一顿里程计和激光数据时间戳不同步检查各传感器消息的timestamp是否一致这里面定位问题占了绝大多数。AMCL的初始位姿如果给错了后面再怎么调参数都没用。我的习惯是启动导航后第一步先在RViz里手动给初始位姿观察到激光点云与地图边缘基本对齐后再发送导航目标点。别偷懒省这一步省掉的这三分钟可能换来半小时的烦恼。6.3 一份我常用的参数备份习惯调参是AGV导航开发里最耗时也最容易反复的部分。我一开始也是看到哪个参数不对劲就随手改结果反复改来改去根本不知道哪一次改动真正起了作用。后来我给自己定了一个“每调必备份”的规矩工作空间用git管理所有配置文件进版本库。每次调参只改动一个变量改完立刻记录效果和截图。参数文件命名带版本号和日期比如costmap_20250115_v1.yaml、costmap_20250115_v2.yaml。调好一批参数后在Rviz里保存一个Bookmarks位置方便回看。这套习惯帮了我大忙。有一次在客户现场调了大半天的参数晚上回去复盘就是靠git历史记录查到了关键改动是哪一行。最后再分享一个我自己的习惯每次拿到一台新的AGV我不会直接开始调导航参数而是先花半天时间把底盘的直线精度和旋转精度摸清楚。对着墙上贴一张纸让车跑1米用卷尺量让车原地转360度看偏差。底盘本身不准的话SLAM建图、AMCL定位、路径规划全是空中楼阁调起来事倍功半。把基础硬件的行为摸透后面软件层的调试才会顺。
返回列表