简介:这份资源面向ROS机器人开发者与学习室内自主导航的学生,聚焦雷达定位EKF与环境建图这一典型SLAM场景,帮助读者理解扩展卡尔曼滤波如何融合激光雷达与IMU数据,为ros小车提供高精度位姿估计,并配合gmapping、amcl完成建图与定位。压缩包共107个文件,约21.01MB,以cpp源码与h头文件为核心,涵盖ekf、ukf、navsat_transform等定位节点实现;test测试用例、srv服务定义、launch启动文件、yaml参数配置与bag数据包共同构成可复现的实验环境,另有rst文档、pdf说明及CMakeLists.txt、package.xml等构建文件,目录结构贴近真实ROS工程。目前已有4358人学习下载。读者可从中获得雷达数据预处理、坐标转换、EKF状态模型设计到地图保存与amcl定位的完整实现思路,并借助测试用例与录包数据排查定位漂移、建图错位等常见问题,适合具备一定ROS基础、希望深入掌握小车建图定位的中高级开发者参考。
1. 从一堆点云到一条能走的路:robot_localization 在 ROS 小车上到底干了什么
很多人第一次把雷达装到 ROS 小车上,打开 RViz 看到一圈圈点云扫出来,心里觉得稳了,结果一按键盘让车动起来,车要么原地画龙,要么走两米就偏出半米。问题往往不在雷达,也不在底盘,而在“定位”这一环——雷达给的是相对观测,轮速计给的是累计漂移,IMU 给的是高频但会飘的姿态,这三路数据各说各话,没有一个东西把它们揉成一个可信的位姿。robot_localization 就是干这件事的:它是一个 ROS 下的状态估计节点包,用扩展卡尔曼滤波(EKF)或无迹卡尔曼滤波(UKF)把轮速、IMU、雷达定位(比如 AMCL 或激光里程计)融合成一条连续的odom → base_link和map → odom变换。环境建图则是它的上游输入,雷达定位是它的观测来源之一。这篇笔记面向的是手里有 ROS 小车、想跑通建图与定位闭环的工程师,从环境建图怎么出图、EKF 参数怎么配、到融合后怎么验证,一步步拆开讲。
2. 环境建图先出图:雷达建图的最小闭环与参数取舍
2.1 为什么建图质量直接决定 EKF 融合的上限
robot_localization 本身不建图,它吃的是别人建好的地图和定位结果。如果你用 gmapping 或 cartographer 建出来的图本身就是歪的、重影的,后面 EKF 再怎么调都是白搭。常见做法是先用激光雷达跑一遍 gmapping 或 cartographer,把/map和map → odom变换发出来,再用 AMCL 在已知地图上做粒子滤波定位,输出/amcl_pose作为 EKF 的一个观测输入。这里有个关键点:EKF 融合的是“位姿观测”,不是原始点云。所以建图阶段要保证雷达安装水平、base_link → laser的外参准确,否则地图和实际环境存在系统性旋转偏差,AMCL 定位会持续偏。
建图时我一般会先确认三件事:雷达话题名对不对、tf树是否完整、轮速计标定是否靠谱。轮速计标定不准,建图时转弯会“糊墙”,直墙变弧墙。标定方法很简单:让车直线走 2 米,看odom里走了多少,算比例系数填回底盘参数。这个系数不调,后面 EKF 融合轮速和雷达时会互相打架。
2.2 用 gmapping 跑通第一张栅格地图的命令与配置
下面是一个最小可用的 gmapping 启动文件片段,假设雷达话题是/scan,轮速里程计话题是/odom,tf树已经由底盘驱动发好。
<!-- gmapping_min.launch --> <launch> <!-- 雷达驱动,实际话题名按你的雷达改 --> <node name="rplidar" pkg="rplidar_ros" type="rplidarNode" output="screen"> <param name="serial_port" value="/dev/ttyUSB0"/> <param name="frame_id" value="laser"/> </node> <!-- gmapping 建图 --> <node name="slam_gmapping" pkg="gmapping" type="slam_gmapping" output="screen"> <param name="base_frame" value="base_link"/> <param name="odom_frame" value="odom"/> <param name="map_frame" value="map"/> <param name="scan_topic" value="/scan"/> <!-- 粒子数,小车室内 30 够用,太大吃 CPU --> <param name="particles" value="30"/> <!-- 更新间隔,雷达 10Hz 就设 0.05 --> <param name="map_update_interval" value="0.05"/> <!-- 栅格分辨率,5cm 是室内小车常用值 --> <param name="delta" value="0.05"/> <!-- 激光测距最大范围,按雷达实际改 --> <param name="maxRange" value="12.0"/> <param name="minRange" value="0.15"/> </node> <!-- RViz 看建图效果 --> <node name="rviz" pkg="rviz" type="rviz" args="-d $(find your_pkg)/rviz/mapping.rviz"/> </launch>逻辑说明:gmapping 订阅/scan和tf,内部用粒子滤波估计机器人轨迹并增量式建图。particles是粒子数,粒子越多越准但计算量越大,室内小车 30 到 50 足够;delta是地图分辨率,0.05 表示每个栅格 5cm,太小地图文件巨大,太大障碍物边缘粗糙;map_update_interval控制地图发布频率,设太小会频繁刷新导致 RViz 卡顿。建图时让车慢速走一圈,速度控制在 0.2m/s 以内,转弯要缓,急转弯会导致雷达帧匹配失败,地图出现重影。
2.3 建图完成后保存地图与检查 tf 树
建完图先别急着关,用map_server把地图存下来:
# 保存地图到当前目录,生成 map.pgm 和 map.yaml rosrun map_server map_saver -f ./map保存后检查map.yaml里的resolution、origin和negate是否正确。origin是地图左下角在map坐标系下的位姿,如果建图时起点不在原点,这个值会偏移,后续 AMCL 定位会整体平移。检查tf树用:
rosrun tf view_frames # 生成 frames.pdf,确认 map → odom → base_link → laser 链路完整常见问题是map → odom由 gmapping 发布,建图结束后 gmapping 关闭,这个变换就断了。所以建图完成后要切换到 AMCL 来发布map → odom,否则 EKF 拿不到地图坐标系下的位姿。
3. robot_localization 双 EKF 配置:把轮速、IMU、雷达定位揉成一条位姿
3.1 为什么用两个 EKF 而不是一个
robot_localization 的经典用法是跑两个ekf_localization_node:一个做odom → base_link的局部融合,输入轮速和 IMU,输出连续平滑的局部里程计;另一个做map → odom的全局融合,输入局部融合结果和 AMCL 定位,输出地图坐标系下的位姿。这样分层的好处是:局部 EKF 不受地图跳变影响,全局 EKF 可以处理 AMCL 的离散修正。如果只用一个 EKF 同时吃轮速和 AMCL,AMCL 偶尔跳一下,整个位姿会抖,小车会突然“抽一下”。
两个 EKF 的world_frame参数不同:局部 EKF 的world_frame设为odom,全局 EKF 设为map。这是最容易配错的地方,配反了tf树会打架,RViz 里车会瞬移。
3.2 局部 EKF 配置文件与参数逐项说明
下面是一个局部 EKF 的 YAML 配置,融合轮速里程计和 IMU。
# ekf_local.yaml ekf_local: frequency: 30.0 # 滤波频率,要高于所有输入话题频率 sensor_timeout: 0.1 # 传感器超时,超过就丢弃该输入 two_d_mode: true # 小车在平面运动,强制二维,减少漂移 publish_tf: true # 发布 odom → base_link map_frame: map odom_frame: odom base_link_frame: base_link world_frame: odom # 局部融合,world 是 odom # 轮速里程计输入 odom0: /wheel_odom odom0_config: [false, false, false, false, false, false, true, true, false, false, false, false, false, false, false] odom0_differential: false odom0_relative: false # IMU 输入 imu0: /imu/data imu0_config: [false, false, false, false, false, false, false, false, false, false, false, true, false, false, false] imu0_differential: false imu0_relative: false imu0_remove_gravitational_acceleration: true逻辑说明:odom0_config是一个 15 位布尔数组,对应[x, y, z, roll, pitch, yaw, vx, vy, vz, vroll, vpitch, vyaw, ax, ay, az]。上面配置表示只取轮速的vx和vyaw,不取绝对位置,因为轮速的绝对位置会漂移,取速度更稳。IMU 只取vyaw(角速度),不取绝对姿态,因为 IMU 的 yaw 会随时间漂移,但短时角速度很准。two_d_mode: true强制忽略 z、roll、pitch,小车在平面跑必须开,否则 IMU 的俯仰噪声会污染位姿。imu0_remove_gravitational_acceleration: true去掉重力分量,不然加速度计数据不能用。
3.3 全局 EKF 配置与 AMCL 接入
全局 EKF 吃局部 EKF 的输出和 AMCL 的位姿。
# ekf_global.yaml ekf_global: frequency: 30.0 sensor_timeout: 0.1 two_d_mode: true publish_tf: true map_frame: map odom_frame: odom base_link_frame: base_link world_frame: map # 全局融合,world 是 map # 局部 EKF 输出作为里程计输入 odom0: /odometry/filtered odom0_config: [true, true, false, false, false, false, true, true, false, false, false, true, false, false, false] odom0_differential: false odom0_relative: false # AMCL 定位输入 pose0: /amcl_pose pose0_config: [true, true, false, false, false, true, false, false, false, false, false, false, false, false, false] pose0_differential: false pose0_relative: false逻辑说明:全局 EKF 的odom0是局部 EKF 发布的/odometry/filtered,取x, y, vx, vy, vyaw;pose0是 AMCL 的/amcl_pose,取x, y, yaw。注意 AMCL 的协方差有时候给得不准,如果发现全局 EKF 被 AMCL 拽得太狠,可以在pose0_config里只取x, y,yaw 靠局部 EKF 的vyaw积分,这样更平滑。world_frame: map让全局 EKF 发布map → odom变换,正好补上 gmapping 关闭后的空缺。
3.4 启动文件与 tf 树验证
把两个 EKF 和 AMCL 一起启动:
<!-- localization.launch --> <launch> <node name="map_server" pkg="map_server" type="map_server" args="$(find your_pkg)/maps/map.yaml"/> <node name="amcl" pkg="amcl" type="amcl" output="screen"> <param name="odom_frame_id" value="odom"/> <param name="base_frame_id" value="base_link"/> <param name="global_frame_id" value="map"/> <param name="scan_topic" value="/scan"/> <!-- 粒子数,定位稳定后可以降到 200 省 CPU --> <param name="min_particles" value="200"/> <param name="max_particles" value="2000"/> </node> <node name="ekf_local" pkg="robot_localization" type="ekf_localization_node" output="screen"> <rosparam command="load" file="$(find your_pkg)/config/ekf_local.yaml"/> </node> <node name="ekf_global" pkg="robot_localization" type="ekf_localization_node" output="screen"> <rosparam command="load" file="$(find your_pkg)/config/ekf_global.yaml"/> </node> </launch>启动后验证tf树:
rosrun tf view_frames # 确认 map → odom 由 ekf_global 发布,odom → base_link 由 ekf_local 发布如果发现map → odom有两个节点在发(比如 AMCL 和 ekf_global 同时发),RViz 里车会来回跳。解决办法是把 AMCL 的publish_tf设为false,让它只发/amcl_pose,tf交给全局 EKF 统一发。
4. 融合效果排查:从 RViz 现象反推参数问题
4.1 车在 RViz 里画龙或瞬移
现象:小车直走时 RViz 里的位姿左右摆动,或者突然跳一段。原因通常是局部 EKF 的odom0_config取了绝对位置x, y,而轮速里程计的绝对位置本身有累积误差,和 IMU 的角速度积分打架。解决:轮速只取vx, vyaw,不取x, y。另一个原因是two_d_mode没开,IMU 的 roll/pitch 噪声混进来。检查ekf_local.yaml里two_d_mode: true是否生效。
4.2 AMCL 定位跳变导致全局位姿抖动
现象:小车走到走廊尽头或长直走廊中间,AMCL 粒子发散,/amcl_pose突然跳几十厘米,全局 EKF 跟着跳。原因:长走廊或对称环境激光匹配歧义,AMCL 粒子退化。解决:在pose0_config里把 AMCL 的yaw关掉,只取x, y,让全局 EKF 的 yaw 靠局部 EKF 的vyaw积分维持;同时把 AMCL 的min_particles调大,增加粒子多样性。如果环境特别对称,可以考虑在走廊贴一些反光标记或改变雷达安装高度,让扫描特征更丰富。
4.3 EKF 输出频率低于输入频率
现象:/odometry/filtered话题频率只有 10Hz,但轮速和 IMU 都是 50Hz。原因:frequency参数设太低,或者某个传感器sensor_timeout设太小导致频繁丢弃。解决:frequency设为最高输入频率的 1.5 倍左右,比如 IMU 50Hz,frequency设 75;sensor_timeout设为输入周期的 3 到 5 倍,比如 IMU 20ms 周期,超时设 0.1s。
4.4 tf 树出现两个父节点
现象:rosrun tf view_frames报错说base_link有两个父节点。原因:底盘驱动发了odom → base_link,局部 EKF 也发odom → base_link,两者冲突。解决:底盘驱动只发轮速话题/wheel_odom,不发tf;tf统一由局部 EKF 发布。如果底盘驱动固件里写死了发tf,在 launch 里把它的publish_tf参数关掉,或者用tf_remove节点过滤。
4.5 建图时地图重影,墙变成两层
现象:gmapping 建出的地图里,同一面墙出现两条平行线。原因:轮速计标定不准,转弯时odom的角速度和实际不符,雷达帧匹配时把墙“抹”开了。解决:重新标定轮速计,让车原地转 360 度,看odom里 yaw 变化是否接近 360 度,差多少就调角速度比例系数。另外检查雷达安装是否松动,建图过程中雷达歪了也会重影。
5. 让融合更稳的几个进阶技巧与验证习惯
5.1 用协方差可视化判断哪路传感器在主导
robot_localization 发布的/odometry/filtered里带协方差,可以在 RViz 里加PoseWithCovariance显示,看椭圆大小。如果椭圆在某方向上特别大,说明那个方向观测不足。比如 yaw 协方差一直很大,说明 IMU 的vyaw没被正确使用,检查imu0_config里vyaw位是否设为true。我一般会在调试阶段把协方差显示打开,跑一圈看哪个方向椭圆鼓起来,针对性调那一路的权重。
5.2 用 rosbag 回放做参数对比
调 EKF 参数最忌讳每次改完都实车跑。正确做法是录一段包含/wheel_odom、/imu/data、/scan、/amcl_pose的 bag,然后离线回放调参:
# 录包 rosbag record -O localization_test.bag /wheel_odom /imu/data /scan /amcl_pose /tf /tf_static # 回放并启动 EKF rosbag play localization_test.bag --clock roslaunch your_pkg localization.launch回放时用--clock让 ROS 时间跟着 bag 走,EKF 的frequency和sensor_timeout才能正确工作。每次改完 YAML 重新启动 EKF,对比/odometry/filtered的轨迹和 AMCL 的轨迹,看哪个更贴合实际。我习惯把两次回放的轨迹同时画在 RViz 里,一眼就能看出参数改好还是改坏。
5.3 关键参数速查表
| 参数 | 作用 | 常用值 | 调大后果 | 调小后果 |
|---|---|---|---|---|
| frequency | 滤波频率 | 30~75 | CPU 占用高 | 输出滞后 |
| sensor_timeout | 传感器超时 | 0.1~0.5 | 响应慢 | 频繁丢数据 |
| two_d_mode | 强制二维 | true | 忽略 z/roll/pitch | 平面车会飘 |
| odom0_config | 轮速取哪些量 | 只取 vx,vyaw | 取绝对位置会漂 | 取太少观测不足 |
| imu0_config | IMU 取哪些量 | 只取 vyaw | 取绝对姿态会漂 | 取太少 yaw 不稳 |
| pose0_config | AMCL 取哪些量 | 只取 x,y | 取 yaw 会跳 | 取太少修正弱 |
这张表是我调参时贴在显示器边上的,每次改参数先看一眼,避免把two_d_mode关了还纳闷车为什么上下跳。
5.4 一个我踩过的坑:AMCL 和 EKF 抢着发 tf
最早跑通建图后,我直接启动 AMCL 和 EKF,RViz 里车走两步就瞬移一次。查了半天以为是 EKF 参数问题,后来rosrun tf view_frames才发现map → odom有两个发布者:AMCL 和全局 EKF。AMCL 默认publish_tf是true,它发的map → odom和 EKF 发的打架,RViz 随机选一个,车就跳。把 AMCL 的publish_tf设为false后立刻稳了。这个坑我后来每次配新机器都会先检查一遍,成了肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取