简介:面向ROS学习者和移动机器人导航开发者,这是一份基于ROS平台融合IMU与GPS实现从指定起点到指定终点运动仿真的精简源码包,解决将惯性测量单元和卫星定位数据转化为可执行导航指令的工程落地问题。资源共5个文件,包含2个C++源文件、1个package.xml配置、1个CMakeLists.txt构建脚本及1个README说明文档,压缩包仅9KB,结构清晰,便于快速阅读与二次开发。源码附超详细中文注释,完整覆盖IMU和GPS数据的读取解析、基于卡尔曼滤波的传感器融合、ROS节点与话题通信机制、导航参数配置及启动流程等关键知识点,可在Gazebo等仿真环境中直接验证从起点到终点的定向移动逻辑。已有241人学习浏览,适合正在学习ROS导航堆栈、传感器融合算法或准备进行机器人自主定位实验的开发者参考,能帮助读者节省环境搭建与排错时间,快速上手基于ROS的实际导航工程。

1. 起点到终点的仿真,先解决“车在哪”再解决“往哪走”

拿到“基于ROS平台利用IMU和GPS从指定起点到指定终点仿真源码+超详细注释.zip”这个标题,我第一反应是:这工程名字看着像教学包,但它解决的是移动机器人导航里最典型的一套组合问题——不让机器人只靠GPS瞎跑,得先回答两个问题:它在哪(定位),它怎么过去(路径规划)。IMU负责短时间内的姿态和加速度推算,GPS负责绝对位置修正,两者一融合,就能弥补单传感器的短板。你如果刚看完ROS基础教程,想搞清楚“导航栈到底怎么跑起来”,或者正在写毕业设计、竞赛方案,需要一套带仿真环境和注释的模板,这类项目是最快的上手路径。很多新手上来就摆一个move_base,然后发现机器人原地转圈,原因其实出在传感器数据没对齐、坐标系没配好,而不是规划器坏了。这篇文我会顺着这套方案讲:传感器怎么建模、数据怎么融合、起终点怎么指定、到达怎么判定。

2. 仿真环境里建立IMU和GPS的传感器模型,先让数据像回事

2.1 用URDF和Gazebo插件把IMU与GPS装到底盘上

做仿真导航,第一件事不是写导航算法,而是让底盘上出现“会发数据”的传感器。常见做法是在URDF文件里声明Gazebo插件,让仿真器替你生成传感器消息。IMU插件挂在底盘连杆上,GPS插件挂在外壳或者底座上,两者的坐标朝向必须和真实安装方向一致,尤其是IMU的x、y、z轴,如果装反了,后续EKF融合出来的姿态全是错的。

一个典型的IMU Gazebo传感器插件配置如下:

<gazebo reference="base_link">
  <sensor name="imu_sensor" type="imu">
    <always_on>true</always_on>
    <update_rate>100</update_rate>
    <visualize>false</visualize>
    <imu>
      <angular_velocity>
        <x><noise type="gaussian"><mean>0</mean><stddev>0.0002</stddev></noise></x>
        <y><noise type="gaussian"><mean>0</mean><stddev>0.0002</stddev></noise></y>
        <z><noise type="gaussian"><mean>0</mean><stddev>0.0005</stddev></noise></z>
      </angular_velocity>
      <linear_acceleration>
        <x><noise type="gaussian"><mean>0</mean><stddev>0.017</stddev></noise></x>
        <y><noise type="gaussian"><mean>0</mean><stddev>0.017</stddev></noise></y>
        <z><noise type="gaussian"><mean>0</mean><stddev>0.017</stddev></noise></z>
      </linear_acceleration>
    </imu>
  </sensor>
</gazebo>

这段配置的核心是把噪声写清楚:角速度的噪声标准差设到 0.0002 到 0.0005 ,加速度噪声设到 0.017 左右,单位分别是 rad/s 和 m/s²。这个数量级接近消费级IMU的表现,如果直接设成零噪声,后面调试EKF参数时你根本分不清是融合的问题还是传感器的问题。

GPS插件配置略不一样,因为Gazebo的GPS插件最终输出的是经纬度和海拔。为了在Rviz里看到连续的位置变化,通常还会配合一个 gps 传感器插件,把仿真世界坐标折算成经纬度增量。注意必须设置 update_rate ,因为GPS的真实更新率一般在5到10Hz,IMU在100到200Hz,两者频率差会导致融合时数据到达顺序不均匀。

2.1.1 IMU重力对齐和坐标系方向的处理

Gazebo里IMU默认输出的是“重力在传感器坐标系下的投影”,换句话说,静止放平的时候,z轴的加速度读数接近9.8。这带来一个关键问题:你的IMU在URDF里的安装方向,决定了重力向量落在哪个轴上。如果IMU被旋转了90度,重力就落在了x轴上,EKF里的状态预测会完全乱掉。所以写完URDF后先启动仿真,用 rostopic echo /imu/data 静态看一眼三个轴的加速度值,这是验证“IMU重力对齐”最直接的手段——静止时谁接近9.8,谁才是垂直轴。

网上搜“IMU重力对齐”常能看到两种诉求:一个是上述的URDF安装方向对齐,另一个是EKF启动时刻的重力估计。robot_localization节点在启动时会根据IMU的线加速度自己估计重力方向,一般不需要你额外传重力向量,但前提是IMU数据里没有混入运动加速度。仿真里这个问题通常不严重,只要你没把底盘模型放到斜坡上就行。

2.2 GPS误差模型:翻转补丁和经纬度噪声

GPS的误差和IMU完全不同,它是慢变的、有漂移的,静态场景下会围绕真实点缓慢抖动。Gazebo的GPS插件支持设置水平位置噪声,常见的做法是给经纬度加高斯噪声,标准差在 0.5 米到 1.0 米之间,这样比较接近普通单频GPS模块的民用精度。如果你在仿真里还想要有“低速漂移”效果,可以对噪声加上一个随机游走项,不过这对学习型项目不是必选项。

仿真过程中常遇到一个很坑的现象:车不动,但GPS纬度或经度偶尔跳变到很离谱的值,这在真实GPS里叫“翻转”,对应网上常说的“gps翻转补丁”。在仿真里复现这个问题不太必要,但你的融合节点必须对异常值做保护。最简单的方法是用 robot_localization 自带的手动限制机制,但更推荐在数据进入EKF之前,写一个小的 std::abs(新值 - 上一帧值) > 阈值 就丢弃的滤波器,这个阈值通常设成0.0005度(约50米),超过就直接丢,防止定位被单点噪声打飞。我一般会把这段代码写成一个ROS节点,订阅 /fix ,发布 /fix/filtered ,逻辑就几十行,但能省掉后面所有的排错时间。

3. 用EKF融合IMU和GPS的位姿,参数设置决定融合质量

3.1 选robot_localization还是自己写融合

IMU和GPS的融合,行业中几乎不会自己从零写卡尔曼滤波,直接用 robot_localization 包里的 ekf_localization_node 是主流做法。这个包在ROS1里已经非常成熟,ROS2里也有对应移植,处理的是“传感器数据进、位姿估计出”这件事。至于为什么不用紧耦合的RTK+IMU组合导航方案,是因为仿真环境里拿到的GPS是Gazebo生成的,不是真实卫星信号,做L1载波相位差分没有意义。EKF这种松耦合方式在仿真里已经能跑到不错的精度,而且参数直观、可调项少。

启动EKF前的关键一步是把GPS的经纬度转成局部坐标。Gazebo的world坐标系本来就是米制的,GPS插件通常能给出 origin 对应的UTM坐标,你如果不想引入地理投影库,可以直接把第一帧GPS点当作原点,之后每个GPS点都减去这个原点,得到相对偏移。下面这个Python节点做的事情就是订阅 /fix ,输出 /fix_rel :

#!/usr/bin/env python3
import rospy
from sensor_msgs.msg import NavSatFix
from geometry_msgs.msg import PointStamped

origin = None

def fix_cb(msg):
    global origin
    if origin is None:
        origin = (msg.latitude, msg.longitude)
        return
    # 仅作演示:在赤道附近,1度纬度约111320米,经度需乘cos(lat)
    lat_offset = (msg.latitude - origin[0]) * 111320.0
    lon_offset = (msg.longitude - origin[1]) * 111320.0 * \
                 __import__('math').cos(msg.latitude / 180.0 * 3.14159)
    out = PointStamped()
    out.header = msg.header
    out.point.x = lon_offset
    out.point.y = lat_offset
    out.point.z = 0.0
    pub.publish(out)

rospy.init_node('fix_to_xy')
pub = rospy.Publisher('/fix_rel', PointStamped, queue_size=10)
rospy.Subscriber('/fix', NavSatFix, fix_cb)
rospy.spin()

这段代码用到了“在赤道附近,1度纬度约111320米”的近似换算,仿真场景范围不大时完全够用。如果你的仿真场景处在高纬度地区,别忘给经度方向乘上 cos(latitude) ,否则经度偏移会被放大,出现“车沿着纬线走但融合结果在斜着飞”的怪异情况。注意这里用的是 PointStamped ,后续喂给EKF时需要转一次类型。

3.2 EKF节点的核心参数:哪些传感器字段参与融合

配置 ekf_localization_node 时,最容易踩坑的不是传感器噪声矩阵,而是“哪些变量需要融合、哪些不融合”。IMU提供的是姿态(roll/pitch/yaw)、角速度和线加速度,GPS提供的是x、y和yaw(如果航向有效)。有些同学把IMU的z轴位置也设成 true,仿真结果出来以后,定位点会顺着重力方向自己飘,这就是典型的量测与预测不匹配。

frequency: 50
sensor_timeout: 0.1
two_d_mode: true
publish_tf: true
map_frame: map
odom_frame: odom
base_link_frame: base_link
world_frame: odom

imu0: /imu/data
imu0_config: [false, false, false,
              false, false, true,
              false, false, false,
              false, false, true,
              true, false, false]
imu0_differential: false
imu0_remove_gravitational_acceleration: true

gps0: /fix_rel_fused
gps0_config: [true,  true,  false,
              false, false, false,
              false, false, false,
              true,  false, false,
              false, false, false]

这份YAML参数表里, imu0_config 有15个布尔值,分别对应 x, y, z, roll, pitch, yaw, vx, vy, vz, VROLL, VPITCH, VYAW, ax, ay, az 。这里IMU只开了yaw角、yaw角速度、x和y的线加速度,也就是IMU负责短期的姿态和加速度趋势,位置全部交给GPS去修正。 gps0_config 开了x、y位置和VYAW(航向角速度),这表示GPS除了给绝对位置,还提供一个全局的航向约束来抑制IMU的偏航漂移。

网上讨论“基于IMU的位姿解算 yaw 仍会慢漂移”的问题时,很多人第一反应是调高IMU的权重,但真正有效的是确保GPS的VYAW有数据来源。如果你的GPS模块不输出航向,那就不要开 VYAW ,否则EKF会拿一个恒定的零值去拉正IMU的yaw,最后导致机器人起步时航向被错误校正,车头方向肉眼可见地偏移。

3.3 fusion的输入输出:从传感器原话题到里程计话题的流转

EKF节点的输出是一个 nav_msgs/Odometry 消息,发布在 /odometry/filtered ,同时它还会维护 odom 到 base_link 的坐标变换。你的导航栈看到的“定位结果”就是这个里程计话题,而不是Gazebo里真正的 /base_pose_ground_truth 。这一步在调试时很关键——很多人直接在Rviz里加载了 RobotModel ,又加载了 /odometry/filtered 的路径显示,结果发现两个图标不重合,于是误以为融合没生效,其实是因为导航栈的 base_link 坐标系由 /odometry/filtered 的TF驱动,而Gazebo的 ground_truth 是独立坐标系。验证融合效果的正确方式是对比 EKF输出的位置 和 GroundTruth位置 这两条轨迹的偏差,而不是要求它们完全重叠在同一个TF下。

如果你发现融合后的位置输出有延迟,检查 sensor_timeout 这个参数,默认0.1秒意味着如果某个传感器数据停了100ms以上,EKF就会发出警告。在仿真里,尤其当你用 rosbag play 加速回放数据时,这个值要适当调大,否则背景噪声会填满终端。

4. 指定起点到指定终点的导航实现:从坐标到路径再到跟踪

4.1 起终点如何指定:Rviz里的2D Nav Goal还是纯代码

标题里“从指定起点到指定终点”有两种理解方式。一种是交互式的——在Rviz里手动设置起点和终点;另一种是纯代码式的——在地图上把起点和终点写成坐标,然后启动仿真自动跑完。如果你拿到的是带超详细注释的仿真源码,大概率是后者,因为竞赛和课程设计需要“一键跑通”的效果。

纯代码方式的核心是发布一个 move_base_msgs/MoveBaseActionGoal 消息。你不需要自己实现路径查找,move_base会帮你做全局规划,你的任务只是把目标坐标转换到 map 坐标系下并发送。默认的地图坐标系原点通常在Gazebo世界的某个角落,你可以用 rostopic echo /initialpose 来确定机器人起始位置,再用 rostopic echo /move_base/goal 检查终点坐标。

下面这段Python代码演示了如何发送起点到终点的目标:

#!/usr/bin/env python3
import rospy
import actionlib
from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal

rospy.init_node('send_goal')
client = actionlib.SimpleActionClient('move_base', MoveBaseAction)
client.wait_for_server()

goal = MoveBaseGoal()
goal.target_pose.header.frame_id = "map"
goal.target_pose.header.stamp = rospy.Time.now()
goal.target_pose.pose.position.x = 3.0   # 终点x坐标
goal.target_pose.pose.position.y = 2.5   # 终点y坐标
goal.target_pose.pose.orientation.w = 1.0  # 朝向正x方向

client.send_goal(goal)
client.wait_for_result()
print("到达终点" if client.get_result() else "导航失败")

这里最终发送的是 MoveBaseGoal ,其中 frame_id 必须设为 map ,如果设成了 odom ,导航目标会随着里程计漂移。 orientation.w 为1.0表示目标方向与map坐标系x轴一致,如果你的终点方向是y轴正方向,可以把 orientation.x = 0 , orientation.y = 0 , orientation.z = 1.0 。注意 wait_for_result() 是阻塞的,真实项目中应该用状态机轮询 get_state() ,因为阻塞调用虽然简单,但是会让机器人在中途无法响应急停指令。

4.2 move_base的全局规划器和局部规划器参数

move_base内部由两个规划器组成:global planner和local planner。前者在地图上生成从起点到终点的全局路径,后者负责实时避障并输出速度指令。常见的配置组合是 NavfnROS 做global planner, DWA 或 TEB 做local planner。TEB的优势是能生成符合机器人运动学约束的平滑轨迹,缺点是参数多、调参慢,DWA则简单直接。在这个仿真项目里,我一般建议用DWA,因为它只需要调三个关键量:最大线速度、最大角速度、加速度限制。

DWA的核心参数在 dwa_local_planner_params.yaml 中:

DWAPlannerROS:
  max_vel_x: 0.5
  min_vel_x: 0.0
  max_vel_y: 0.0
  max_vel_trans: 0.5
  min_vel_trans: 0.1
  max_vel_theta: 1.0
  min_vel_theta: 0.3
  acc_lim_x: 2.5
  acc_lim_theta: 3.2
  xy_goal_tolerance: 0.2
  yaw_goal_tolerance: 0.1
  sim_time: 2.0
  vx_samples: 20
  vtheta_samples: 40

max_vel_x 设为0.5m/s,是室内差速底盘常见的速度上限; xy_goal_tolerance 是最终到达终点时允许的半径误差,设成0.2米。如果你把 xy_goal_tolerance 设成0.05,小车会在终点附近来回震荡,因为DWA在接近目标时速度最低只有 min_vel_trans: 0.1 ,这个速度下走过的每一步都是0.1米,无法精确停在误差半径5厘米的圈里。所以终点判定微调优先调这个参数,而不是去改控制器。 sim_time 是DWA向前模拟轨迹的时间窗,设2秒可以让轨迹预判长一些,但不要超过3秒,否则在狭窄走廊里会产生穿越墙体的预判轨迹。

4.3 全局路径的代价地图和静态层

全局规划器依赖代价地图的代价数据,map_server加载一张静态地图后,costmap_2d会把障碍物膨胀。仿真里一个比较隐蔽的问题是:地图的 resolution 和实际环境不匹配。比如你的地图分辨率是0.05米/像素,但Gazebo里的碰撞体尺寸比地图栅格小,move_base规划的路径就会“贴墙走”,甚至把机器人看成穿过墙。最简单的手工修正方法是在 costmap_common_params.yaml 里把 inflation_radius 设到0.5米,让膨胀层把墙边区域的代价值抬高,这样路径自动往走廊中央收。

启动move_base的launch文件通常包括这几项:地图服务器节点、move_base节点、静态层/障碍物层/膨胀层配置。有一个非常影响起终点设置的问题是 base_local_planner 在全局路径和局部路径冲突时的行为,当你的起点被标记为代价过高时,move_base会报 "Robot is outside of the global costmap!" 错误,这通常是因为机器人的初始位姿没有正确设置到地图上。

5. 验证与调试技巧:确认到达终点不是“看起来到了”

5.1 记录轨迹并计算真实偏差

一条从起点到终点的仿真跑通,最直观的验证方式是画轨迹。用 rosbag record /odometry/filtered /base_pose_ground_truth /move_base/result 录一包数据,然后写一个离线脚本,重复采样对比融合位置的终点和真值终点的距离差。这里有个容易漏掉的细节: move_base/result 里的状态是 SUCCEEDED 不代表机器人一定在终点附近,因为 xy_goal_tolerance 就是终点判定阈值,系统只能保证位置偏差小于这个值,你要验证的正是这个“小于”到底是多少。

5.2 用IMU的零速检测判断车是否停稳

到达终点后,机器人是否真的停下来,可以通过IMU角速度的模来判断。正常静止时,IMU角速度在零附近小幅抖动,如果均值超过0.02 rad/s,说明底盘还在蠕动。配合 rostopic echo /imu/data 观察连续10帧数据,比看里程计更直接。仿真里出现“到了不停”的常见原因是控制器还在响应微小的 odom 位置波动,调低 xy_goal_tolerance 和 yaw_goal_tolerance 能缩短这个微调过程,但过小又会振荡,需要在代码注释里标明这个权衡。

5.3 GPS翻转补丁的位子应当写在数据入口

前面提过GPS翻转问题,如果你不想在融合前单独为GPS写滤波器,也可以在 ekf_localization_node 前面加一个 republish 节点,利用它自带的 mahalanobis_distance_threshold 参数按统计距离剔除异常值。在YAML里加到 gps0_config 对应的位置并不生效,正确写法是设置 gps0_mahalanobis_distance_threshold: 1.0 。这个值表示超过1个马氏距离的观测被认为是离群点并被丢弃。仿真中GPS正常情况下不太会触发这条规则,但写进配置里等于给整个起终点导航上了一道保险,做项目时我会顺手把 base_local_planner 里靠近目标时对GPS的依赖关到最小,因为最后10厘米的定位精度应交给里程计和IMU净推,而不是指望GPS抖动收敛到厘米级。

至此,定位、融合、规划、验证这套链路都能在一个终端窗口里复现出来,剩下的只是在代码注释里把每个参数的可调范围标清楚,下一次换场景时就只剩改坐标和调阈值这两件事了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

聚焦RDK与具身智能,汇集机器人开发教程、开源案例和技术交流

更多推荐