BACK_TO_BASE
Engineering Notebook // Build Log
/
18:05:26
/
NOTEBOOK_ENTRY

GPS 定位导航使用指南

目录 第一部分:基础知识——先搞懂 GPS 是什么 1. GNSS 卫星定位基础 1 gnss 卫星定位基础 2. 坐标系统与大地测量基础 2 坐标系统与大地测量基础 3. NMEA 协议——GPS 模块的"语言" 3 nmea 协议gps 模块的语言 4. GPS 精度与误差来源 4 gps 精度与误差来源 5. 坐标转换数学——从经纬度到平面米制坐标 5 坐标转换数学从经纬度到平面米制坐标 6. 传感器融合理论——为什么不能只靠 G…

Notebook Time
8 min
Image Frames
0
View Tracks
160
学习
FIELD_GUIDE

FIELD GUIDE

Use the guide rail to jump between sections.

目录

第一部分:基础知识——先搞懂 GPS 是什么

  1. GNSS 卫星定位基础
  2. 坐标系统与大地测量基础
  3. NMEA 协议——GPS 模块的"语言"
  4. GPS 精度与误差来源
  5. 坐标转换数学——从经纬度到平面米制坐标
  6. 传感器融合理论——为什么不能只靠 GPS
  7. ROS 2 GPS 生态——消息类型与工具链

第二部分:本项目 GPS 实现详解 8. 硬件连接与驱动层 9. GPS 数据流全景 10. GNSS 校准节点 —— gnss_calibration 11. PGO GPS 因子融合 —— 因子图优化中的 GPS 12. ENU→map 对齐 —— 连接 GPS 世界与 SLAM 世界 13. 三种运行模式中的 GPS 角色

第三部分:扩展开发——实现 GPS 远距离导航 14. 当前瓶颈分析 15. 拓展方向一:GPS 质量自适应融合 16. 拓展方向二:多点启动标定与地图锚定 17. 拓展方向三:GPS 全局路径规划闭环 18. 拓展方向四:RTK/差分 GPS 接入 19. 拓展方向五:GPS 辅助重定位与丢失恢复 20. 开发者实操 Cookbook


第一部分:基础知识——先搞懂 GPS 是什么

1. GNSS 卫星定位基础

1.1 什么是 GNSS

GNSS(Global Navigation Satellite System,全球导航卫星系统)是所有卫星定位系统的统称。GPS 只是其中之一:

系统国家/地区卫星数说明
GPS美国31 颗最早也最常用
GLONASS俄罗斯24 颗与 GPS 互补
BeiDou(北斗)中国45+ 颗亚太区精度更高
Galileo欧洲30 颗精度最高的民用系统

日常说的"GPS"往往指整个 GNSS 定位功能,本项目使用的模块实际上同时接收多个系统的信号(NMEA 语句以 $GN 开头就表示多系统联合解算)。

1.2 定位原理——三球交汇

GPS 定位的核心原理非常直观:

卫星 A 说:"你距我 20,000 km"  →  你在以 A 为圆心、20,000km 为半径的球上
卫星 B 说:"你距我 21,000 km"  →  两个球的交线是一个圆
卫星 C 说:"你距我 19,500 km"  →  三个球交于两个点
卫星 D 说:"你距我 20,200 km"  →  四颗卫星精确确定唯一位置

为什么需要 4 颗而不是 3 颗? 因为接收器的时钟不够精确,需要第 4 颗卫星解算时钟偏差。所以 GPS 实际上同时解算 4 个未知数:x, y, z, t(三维位置 + 时间)。

1.3 定位输出——"Fix" 的含义

GPS 模块输出的定位结果叫做 Fix(定位解)。Fix 有不同的质量等级:

Fix 类型状态码精度说明
No Fix-1无法定位(室内、遮挡严重)
Standard (SPS)02-10m标准民用定位
DGPS10.5-2m差分校正后的定位
RTK Float50.2-1mRTK 浮点解
RTK Fixed40.01-0.02mRTK 固定解,厘米级

在 ROS 2 中,sensor_msgs/NavSatFix 消息的 status.status 字段就对应上表。本项目当前使用的是 Standard Fix (status=0),精度约 2.5m。

1.4 关键质量指标

DOP(精度因子)

DOP 反映卫星几何分布对精度的影响。卫星分布越分散,DOP 越小,精度越高:

      好的几何分布(低 DOP)          差的几何分布(高 DOP)
          ★                              ★  ★
        /   \                              ||
      ★       ★                           ★  ★
       \     /
         📍                                📍
    精度: ~2m                           精度: ~8m
DOP 类型全称说明
HDOP水平精度因子水平方向的精度衰减,本项目主要看这个
VDOP垂直精度因子垂直方向的精度衰减
PDOP位置精度因子三维位置精度衰减
GDOP几何精度因子包含时间的总体精度衰减

经验值:HDOP < 1.5 优秀,< 3.0 可用,> 5.0 不可靠。本项目默认阈值 gps.quality_hdop_max: 3.0

卫星数

可见卫星越多,定位越稳定。经验上:

  • < 4 颗:无法定位
  • 4-6 颗:勉强可用,精度差
  • 7-10 颗:正常工作
  • > 10 颗:精度良好

2. 坐标系统与大地测量基础

这是做 GPS 开发最容易搞混的部分。花 10 分钟理解,可以避免数天的调试。

2.1 WGS84——GPS 的原生坐标系

GPS 模块输出的经纬度基于 WGS84(World Geodetic System 1984)椭球体。

北极
|
| 纬度 (latitude): 赤道为 0°,北为正
| 范围: -90° ~ +90°
| 范围: -180° ~ +180°
|
南极

注意:经纬度是"角度",不是长度!
1° 纬度 ≈ 111 km(处处相同)
1° 经度 ≈ 111 km × cos(纬度)(越靠近极点越短)
在苏州(纬度 ≈ 31°),1° 经度 ≈ 95 km

本项目中 ENU 原点lat: 31.274927, lon: 120.737548,位于 XJTLU 校区附近。

2.2 大地坐标与笛卡尔坐标

GPS 给出的是 大地坐标(经度、纬度、海拔),这是球面上的坐标,无法直接用于平面计算(比如求两点距离、求路径角度)。因此需要转换到某种 平面直角坐标系

2.3 ENU 坐标系——本项目的核心中间坐标系

ENU(East-North-Up) 是以某个固定点为原点的局部笛卡尔坐标系:

        North (Y)
          ↑
          |
          |
          +———————→ East (X)
         /
        /
       ↓ 
      Up (Z)(垂直向上)

原点 = 你选择的固定参考点
单位 = 米

为什么选 ENU?

  • 与 GPS 自然对接:经度增加 → East 增加,纬度增加 → North 增加
  • 单位是米,可以直接计算距离
  • ROS 里许多 GPS 工具默认使用 ENU

本项目中 ENU 原点的配置master_params.yaml):

"gps.origin_lat": 31.274927
"gps.origin_lon": 120.737548
"gps.origin_alt": 0.0

至关重要:PGO、dispatcher、aligner 三个模块的 ENU 原点必须完全一致,否则整个 GPS 坐标系就是错位的。

2.4 map 坐标系——SLAM 的世界

FAST-LIO2 启动时,车头正对的方向和 IMU 初始姿态共同决定了 map 坐标系的朝向。每次启动,map 坐标系的"北"可能指向不同的方向。

这就是 GPS 融合最核心的问题:ENU 的"北"是固定的(地理北),而 map 的"北"是随机的。

ENU 坐标系 (固定) map 坐标系 (每次不同)
North y_map
| | /

所以需要估计一个 ENU → map 的刚体变换(旋转角 θ + 平移 tx, ty),这就是 gps_global_aligner 做的事。

2.5 完整坐标系链

WGS84 (经纬度,°)
ENU (东-北-上, 米) ←── 以固定原点为零点的平面坐标
│ 由 aligner 或 PGO 估计
map (SLAM 全局坐标系) ←── 方向取决于启动时 IMU 姿态
│ map→odom (由 PGO 发布的校正量)
odom (里程计坐标系) ←── 高频精准但会漂移

3. NMEA 协议——GPS 模块的"语言"

3.1 NMEA 0183 格式

几乎所有消费级 GPS 模块都通过 NMEA 0183 协议输出数据。这是纯文本格式,通过串口传输。

一条典型的 NMEA 语句

$GNGGA,073412.00,3116.4956,N,12044.2529,E,1,08,1.2,15.3,M,-9.0,M,,*6A

拆解如下:

字段含义
$GNGGA语句类型:GN=多系统,GGA=定位数据
073412.00UTC 时间:07:34:12.00
3116.4956,N纬度:31°16.4956'N = 31.274927°
12044.2529,E经度:120°44.2529'E = 120.737548°
1定位质量:1=GPS fix
08使用卫星数:8 颗
1.2HDOP:1.2(优秀)
15.3,M海拔:15.3 米
*6A校验和

3.2 常见 NMEA 语句类型

语句说明包含信息
GGA定位数据时间、经纬度、质量、卫星数、HDOP、海拔
RMC推荐最小定位数据时间、经纬度、速度、航向、日期
GSA卫星状态DOP 值、使用的卫星编号
GSV可见卫星信息每颗卫星的仰角、方位角、信噪比
VTG地面速度和航向真北航向、磁北航向、速度

在本项目中nmea_navsat_driver 负责解析这些语句并转为 ROS 消息。它主要使用 GGA 语句。

3.3 纬度格式转换

NMEA 的纬度格式是 ddmm.mmmm(度分格式),而 ROS 和大多数编程接口使用 十进制度(decimal degrees)

# NMEA: 3116.4956
# 拆分: 31 度 16.4956 分
# 转换: 31 + 16.4956 / 60 = 31.274927°

def nmea_to_decimal(nmea_value, direction):
    degrees = int(nmea_value / 100)
    minutes = nmea_value - degrees * 100
    decimal = degrees + minutes / 60.0
    if direction in ('S', 'W'):
        decimal = -decimal
    return decimal

4. GPS 精度与误差来源

4.1 误差分类

误差来源量级能否消除
电离层延迟2-10m差分/双频可消
对流层延迟0.5-2m模型可减小
多径效应(信号反射)1-5m选择空旷位置
卫星轨道误差0.5-2m差分可消
接收器噪声0.3-1m取决于硬件
卫星几何分布 (DOP)倍增因子等待或换位置

4.2 多径效应——室外最常见的干扰

         卫星
        / |  \
直达路径/  |   \反射路径
      /   |    \
     📍  🏢    📍
    接收器 建筑物 反射面

接收器同时收到直达信号和反射信号
→ 距离计算出现偏差
→ 位置抖动或系统性偏移

本项目在 XJTLU 校园内运行,建筑物密集,多径效应是 GPS 误差的主要来源。

4.3 为什么本项目的 GPS 精度是 ~2.5m

本项目使用的是 Wheeltec GPS 模块,属于消费级单频 GNSS 接收器:

  • 单频(L1 频段)→ 无法消除电离层误差
  • 无差分校正 → 保留所有系统误差
  • 校园内建筑遮挡 → 可见卫星少 + 多径效应
  • 9600 波特率串口 → 更新率约 1Hz

这些因素叠加,实测精度约 2.5m(CEP95,即 95% 的时间误差在 2.5m 以内)。


5. 坐标转换数学——从经纬度到平面米制坐标

5.1 Haversine 公式——球面两点距离

最基础的 GPS 计算:已知两个经纬度,求它们之间的地面距离。

import math

def haversine_m(lat1, lon1, lat2, lon2):
    """计算两个经纬度点之间的地表距离(米)"""
    R = 6371000.0  # 地球平均半径(米)
    dlat = math.radians(lat2 - lat1)
    dlon = math.radians(lon2 - lon1)
    a = (math.sin(dlat/2)**2 +
         math.cos(math.radians(lat1)) *
         math.cos(math.radians(lat2)) *
         math.sin(dlon/2)**2)
    return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))

本项目中多处使用这个函数,比如 gnss_calibration_node.py 里检查样本稳定性、collect_gps_scene.py 里检查点间距离。

5.2 WGS84 → ENU 转换

本项目使用了两种实现方式:

方式一:pyproj(Python 端)

from pyproj import CRS, Transformer

# 创建以固定原点为中心的等距方位投影
origin_lat, origin_lon = 31.274927, 120.737548
local_crs = CRS.from_proj4(
    f"+proj=aeqd +lat_0={origin_lat} +lon_0={origin_lon} "
    "+datum=WGS84 +units=m +no_defs"
)
transformer = Transformer.from_crs("EPSG:4326", local_crs, always_xy=True)

# 转换:经纬度 → ENU (x=东, y=北)
def latlon_to_enu(lat, lon):
    x, y = transformer.transform(lon, lat)  # 注意 pyproj 参数顺序: lon, lat
    return x, y  # x=East, y=North

这就是本项目 scene_runtime.pyFixedENUProjector 类的实现。

方式二:GeographicLib(C++ 端)

#include <GeographicLib/LocalCartesian.hpp>

// 以固定原点创建转换器
GeographicLib::LocalCartesian geo_converter(
    31.274927,   // 原点纬度
    120.737548,  // 原点经度
    0.0          // 原点海拔
);

// 转换:经纬度 → ENU
double enu_x, enu_y, enu_z;
geo_converter.Forward(lat, lon, alt, enu_x, enu_y, enu_z);
// enu_x = East, enu_y = North, enu_z = Up

这就是本项目 PGO 节点 (pgo_node.cpp) 中 GPS 因子计算的实现方式。

5.3 ENU → map 刚体变换

这是连接 GPS 世界和 SLAM 世界的关键变换。数学形式:

│ │ = │ │ × │ │ + │ │

其中:

  • θ = ENU 坐标系与 map 坐标系之间的旋转角
  • tx, ty = 平移量
  • 这三个参数就是 gps_global_aligner 在线估计的

估计方法——最小二乘配准:

已知 N 组配对 (enu_i, map_i),需要找到最优的 (θ, tx, ty) 使得:

这是一个标准的 2D 刚体配准问题,有闭式解(不需要迭代):

def solve_rigid_2d(enu_points, map_points):
# 1. 计算质心

# 2. 去质心

# 3. 计算旋转角(使用 SVD 或直接公式)

# 4. 计算平移
[sin(theta), cos(theta)]])

return theta, t[0], t[1]

5.4 方位角(Bearing)计算

从点 A 到点 B 的地理方位角(以正北为 0°,顺时针):

def bearing_deg(lat1, lon1, lat2, lon2):
    """计算从 (lat1,lon1) 到 (lat2,lon2) 的方位角"""
    y = math.sin(math.radians(lon2 - lon1)) * math.cos(math.radians(lat2))
    x = (math.cos(math.radians(lat1)) * math.sin(math.radians(lat2)) -
         math.sin(math.radians(lat1)) * math.cos(math.radians(lat2)) *
         math.cos(math.radians(lon2 - lon1)))
    return (math.degrees(math.atan2(y, x)) + 360) % 360

注意:这个方位角是 罗盘方向(0°=北,90°=东),而 ENU 坐标系的 yaw 是 数学方向(0°=东,90°=北,逆时针正)。转换公式:enu_yaw = 90° - compass_heading。这就是项目中 compass_heading_to_enu_yaw_deg() 函数做的事。


6. 传感器融合理论——为什么不能只靠 GPS

6.1 各传感器对比

频率:   IMU 200Hz ████████████████████████████████████████
       LiDAR 10Hz ████
         GPS  1Hz █

精度:    GPS ██████████ 2.5m
       LiDAR ██ 0.05m (局部)
         IMU █ 0.01m (短期)

漂移:    GPS ✗ 不漂移(绝对坐标)
       LiDAR ✓ 长距离漂移
         IMU ✓ 快速漂移

核心矛盾:精度高的传感器会漂移,不漂移的传感器精度低。融合的目的就是取长补短。

6.2 因子图优化(Factor Graph)——本项目的融合方法

本项目使用 GTSAM 库进行因子图优化。这是当前机器人定位领域最主流的融合框架。

因子图的直觉理解

变量节点(位姿):   X0 ——— X1 ——— X2 ——— X3 ——— X4
                    |       |       |       |       |
因子(约束):       里程计  里程计  里程计  里程计  里程计
                    |               |               |
额外因子:          GPS因子         GPS因子         GPS因子
                                    |
                               回环检测因子

每个因子 = 一条"弹簧",附带噪声模型(弹簧的弹性系数)
优化 = 找到让所有弹簧能量总和最小的位姿序列

GPS 因子的数学定义(GTSAM 中的 GPSFactor):

给定关键帧 i 的位姿 Xi
GPS 在该时刻观测到 ENU 坐标 (gps_x, gps_y, gps_z)

GPS 因子 = \|Xi.translation - (gps_x, gps_y, gps_z)\|^2_\sum


关键理解noise_xy 越大,GPS 因子的"弹簧"越松,对最终优化结果的影响越小。这就是为什么 RTK GPS (σ=0.02m) 的约束力远大于普通 GPS (σ=2.5m)。

6.3 紧耦合 vs 松耦合

紧耦合松耦合
数据层级原始观测量(伪距、载波相位)定位结果(经纬度)
实现复杂度
精度潜力更高受限于 GPS 模块自身解算
本项目✗ 未使用✓ 直接使用 NavSatFix

本项目使用的是松耦合方案:GPS 模块自行解算出经纬度,PGO 把它作为一个位置约束加入因子图。这种方案实现简单,对当前的硬件精度来说已经足够。


7. ROS 2 GPS 生态——消息类型与工具链

7.1 核心消息类型

sensor_msgs/msg/NavSatFix

这是 GPS 数据的标准 ROS 消息,本项目所有 GPS 数据都用这个类型:

# 消息结构
Header header # 时间戳 + 坐标系 ID
NavSatStatus status # 定位状态
float64 latitude # 纬度(十进制度)
float64 longitude # 经度(十进制度)
float64 altitude # 海拔(米)
float64[9] position_covariance # 3×3 协方差矩阵(行主序)
uint8 position_covariance_type # 协方差类型

协方差矩阵的含义

position_covariance = [
σ_east², 0, 0, # [0] = 东向方差
0, σ_north², 0, # [4] = 北向方差
0, 0, σ_up² # [8] = 垂直方差
]

在代码中取水平精度:

horizontal_sigma = math.sqrt(max(msg.position_covariance[0],
                                  msg.position_covariance[4]))

sensor_msgs/msg/NavSatStatus

int8 STATUS_NO_FIX  = -1   # 无法定位
int8 STATUS_FIX     =  0   # 标准定位
int8 STATUS_SBAS_FIX =  1  # 差分定位
int8 STATUS_GBAS_FIX =  2  # 地面增强

int8 status                # 当前状态
uint16 service             # 使用的卫星系统

7.2 本项目使用的关键 ROS 2 包

作用本项目中的位置
nmea_navsat_driverNMEA 串口解析 → NavSatFixsrc/sensor_drivers/gnss/nmea_navsat_driver/
nmea_msgsNMEA 消息定义src/sensor_drivers/gnss/nmea_msgs/
tf2_ros坐标变换广播与监听ROS 2 内置
nav2_msgs导航 Action 接口Nav2 内置

7.3 常用调试命令

# 查看 GPS 话题是否存在
ros2 topic list | grep -E "fix|gnss|gps"

# 实时查看 GPS 数据
ros2 topic echo /fix

# 查看 GPS 发布频率
ros2 topic hz /fix

# 查看 GPS 协方差
ros2 topic echo /fix --field position_covariance

# 查看 TF 链是否完整
ros2 run tf2_ros tf2_echo map base_link

# 查看所有 TF 关系
ros2 run tf2_tools view_frames

第二部分:本项目 GPS 实现详解

8. 硬件连接与驱动层

8.1 硬件规格

项目
GPS 模块Wheeltec GPS(多系统 GNSS 接收器)
连接方式USB-UART 串口
设备文件/dev/wheeltec_gps
波特率9600
更新率~1 Hz
支持系统GPS + BeiDou + GLONASS
精度等级~2.5m (CEP95)

8.2 驱动节点:nmea_navsat_driver

nmea_navsat_driver 是一个标准的 ROS 2 GPS 驱动,它做的事情:

串口 → 读取 NMEA 语句 → 解析 GGA/RMC → 发布 /fix (NavSatFix)
                                       → 发布 /heading (如有)
                                       → 发布 /time_reference

配置在 master_params.yaml

/nmea_navsat_driver:
  ros__parameters:
    port: /dev/wheeltec_gps    # 串口设备路径
    baud: 9600                 # 波特率
    frame_id: gps              # TF 坐标系名
    useRMC: false              # 是否优先用 RMC 语句

9. GPS 数据流全景

┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│   GPS 卫星 → GPS 模块 → /dev/wheeltec_gps (NMEA 串口)                   │
│                              │                                           │
│                              ▼                                           │
│                    ┌─────────────────────┐                               │
│                    │ nmea_navsat_driver   │                               │
│                    │ (NMEA → NavSatFix)   │                               │
│                    └─────────┬───────────┘                               │
│                              │                                           │
│                              ▼                                           │
│                       话题: /fix                                         │
│                   (原始 GPS: 经纬度+状态)                                │
│                     ┌────────┼──────────┐                                │
│                     │        │          │                                 │
│                     ▼        ▼          ▼                                 │
│            ┌────────────┐ ┌──────┐ ┌──────────────┐                      │
│            │gnss_calibra│ │ PGO  │ │gps_global_   │                      │
│            │tion        │ │(直接 │ │aligner       │                      │
│            │(偏移校准)  │ │消费) │ │(独立对齐)    │                      │
│            └──────┬─────┘ └──┬───┘ └──────┬───────┘                      │
│                   │          │            │                               │
│                   ▼          ▼            ▼                               │
│             话题: /gnss   GPS Factor   ENU→map 变换                      │
│             (校准后)     (GTSAM)     (/gps_corridor/enu_to_map)          │
│                   │          │            │                               │
│                   ▼          ▼            ▼                               │
│              PGO GPS     map→odom    gps_route_runner                    │
│              Factor       TF 校正     (GPS 路点→Nav2 目标)              │
│                                          │                               │
│                                          ▼                               │
│                                   NavigateToPose                         │
│                                   → /cmd_vel → 电机                     │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

10. GNSS 校准节点 ── gnss_calibration

10.1 为什么需要校准

低成本 GPS 模块有固定的系统偏移。每次开机偏移量可能不同,但同一次运行期间偏移相对稳定。

10.2 校准算法

# 核心逻辑 (gnss_calibration_node.py)

# 1. 已知参考点坐标(从 calibration_points.yaml 读取)
ref_lat, ref_lon = 31.274927, 120.737548

# 2. 收集 5 个 GPS 样本
samples = [(31.274903, 120.737530), (31.274910, 120.737525), ...]

# 3. 检查稳定性(任意两点距离 < 1m)
for sample in samples:
    if haversine_m(samples[0], sample) > 1.0:
        重新采集

# 4. 计算偏移量
avg_lat = mean(s.lat for s in samples)  # GPS 平均读数
avg_lon = mean(s.lon for s in samples)
lat_offset = ref_lat - avg_lat          # 偏移 = 真实 - 观测
lon_offset = ref_lon - avg_lon

# 5. 后续所有 GPS 数据加上偏移量
calibrated_lat = raw_lat + lat_offset
calibrated_lon = raw_lon + lon_offset

10.3 关键限制

  • 启动时车必须停在校准点上——如果车不在,偏移量计算就是错的
  • 只做平移校正,不做旋转——假设 GPS 误差在局部区域内是均匀偏移
  • 单次校准,不持续更新——不适合长时间运行(偏移可能随大气条件变化)

11. PGO GPS 因子融合 ── 因子图优化中的 GPS

11.1 GPS 因子添加流程

每收到一帧关键帧:
  key_pose_count++
  if key_pose_count % gps_factor_interval == 0:
    从 GPS 缓存队列中找时间最近的 GPS 消息
    if 时间差 < 1秒:
      经纬度 → GeographicLib → ENU (x,y,z)
      if ENU→map 变换已知:
        将 ENU 转为 map 坐标
      创建 GTSAM::GPSFactor(关键帧索引, map坐标, 噪声模型)
      加入因子图
      触发 GTSAM 优化

11.2 噪声模型如何工作

// pgo_node.cpp 中的核心代码逻辑
gtsam::Vector3 noise_vec;
noise_vec << gps_noise_xy, gps_noise_xy, gps_noise_z;
auto gps_noise = gtsam::noiseModel::Diagonal::Sigmas(noise_vec);

// GPS 因子只约束位置 (x,y,z),不约束旋转
// 这意味着车的朝向完全由 LiDAR + IMU 决定
gtsam::GPSFactor gps_factor(key_index, gps_point, gps_noise);

热身机制:对齐刚建立时的前 N 个 GPS 因子使用更大的 sigma(更松的约束),避免不准确的对齐"硬拉"位姿图。

"gps.alignment_warmup_factors": 5     # 前 5 个 GPS 因子用松约束
"gps.alignment_warmup_sigma": 10.0    # 热身期 sigma = 10m(比正常的 2.5m 松很多)

11.3 ENU→map 对齐估计(PGO 内部)

PGO 在积累 GPS 因子的同时,也会缓存 (ENU, map) 配对。当配对数 ≥ alignment_min_points 且空间展幅 ≥ alignment_min_spread_m 时,执行 2D 刚体配准解算 ENU→map 变换。


12. ENU→map 对齐 ── 连接 GPS 世界与 SLAM 世界

12.1 为什么需要独立 aligner

最初的设计是让 PGO 同时做 loop closure 和 GPS 对齐。但实车发现 PGO 在 loop closure 时会调整所有位姿,导致 ENU→map 变换跟着"晃"。于是架构演进为:

v1: PGO 同时做 GPS 对齐 + loop closure → 对齐不稳定
v2: 独立 gps_global_aligner,与 PGO 解耦 → 对齐平滑

12.2 gps_global_aligner 的核心机制

# 简化版运行逻辑

# 1. Bootstrap: 启动时立即估计初始 ENU→map
yaw0 = 从 TF 读取 map→base_link 的朝向
launch_yaw_deg = 从 route 文件读取(车的地理朝向)
theta_bootstrap = yaw0 - radians(launch_yaw_deg)

# 2. 持续采集配对
while running:
    fix = 最新 /fix 消息
    enu = WGS84_to_ENU(fix.lat, fix.lon)
    map_xy = 从 TF 读取当前 map→base_link 位置

    # 只有移动了足够距离才添加配对
    if distance(enu, last_enu) > pair_min_spacing_m:
        pairs.append((enu, map_xy))

    # 3. 在线解算
    if len(pairs) >= alignment_min_pairs:
        raw_alignment = solve_rigid_2d(pairs)

        # 4. 限速平滑——防止跳变
        if |raw.theta - current.theta| > max_theta_step:
            theta_step = clamp(delta, -max_step, +max_step)
        if |raw.translation - bootstrap.translation| > max_delta:
            reject!  # 偏离 bootstrap 太远,可能是 GPS 跳变

        current_alignment += step  # 渐进更新

12.3 Waypoint 内冻结机制

当 runner 在执行一个 waypoint 到下一个 waypoint 的导航时,冻结当前 alignment 不更新。只在 waypoint 边界处才吸收新的 alignment。这防止了"走到一半目标点突然跳了"的问题。


13. 三种运行模式中的 GPS 角色

Explore GPSNav GPSCorridor
启动命令make launch-explore-gpsmake launch-nav-gpsmake launch-corridor
GPS 校准gnss_calibrationgps_anchor_localizer无(直接用 /fix
GPS→PGO✓ 通过 /gnss✓ 通过 /gnss✗ PGO GPS 关闭
全局对齐PGO 内部估计PGO 内部估计独立 gps_global_aligner
导航目标手动(RViz)按名字选择(goto_name自动分段(route 文件)
启动位置校准点附近任意 anchor 附近固定 Launch Pose

第三部分:扩展开发——实现 GPS 远距离导航

14. 当前瓶颈分析

基于项目文档和实车验证记录,当前 GPS 远距离导航存在以下核心问题:

14.1 GPS 启动锚定误差(2.5-4.75m)

单次启动采样的 GPS 坐标本身就有 ~2.5m 误差。整条路线的起点就已经偏了,后续所有路点都会系统性偏移。

14.2 ENU→map 对齐精度不足

因为 GPS 精度有限 + 采集配对需要足够空间展幅,对齐在运行初期(展幅不足 20m)几乎没有纠错能力。

14.3 FAST-LIO2 odom 发散

在持续 recovery/backup 动作后,FAST-LIO2 的 odom → base_link 出现跳变(单步 2.43m/0.11s),导致 Nav2 控制器混乱。这不是 GPS 的问题,但会影响 GPS 纠偏的效果。

14.4 PGO 稳定性

PGO 节点偶现段错误(exit code -11),导致 map → odom TF 消失,整个导航链断裂。


15. 拓展方向一:GPS 质量自适应融合

15.1 问题

当前 GPS 因子使用固定噪声模型(noise_xy = 2.5m),不区分 GPS 信号好坏。

15.2 解决思路

根据 GPS 消息自带的协方差动态调整因子噪声:

# 概念代码
def adaptive_gps_noise(fix_msg, base_noise_xy=2.5):
"""根据 GPS 协方差动态计算噪声"""
# 从消息中提取水平 sigma
max(fix_msg.position_covariance[0], # σ_east²
fix_msg.position_covariance[4]) # σ_north²
)
else:

# 限幅:不低于 0.3m(防止过度信任),不超过 10m

return sigma

15.3 在项目中的实现位置

修改 pgo_node.cpp 中的 tryAddGPSFactor() 函数:

// 当前: 固定噪声
gtsam::Vector3 noise_vec;
noise_vec << m_node_config.gps_noise_xy,
             m_node_config.gps_noise_xy,
             m_node_config.gps_noise_z;

// 改为: 自适应噪声
double reported_sigma_xy = std::sqrt(
    std::max(gps_msg->position_covariance[0],
             gps_msg->position_covariance[4]));
double clamped_sigma_xy = std::clamp(reported_sigma_xy, 0.3, 10.0);
noise_vec << clamped_sigma_xy, clamped_sigma_xy, m_node_config.gps_noise_z;

15.4 进阶:基于 HDOP 的因子间隔调整

# GPS 信号好的时候多加因子,差的时候少加
if hdop < 1.5:
    factor_interval = 3   # 密集
elif hdop < 3.0:
    factor_interval = 5   # 正常
else:
    factor_interval = 15  # 稀疏,甚至可以跳过

16. 拓展方向二:多点启动标定与地图锚定

16.1 问题

当前启动标定依赖单点 GPS,误差 ~2.5m 直接传导到整条路线。

16.2 解决思路:启动阶段短程移动+多点标定

1. 车辆上电,原地采集 GPS 样本 → 粗略锚定
2. 车辆沿已知路径移动 10-20m → 采集第二组 GPS 样本
3. 用两组 GPS + 对应 map 位姿做刚体配准
4. 多点标定的精度远高于单点

16.3 代码框架

class MultiPointCalibrator:
    """启动阶段多点标定器"""

    def __init__(self, projector: FixedENUProjector, min_spread_m=10.0):
        self.projector = projector
        self.min_spread = min_spread_m
        self.calibration_pairs = []  # [(enu_x, enu_y, map_x, map_y), ...]

    def add_pair(self, lat, lon, map_x, map_y):
        enu_x, enu_y = self.projector.forward(lat, lon)
        self.calibration_pairs.append((enu_x, enu_y, map_x, map_y))

    def can_solve(self) -> bool:
        if len(self.calibration_pairs) < 3:
            return False
        enu_points = [(p[0], p[1]) for p in self.calibration_pairs]
        spread = max_pairwise_distance(enu_points)
        return spread >= self.min_spread

    def solve(self) -> tuple[float, float, float]:
        """返回 (theta, tx, ty)"""
        enu = np.array([(p[0], p[1]) for p in self.calibration_pairs])
        map_ = np.array([(p[2], p[3]) for p in self.calibration_pairs])
        return solve_rigid_2d(enu, map_)

16.4 在项目中如何集成

  1. 修改 gps_global_aligner_node.py,在 bootstrap 阶段增加一个"移动标定"步骤
  2. 或者新建一个 multi_point_bootstrap_node.py,在 corridor runner 之前执行

17. 拓展方向三:GPS 全局路径规划闭环

17.1 当前状态

项目中有一个实验包 gnss_global_path_planner,支持 GeoJSON 路网 + A* 规划,但还没有接入主运行链

17.2 完整闭环的缺失环节

已有:
  ✓ GPS 采点脚本 (collect_gps_scene.py)
  ✓ GeoJSON 路网文件
  ✓ A* 全局规划器 (global_path_planner.py)
  ✓ 坐标转换节点 (global2local_tf.py)

缺失:
  ✗ GPS 路径 → Nav2 全局路径的实时对接
  ✗ 规划路径的 costmap 感知(当前 A* 只看 GPS 图,不看障碍物)
  ✗ 失败后的重规划机制

17.3 实现建议

方案 A:GPS 路径作为 Nav2 全局路径

# 新节点: gps_path_to_nav2.py
class GPSPathToNav2(Node):
    """将 GPS 全局路径转换为 Nav2 可执行路径"""

    def __init__(self):
        # 订阅 GPS 全局规划器的路径
        self.sub = self.create_subscription(
            Path, '/path4global', self.on_global_path, 10)
        # 使用 Nav2 的 FollowPath action
        self.follow_path_client = ActionClient(
            self, FollowPath, '/follow_path')

    def on_global_path(self, global_path: Path):
        # 1. GPS 路径 (经纬度) → ENU → map
        map_path = self.convert_to_map_frame(global_path)

        # 2. 沿路径采样点,检查 costmap 中是否有障碍
        checked_path = self.validate_against_costmap(map_path)

        # 3. 发送给 Nav2 执行
        self.follow_path_client.send_goal(checked_path)

方案 B:GPS 只做"远处目标选择",局部路径完全交给 Nav2

这是更保守但更可靠的方案。GPS 路网只决定"下一个要去的路口节点",Nav2 负责"怎么去那个节点":

# 类似当前 gps_waypoint_dispatcher 的思路
while not reached_final_destination:
    next_node = gps_planner.get_next_waypoint()
    map_goal = enu_to_map(latlon_to_enu(next_node.lat, next_node.lon))
    nav2.navigate_to_pose(map_goal)
    wait_until_reached_or_failed()

18. 拓展方向四:RTK/差分 GPS 接入

18.1 RTK 是什么

RTK(Real-Time Kinematic)利用载波相位测量 + 基站差分,可以达到 厘米级精度

                    RTK 工作原理

  GPS 卫星 ──→ 基站 (已知坐标) ──→ 计算校正量
      │                                │
      │                                │ RTCM 数据流
      │                                │ (通过网络/电台)
      ▼                                ▼
  GPS 卫星 ──→ 移动站 (你的车) + 校正量 = 厘米级定位

18.2 如果接入 RTK,需要改什么

硬件层

  • 更换为支持 RTK 的 GNSS 模块(如 u-blox F9P、华测 CGI-410)
  • 配置 NTRIP 客户端接收基站校正数据

软件层

# master_params.yaml 调整
/nmea_navsat_driver:
  ros__parameters:
    port: /dev/rtk_gnss       # 新设备路径
    baud: 115200               # RTK 模块通常用 115200
    frame_id: gps

/pgo:
  pgo_node:
    ros__parameters:
      "gps.noise_xy": 0.05    # RTK Fixed 精度 → 大幅减小噪声
      "gps.noise_z": 0.10
      "gps.factor_interval": 2 # 可以更频繁地加入 GPS 因子

同时需要根据 fix status 动态切换噪声

if fix.status == STATUS_RTK_FIXED:      # status = 4
    noise_xy = 0.02  # 2cm
elif fix.status == STATUS_RTK_FLOAT:    # status = 5
    noise_xy = 0.5   # 50cm
elif fix.status == STATUS_DGPS:         # status = 1
    noise_xy = 1.0   # 1m
else:
    noise_xy = 2.5   # 2.5m

18.3 NTRIP 客户端接入

# 安装 NTRIP 客户端
sudo apt install str2str  # RTKLIB 工具

# 从千寻位置等服务接收 RTCM 校正数据
str2str -in ntrip://username:password@rtk.ntrip.qxwz.com:8002/RTCM32_GGB \
        -out serial:///dev/rtk_gnss:115200

19. 拓展方向五:GPS 辅助重定位与丢失恢复

19.1 场景

长距离导航中,LiDAR 定位可能因为环境变化(比如进入一个没有特征的空旷区域)而丢失。此时需要 GPS 辅助恢复。

19.2 实现思路

class GPSRelocalizationNode(Node):
    """GPS 辅助重定位节点"""

    def __init__(self):
        self.localization_quality_sub = self.create_subscription(...)
        self.fix_sub = self.create_subscription(NavSatFix, '/fix', ...)

    def check_localization_health(self):
        """监控定位质量"""
        # 指标 1: PGO optimized_odom 发布频率下降
        # 指标 2: map→odom TF 时间戳过旧
        # 指标 3: Nav2 controller 频繁报 patience exceeded
        pass

    def gps_relocalize(self):
        """用 GPS 重新初始化位置"""
        # 1. 获取当前 GPS 位置
        gps_enu = self.get_stable_gps_enu(n_samples=10)

        # 2. 用已有的 ENU→map 对齐转为 map 坐标
        map_x = cos(theta) * gps_enu[0] - sin(theta) * gps_enu[1] + tx
        map_y = sin(theta) * gps_enu[0] + cos(theta) * gps_enu[1] + ty

        # 3. 发布初始位姿给 AMCL 或直接重置 PGO
        initial_pose = PoseWithCovarianceStamped()
        initial_pose.pose.pose.position.x = map_x
        initial_pose.pose.pose.position.y = map_y
        # 朝向用 GPS 航向或上一次已知朝向
        self.initial_pose_pub.publish(initial_pose)

20. 开发者实操 Cookbook

20.1 如何新增一个 GPS 相关节点

第 1 步:在合适的包下创建 Python 节点(遵循项目约定用 ament_python):

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import NavSatFix

class MyGPSNode(Node):
    def __init__(self):
        super().__init__('my_gps_node')
        self.sub = self.create_subscription(
            NavSatFix, '/fix', self.fix_callback, 10)

    def fix_callback(self, msg: NavSatFix):
        if msg.status.status < 0:
            return  # 无效定位,跳过
        self.get_logger().info(
            f'GPS: lat={msg.latitude:.7f}, lon={msg.longitude:.7f}')

def main(args=None):
    rclpy.init(args=args)
    node = MyGPSNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

第 2 步:在 setup.py 中注册可执行文件

第 3 步:构建和测试

colcon build --packages-select <your_package> --symlink-install --parallel-workers 1
source install/setup.bash
ros2 run <your_package> my_gps_node

20.2 如何使用 FixedENUProjector 做坐标转换

from gps_waypoint_dispatcher.scene_runtime import FixedENUProjector

# 创建投影器(使用项目统一的 ENU 原点)
projector = FixedENUProjector(
    origin_lat=31.274927,
    origin_lon=120.737548,
    origin_alt=0.0
)

# 经纬度 → ENU 坐标 (x=东, y=北, 单位=米)
enu_x, enu_y = projector.forward(31.275000, 120.738000)
print(f"ENU: ({enu_x:.2f}, {enu_y:.2f}) m")

20.3 如何读取当前 TF 位姿

from tf2_ros import Buffer, TransformListener

tf_buffer = Buffer()
tf_listener = TransformListener(tf_buffer, self)

# 获取 map→base_link 变换
try:
    t = tf_buffer.lookup_transform('map', 'base_link', rclpy.time.Time())
    x = t.transform.translation.x
    y = t.transform.translation.y
    # 提取 yaw
    q = t.transform.rotation
    yaw = math.atan2(2*(q.w*q.z + q.x*q.y), 1 - 2*(q.y**2 + q.z**2))
except TransformException as e:
    self.get_logger().warn(f'TF lookup failed: {e}')

20.4 如何发送 Nav2 导航目标

from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient

nav_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')
nav_client.wait_for_server()

goal = NavigateToPose.Goal()
goal.pose.header.frame_id = 'map'
goal.pose.header.stamp = self.get_clock().now().to_msg()
goal.pose.pose.position.x = target_x
goal.pose.pose.position.y = target_y
goal.pose.pose.orientation.z = math.sin(target_yaw / 2)
goal.pose.pose.orientation.w = math.cos(target_yaw / 2)

future = nav_client.send_goal_async(goal)

20.5 如何调试 GPS 融合效果

# 1. 录制 bag(包含所有 GPS 相关话题)
ros2 bag record /fix /gnss /fastlio2/lio_odom /pgo/optimized_odom /tf /tf_static

# 2. 回放分析
ros2 bag play <bag_directory>

# 3. 在 RViz 中对比
#    - 添加 /pgo/optimized_odom 的 Odometry 显示
#    - 添加 /fix 的 NavSatFix 显示(需要 mapviz 或自定义 marker)

# 4. 数值对比
ros2 topic echo /fix --field latitude --field longitude
ros2 topic echo /pgo/optimized_odom --field pose.pose.position

20.6 关键文件位置速查

功能文件路径
GPS 驱动配置src/bringup/config/master_params.yaml (/nmea_navsat_driver 段)
PGO GPS 因子参数src/bringup/config/master_params.yaml (/pgogps.*)
GPS 校准节点src/sensor_drivers/gnss/gnss_calibration/gnss_calibration/gnss_calibration_node.py
GPS 校准点src/sensor_drivers/gnss/gnss_calibration/config/calibration_points.yaml
PGO C++ 核心src/perception/pgo_gps_fusion/src/pgo_node.cpp
GTSAM 因子图src/perception/pgo_gps_fusion/src/pgos/simple_pgo.cpp
独立 Alignersrc/navigation/gps_waypoint_dispatcher/gps_waypoint_dispatcher/gps_global_aligner_node.py
Route Runnersrc/navigation/gps_waypoint_dispatcher/gps_waypoint_dispatcher/gps_route_runner_node.py
坐标转换工具src/navigation/gps_waypoint_dispatcher/gps_waypoint_dispatcher/scene_runtime.py
GPS 采场景脚本scripts/collect_gps_scene.py
GPS 采路线脚本scripts/collect_gps_route.py
Explore GPS Launchsrc/bringup/launch/system_explore_gps.launch.py
Corridor Launchsrc/bringup/launch/system_gps_corridor.launch.py
Nav GPS Launchsrc/bringup/launch/system_nav_gps.launch.py
Nav2 GPS 参数src/bringup/config/nav2_gps.yaml
运行时 GPS 数据~/XJTLU-autonomous-vehicle/runtime-data/gnss/

总结:要做好 GPS 远距离导航,核心是理解三件事:

  1. 坐标转换链(WGS84 → ENU → map)和每一步的误差传播
  2. 因子图融合的噪声模型如何平衡 GPS 与 LiDAR 的信任度
  3. ENU→map 对齐是整个系统的薄弱环节,改进它(多点标定、自适应更新、RTK 接入)是提升精度的最直接路径