☰
Fast-LIO融合GPS的三大技术关:坐标系、尺度、噪声建模
2026/11/3 5:07:02 网站建设 项目流程

简介:本资源是一份面向机器人与自动驾驶领域研究人员及工程师的Fast-LIO算法深度解析与工程改进文档,聚焦多传感器融合建图与定位中的关键瓶颈——点云特征缺失导致的建图失败问题。作者在原始Fast-LIO(IMU+激光雷达紧耦合)基础上,创新性引入GPS数据,完成坐标系对齐标定、姿态矫正与联合状态优化,显著提升复杂场景下的鲁棒性与定位精度。文档涵盖算法目的、完整流程(含时间对齐、前向/反向传播、KD树建图、观测方程设计与偏移量优化)、核心数学推导(位置/旋转/速度更新、李代数运算、误差传播协方差建模)及源码级改进细节。资源为1个756KB的docx文件,结构清晰,含详细目录、公式推导、流程图与参数说明,便于理论研读与代码对照。目前已有437人学习下载,适合具备SLAM基础、熟悉线性代数与概率论,并希望深入理解激光-IMU-GPS三源融合机制的进阶实践者。

1. Fast-LIO 不是“加个GPS就稳了”的黑匣子:它本质是IMU-雷达紧耦合的实时状态估计器,而GPS融合必须过三关——坐标系对齐、尺度漂移抑制、观测噪声建模

Fast-LIO 是一个典型的激光雷达惯性里程计(LIO)系统,它的核心不是“建图”,而是以IMU为时间轴、以点云为观测约束的高频率状态滤波器。很多人一看到“Fast-LIO + GPS”就默认“精度翻倍、鲁棒性拉满”,结果在mid360雷达+u-blox M8N GPS实测中,建图直接发散、轨迹跳变超2米——问题不在代码,而在对Fast-LIO底层机制的误读。它原生只用IMU和点云,是因为这两者在时间连续性、运动学一致性、可观测性维度上天然匹配:IMU提供100–200Hz的角速度/加速度微分量,点云提供每0.1s一次的全局几何观测,二者构成“高频预测+低频校正”的闭环。而GPS是离散、非同步、带多径误差、无姿态信息的绝对位置源,强行塞进Fast-LIO的状态向量(如把经纬度直接当p_I更新),等于让一个靠微分方程推演的精密陀螺仪,去信任一个每秒只报1次、误差±3米的定位信标。本文拆解的正是这个“塞”的过程:不是简单加一行gps_pose = convert_latlon_to_enu(gps_data),而是从坐标系标定、协方差注入、观测方程重构、状态扰动抑制四个硬核环节,把GPS变成Fast-LIO可消化的“营养剂”,而非“致幻剂”。适合正在调试mid360+IMU+GPS组合、卡在建图漂移或初始化失败的工程师;也适合想搞懂“为什么VIO加GPS不等于精度叠加”的SLAM初学者——你将亲手看到,一个H_gps雅可比矩阵怎么写,Q_gps协方差怎么设,以及为什么gps_yaw不能直接赋值给R_I。


2. Fast-LIO原生流程再精读:前向传播不是“积分”,而是李群上的状态流形演化,反向传播才是点云时空对齐的命门

Fast-LIO的实时性不来自算法简化,而来自对IMU运动学模型的严格李代数表达与高效数值实现。很多复现者卡在“跑起来但轨迹歪斜”,根源是对state_propagation()中R_I = R_I * exp((w_m - b_w) * dt)这行代码的物理意义理解偏差——它不是欧氏空间的矩阵乘法,而是SO(3)流形上的指数映射。本节从源码逻辑出发,逐层还原其数学内核与工程取舍。

2.1 状态向量设计:为什么R_L和T_L被固化,而b_w、b_a在源码中形同虚设?

Fast-LIO状态向量定义为:
x = [p_I, R_I, R_L, T_L, v_I, b_w, b_a, g]
共23维(p_I,v_I,g各3维;R_I,R_L各9维但实际存储为旋转四元数或李代数;T_L3维;b_w,b_a各3维)。但源码中b_w、b_a的更新项被注释掉,原因有三:

  1. 计算开销敏感:每帧点云处理需进行数十次前向传播迭代(对应点云内每个点的时间戳插值),若每次迭代都更新6维偏置,雅可比矩阵F_x维度从23×23升至29×29,求逆耗时增加40%以上;
  2. 可观测性不足:单线激光雷达缺乏足够几何约束来分离b_w与R_I的耦合误差,尤其在匀速直线运动时,b_w与真实角速度w同向,无法被观测方程区分;
  3. 硬件级补偿已存在:mid360配套IMU(如ADIS16470)出厂已做温漂标定,b_w、b_a在10分钟内漂移<0.01 rad/s,远小于点云匹配残差主导的误差量级。

提示:若你的IMU无出厂标定(如自搭MPU6050),必须启用b_w、b_a并增加外参标定步骤,否则R_I会随时间累积不可逆偏转。

R_L和T_L被固化为常量,因其代表雷达与IMU的刚体安装外参。Fast-LIO假设二者相对位姿恒定,故在初始化阶段通过手眼标定(如AprilTag+IMU静止采集)获得R_L0,T_L0后,直接赋值为状态向量初值,后续不参与优化。这是合理取舍:若每帧都优化R_L,则点云匹配会因外参抖动产生虚假运动伪影。

2.2 前向传播:exp()不是函数调用,而是李代数到李群的映射,dt必须是IMU帧间精确时间差

前向传播公式:
x_{i+1} = x_i + Δt * f(x_i, u_i, 0)
其中关键项为旋转更新:
R_I^{i+1} = R_I^i * exp( (w_m^i - b_w^i) ^ * Δt )

这里exp(·^)是SO(3)的指数映射,w_m^i是第i帧IMU角速度测量值(rad/s),Δt是该帧与下一帧的时间间隔(秒)。致命误区是直接用Δt = 0.005(IMU标称频率倒数)。实际中,mid360 IMU数据包含时间戳,需计算Δt = t_{i+1} - t_i,因IMU硬件时钟抖动,Δt在0.0048~0.0052s间波动。源码中若用固定Δt,1000次迭代后旋转误差可达0.5°,导致点云拼接错位。

# 正确做法:从IMU消息中提取精确时间戳 def imu_callback(msg): global last_imu_time, imu_queue current_time = msg.header.stamp.to_sec() if last_imu_time is not None: dt = current_time - last_imu_time # 精确Δt # 构造u_i = [w_m.T, a_m.T].T u_i = np.hstack([msg.angular_velocity.x, msg.angular_velocity.y, msg.angular_velocity.z, msg.linear_acceleration.x, msg.linear_acceleration.y, msg.linear_acceleration.z]) # 调用前向传播 state = forward_propagate(state, u_i, dt) last_imu_time = current_time

forward_propagate()内部对R_I的更新必须使用李代数运算库(如scipy.spatial.transform.Rotation或pytransform3d),禁用np.dot(R, delta_R)等欧氏近似——后者在大角度旋转时引入二阶截断误差。

2.3 反向传播:点云中每个点的“出生时间”决定其世界坐标,这才是Fast-LIO实时性的底层逻辑

Fast-LIO的“实时”并非指单帧处理快,而是对单帧点云内每个点进行毫秒级时间戳插值与坐标变换。mid360单帧点云含约20,000点,扫描时间约0.1s,但点云内各点并非同时采集:首点时间戳为t_start,末点为t_end = t_start + 0.1,中间点按线性扫描模式分配时间戳。反向传播即对每个点p_j,找到其时间戳t_j所对应的IMU状态x(t_j),再经R_L,T_L转换到世界坐标。

流程如下:

  1. 对点云cloud中每个点p_j,计算其在扫描周期内的归一化时间α_j = (t_j - t_start) / 0.1;
  2. 在IMU队列中二分查找[t_i, t_{i+1}]使得t_i ≤ t_j < t_{i+1};
  3. 对x_i进行线性插值(或更优的slerp插值)得x(t_j);
  4. 计算世界坐标:p_G = R_I(t_j) * (R_L * p_j + T_L) + p_I(t_j)。

注意:步骤3中若用线性插值旋转矩阵R_I,会导致R_I(t_j)不满足正交性,引发后续KD树匹配失败。必须用四元数球面插值slerp(q_i, q_{i+1}, α)或李代数插值q(t_j) = q_i * exp(δθ * α)。

此过程耗时占单帧处理70%以上,是Fast-LIO性能瓶颈。源码中通过预分配IMU状态缓存、使用numba.jit加速插值,将20,000点反向传播控制在8ms内。

2.4 KD树建图:不是“存点”,而是构建带体素约束的最近邻索引,且必须动态更新

Fast-LIO的建图模块本质是动态维护一个体素化KD树(Voxelized KD-Tree),其设计目标是:

  • 每个体素(如0.2m³)仅保留距离体素中心最近的一个点,避免重复点污染匹配;
  • 树结构支持O(log N)最近邻搜索,用于观测方程中点到平面的距离计算;
  • 点云随机器人运动持续插入/删除,树需增量更新而非全量重建。

源码中map_kdtree类的关键参数:

  • voxel_size: 体素边长,mid360推荐0.15~0.25m。过小则体素过多,内存暴涨;过大则丢失细节,平面拟合失效;
  • max_points_per_voxel: 每个体素最大点数,设为1强制去重;
  • max_range: 最大有效距离,过滤远距离噪声点(如>50m的天空点)。
// C++源码片段:体素哈希与点插入 struct VoxelKey { int x, y, z; bool operator==(const VoxelKey& other) const { return x == other.x && y == other.y && z == other.z; } }; std::unordered_map<VoxelKey, Point, VoxelHash> voxel_map; void insert_point(const Point& p) { VoxelKey key; key.x = floor(p.x / voxel_size); key.y = floor(p.y / voxel_size); key.z = floor(p.z / voxel_size); auto it = voxel_map.find(key); if (it == voxel_map.end()) { voxel_map[key] = p; // 首点直接存 } else { // 计算到体素中心距离,保留更近者 float dist_new = dist_to_voxel_center(p, key); float dist_old = dist_to_voxel_center(it->second, key); if (dist_new < dist_old) voxel_map[key] = p; } }

避坑 / 常见问题 / 排查 / 注意

  1. 现象:建图初期点云稀疏,KD树匹配失败率>90%,hj(xk,fj)=0无解,优化崩溃。
    原因:初始几帧点云未填满体素,voxel_map为空,kdtree.nearest_neighbor()返回空。
    解决:在map_kdtree初始化时,预插入100个随机分布点(如[-10,-10,-10]到[10,10,10]),确保树结构存在;或添加判空逻辑:if (kdtree.size() < 100) return;

  2. 现象:长时间运行后内存占用飙升至10GB+,ROS节点被OOM killer终止。
    原因:voxel_map未设置最大容量,旧体素点永不删除,而机器人移动导致新区域持续插入。
    解决:添加滑动窗口机制——记录每个体素最后更新时间戳,当current_time - last_update > 30.0(秒),自动清除该体素;或限制voxel_map.size()上限为50,000。

  3. 现象:建图出现“鬼影”,同一物体在地图中重复出现两处,间距约IMU零偏导致的位移。
    原因:R_L、T_L标定误差未消除,反向传播时R_L * p_j + T_L计算错误,点云在IMU坐标系下扭曲。
    解决:用kalibr工具链重新标定外参,获取R_L的旋转误差角<0.1°、T_L平移误差<2mm;或在Fast-LIO启动时,让机器人静止10秒,用IMU重力向量g反推R_L的俯仰/横滚角。

  4. 现象:点云匹配残差忽大忽小(如0.02m跳至0.3m),轨迹抖动。
    原因:max_range设为100m,包含远处树木/建筑点,其法向量不稳定,导致平面拟合失败。
    解决:根据应用场景动态设max_range——室内用20m,城市场景用50m,并在点云预处理中添加基于曲率的边缘点剔除(curvature < 0.1)。

  5. 现象:CPU占用率100%,但点云处理帧率仅5Hz,远低于mid360的10Hz。
    原因:kdtree.nearest_neighbor()未使用近似搜索(ANN),每次强制遍历全树。
    解决:切换至nanoflann库,启用KNN搜索(num_neighbors=5),并设置epsilon=0.1允许10%精度损失,速度提升3倍。


3. GPS融合的三大生死关:坐标系对齐、尺度漂移抑制、观测噪声建模,缺一不可

将GPS接入Fast-LIO绝非“读取经纬度→转ENU→赋值给p_I”的三行代码。GPS数据与Fast-LIO原生状态存在坐标系不一致、时间不同步、尺度不匹配、噪声特性迥异四大鸿沟。本节直击要害,给出可落地的工程方案。

3.1 坐标系对齐:ENU不是万能坐标系,必须用WGS84椭球参数做高精度投影

Fast-LIO的世界坐标系(Global Frame)是局部ENU(东-北-天),原点为第一帧IMU位置。GPS输出的是WGS84经纬度(lat, lon, alt),需转为ENU坐标(e, n, u)。常见错误是直接调用pyproj的Transformer.from_crs("EPSG:4326", "EPSG:25832")——这适用于大范围测绘,但对<1km的建图场景,其平面投影误差达厘米级,且忽略地球曲率导致u(高度)失真。

正确做法:使用WGS84椭球参数(长半轴a=6378137.0,扁率f=1/298.257223563)进行局部切平面投影:

import numpy as np def latlonalt_to_enu(lat0, lon0, alt0, lat, lon, alt): """ WGS84 to ENU conversion at reference point (lat0, lon0, alt0) :param lat0, lon0, alt0: reference point in radians and meters :param lat, lon, alt: target point in radians and meters :return: e, n, u in meters """ # WGS84 parameters a = 6378137.0 f = 1/298.257223563 e2 = 2*f - f*f # first eccentricity squared # Prime vertical radius of curvature at lat0 N0 = a / np.sqrt(1 - e2 * np.sin(lat0)**2) # ENU components e = (lon - lon0) * np.cos(lat0) * (N0 + alt0) n = (lat - lat0) * (a * (1 - e2) / (1 - e2 * np.sin(lat0)**2)**1.5 + alt - alt0) u = alt - alt0 return e, n, u # 初始化时获取第一帧GPS作为参考点 ref_lat, ref_lon, ref_alt = 31.2304, 121.4737, 5.0 # 上海某地 ref_lat_rad, ref_lon_rad, ref_alt_m = np.radians(ref_lat), np.radians(ref_lon), ref_alt def gps_callback(msg): lat_rad = np.radians(msg.latitude) lon_rad = np.radians(msg.longitude) alt_m = msg.altitude e, n, u = latlonalt_to_enu(ref_lat_rad, ref_lon_rad, ref_alt_m, lat_rad, lon_rad, alt_m) gps_enu = np.array([e, n, u]) # 后续注入到Fast-LIO

注意:ref_lat,ref_lon必须取自建图起始点的GPS,且需静止10秒取均值,避免初始化跳变。

3.2 尺度漂移抑制:GPS不提供尺度,必须用重力向量锚定Z轴,否则高度发散

Fast-LIO的p_I包含Z轴(天向)分量,而GPS高度alt受大气折射、多径影响,误差达±5m。若直接将u赋值给p_I[2],会导致建图高度随时间漂移。根本解法是用IMU重力向量g约束Z轴尺度:

  • IMU静止时,加速度计读数a_m ≈ -R_I * g_world,其中g_world = [0,0,9.80665];
  • 通过R_I的第三列(即Z轴方向)可得g_measured = -a_m,其模长应为9.80665;
  • 若||g_measured|| ≠ 9.80665,说明R_I存在尺度误差,需校准。

源码中在初始化阶段加入重力校准:

// 初始化时静止10秒,计算平均加速度 Eigen::Vector3d a_avg = Eigen::Vector3d::Zero(); int count = 0; while (count < 1000) { // 1000帧@100Hz a_avg += imu_data.a_m; count++; } a_avg /= 1000.0; double g_norm = a_avg.norm(); // 计算尺度因子 double scale_z = 9.80665 / g_norm; // 在前向传播中修正Z轴加速度 a_corrected = a_avg * scale_z;

GPS的u值仅用于约束p_I的Z轴绝对位置,而非提供尺度。观测方程中,GPS观测为:
z_gps = H_gps * x_k + v_gps
其中H_gps = [0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0](仅选择p_I[2]),协方差R_gps = diag([0.1, 0.1, 25.0])(XY方向0.1m,Z方向5m²,因高度误差大)。

3.3 观测噪声建模:GPS不是高斯白噪声,必须用时间相关协方差Q_gps(t)替代静态R

GPS噪声具有强时间相关性:多径效应导致连续几秒定位误差同向,R_gps若设为静态对角阵,会使卡尔曼增益K过度信任GPS,引发轨迹震荡。正确建模是将GPS观测视为有色噪声,用一阶马尔可夫过程描述:

v_gps(t) = ρ * v_gps(t-1) + w(t)
其中ρ=0.9为相关系数,w(t) ~ N(0, Q_w)为白噪声。

Fast-LIO中,将GPS观测融入状态向量扩展为x_aug = [x; v_gps],则扩展状态方程为:
x_aug_{k+1} = [F 0; 0 ρ] * x_aug_k + [0; w_k]
观测方程为:
z_gps = [0 I] * x_aug_k + v_k

但此法增加6维状态,计算量剧增。工程折中方案:在每次GPS更新时,动态调整R_gps:

# GPS协方差随时间衰减 last_gps_time = 0.0 def gps_callback(msg): global last_gps_time current_time = msg.header.stamp.to_sec() dt = current_time - last_gps_time last_gps_time = current_time # 时间越久,GPS可信度越低 alpha = np.exp(-dt / 5.0) # 5秒时间常数 R_gps_dynamic = np.diag([0.1, 0.1, 25.0 * (1.0 + 0.5 * (1.0 - alpha))]) # 注入观测 gps_enu = latlonalt_to_enu(...) fastlio.add_gps_observation(gps_enu, R_gps_dynamic)

alpha随dt增大而减小,R_gps_dynamic[2,2]从25.0增至37.5,迫使滤波器降低对GPS高度的信任度,转而依赖IMU积分。

3.4 观测方程重构:GPS不提供姿态,但可通过航向角约束R_I的偏航,需构造H_yaw

GPS模块(如u-blox M8N)可输出course_over_ground(对地航向角),精度约±2°。虽不能直接给R_I,但可约束其偏航角yaw。Fast-LIO中R_I用四元数q = [q_w, q_x, q_y, q_z]表示,偏航角ψ = atan2(2(q_w*q_z + q_x*q_y), 1-2(q_y²+q_z²))。

观测方程为:
z_yaw = ψ(q) - ψ_gps + v_yaw
其雅可比矩阵H_yaw = ∂ψ/∂q需解析推导:

def compute_yaw_jacobian(q): """ q: [q_w, q_x, q_y, q_z] return: 1x4 Jacobian dψ/dq """ qw, qx, qy, qz = q denom = 1 - 2*(qy**2 + qz**2) # ψ = atan2(numerator, denominator) num = 2*(qw*qz + qx*qy) den = denom # dψ/dq = (den * d(num)/dq - num * d(den)/dq) / (num^2 + den^2) d_num_dq = np.array([2*qz, 2*qy, 2*qx, 2*qw]) d_den_dq = np.array([0, 0, -4*qy, -4*qz]) d_psi_dq = (den * d_num_dq - num * d_den_dq) / (num**2 + den**2) return d_psi_dq.reshape(1, -1) # 在观测注入时 psi_gps = np.radians(msg.course_over_ground) H_yaw = compute_yaw_jacobian(current_q) R_yaw = np.array([[0.001]]) # (2°)^2 ≈ 0.001 rad² fastlio.add_yaw_observation(psi_gps, H_yaw, R_yaw)

此操作将GPS航向角转化为对R_I的软约束,显著改善长走廊场景下的偏航漂移。

避坑 / 常见问题 / 排查 / 注意

  1. 现象:GPS注入后,轨迹在XY平面收敛,但Z轴(高度)持续上升/下降,10分钟漂移超10m。
    原因:未做重力校准,R_I的Z轴方向错误,导致a_m在R_I下投影失真,IMU积分高度发散;GPS高度u又因多径误差偏高,滤波器被误导。
    解决:强制在初始化阶段执行重力校准(静止10秒),并设置R_gps[2,2] = 100.0(10m误差),降低GPS高度权重。

  2. 现象:GPS航向角course_over_ground突变(如从0°跳至180°),导致R_I瞬间翻转,建图断裂。
    原因:atan2函数在ψ=±π处不连续,GPS航向角噪声触发跳变。
    解决:对ψ_gps做一阶低通滤波:ψ_smooth = 0.95 * ψ_smooth_prev + 0.05 * ψ_gps;或改用np.unwrap()处理航向角序列。

  3. 现象:多台设备同时运行时,GPS时间戳与IMU不同步,gps_callback与imu_callback时间差达200ms,导致观测滞后。
    原因:未做硬件时间同步,GPS与IMU各自使用独立晶振,日漂移达毫秒级。
    解决:使用PTP(Precision Time Protocol)或GPS PPS(Pulse Per Second)信号同步所有传感器时钟;或在软件层,用rosbag重播时启用--clock参数,统一时间基准。

  4. 现象:城市峡谷中GPS信号弱,fix_type=0(无定位),但Fast-LIO仍尝试注入NaN观测,导致状态向量崩溃。
    原因:未检查GPS消息有效性字段。
    解决:在gps_callback中添加:if msg.status.status == -1 or msg.status.service == 0: return(status=-1表示无定位)。

  5. 现象:GPS经纬度转ENU后,e、n值巨大(如e=300000),导致p_I溢出,double精度丢失。
    原因:参考点lat0, lon0选在赤道,而实际运行在中纬度,投影畸变放大。
    解决:lat0, lon0必须取自建图区域中心,且每次新任务前重置;或改用相对坐标:e_rel = e - e_ref,n_rel = n - n_ref,e_ref,n_ref为第一帧GPS值。


4. 改进版Fast-LIO源码集成:从修改CMakeLists.txt到重写gps_handler.cpp,五步完成GPS融合

将GPS融合代码集成到Fast-LIO官方仓库(https://github.com/hku-mars/Fast-LIO)需修改7个文件,但核心改动集中在5处。本节提供可直接复制的代码块与参数说明,避免“改完编译不过”的玄学时刻。

4.1 第一步:扩展状态向量,在src/utility.h中定义State结构体新增GPS相关成员

Fast-LIO原状态向量State定义在src/utility.h,需为其添加GPS偏置项p_gps_bias(用于吸收GPS系统性偏差):

// src/utility.h 修改 State 结构体 struct State { Vec3 pos; // p_I: position in global frame Mat3 rot; // R_I: rotation from global to IMU frame Mat3 rot_lidar; // R_L: rotation from lidar to IMU frame (fixed) Vec3 t_lidar; // T_L: translation from lidar to IMU frame (fixed) Vec3 vel; // v_I: velocity in global frame Vec3 bg; // b_w: gyro bias (unused in original) Vec3 ba; // b_a: accel bias (unused in original) Vec3 gravity; // g: gravity vector Vec3 p_gps_bias; // NEW: GPS position bias [e, n, u] to absorb systematic error double last_gps_time; // NEW: timestamp of last GPS update };

参数说明:p_gps_bias是3维向量,初值设为Vec3::Zero();last_gps_time用于计算GPS时间间隔,初值为-1.0。

4.2 第二步:修改前向传播,在src/estimator.cpp中更新State传播逻辑

前向传播函数propagate_state()需在更新pos、rot后,保持p_gps_bias不变(因其为慢变偏差,不参与IMU动力学):

// src/estimator.cpp 修改 propagate_state() void Estimator::propagate_state(State& s, const Vec6& u, double dt) { // ... 原有pos, rot, vel更新代码 ... // 新增:保持GPS偏差不变 // s.p_gps_bias 不更新,因其变化远慢于IMU状态 // s.last_gps_time 不更新,由GPS回调设置 }

4.3 第三步:编写GPS处理器,在src/gps_handler.cpp中实现坐标转换与观测注入

新建src/gps_handler.cpp,封装GPS处理逻辑:

// src/gps_handler.cpp #include "utility.h" #include <cmath> #include <Eigen/Dense> // WGS84椭球参数 const double a = 6378137.0; const double f = 1.0 / 298.257223563; const double e2 = 2*f - f*f; struct GPSHandler { double ref_lat, ref_lon, ref_alt; // 弧度制 double last_gps_time = -1.0; void set_reference(double lat_deg, double lon_deg, double alt_m) { ref_lat = lat_deg * M_PI / 180.0; ref_lon = lon_deg * M_PI / 180.0; ref_alt = alt_m; } Vec3 latlonalt_to_enu(double lat_deg, double lon_deg, double alt_m) { double lat = lat_deg * M_PI / 180.0; double lon = lon_deg * M_PI / 180.0; double N0 = a / sqrt(1 - e2 * sin(ref_lat)*sin(ref_lat)); double e = (lon - ref_lon) * cos(ref_lat) * (N0 + ref_alt); double n = (lat - ref_lat) * (a * (1 - e2) / pow(1 - e2 * sin(ref_lat)*sin(ref_lat), 1.5)) + alt_m - ref_alt; double u = alt_m - ref_alt; return Vec3(e, n, u); } void handle_gps(Estimator& est, double lat, double lon, double alt, double time) { Vec3 enu = latlonalt_to_enu(lat, lon, alt); // 减去GPS偏差,得到真实位置观测 Vec3 z_gps = enu - est.state.p_gps_bias; // 动态协方差 double dt = (time - est.state.last_gps_time); double alpha = std::exp(-dt / 5.0); Mat3 R_gps = Mat3::Zero(); R_gps(0,0) = 0.01; // 0.1m² R_gps(1,1) = 0.01; R_gps(2,2) = 25.0 * (1.0 + 0.5 * (1.0 - alpha)); // 高度协方差随时间增大 // 注入观测:H_gps = [I_3, 0_3x20],仅观测pos est.add_gps_observation(z_gps, R_gps); est.state.last_gps_time = time; } };

4.4 第四步:修改观测注入,在src/estimator.cpp中重载add_gps_observation

在Estimator类中添加add_gps_observation()方法,构造GPS观测雅可比矩阵H_gps:

// src/estimator.cpp 在Estimator类中添加 void Estimator::add_gps_observation(const Vec3& z_gps, const Mat3& R_gps) { // H_gps: 3x23 matrix, selects only pos (first 3 elements) MatX H_gps = MatX::Zero(3, state_dim); H_gps.block<3,3>(0,0) = Mat3::Identity(); // pos is at index 0-2 // 观测残差 Vec3 r = z_gps - state.pos; // 卡尔曼更新 MatX S = H_gps * P * H_gps.transpose() + R_gps; MatX K = P * H_gps.transpose() * S.inverse(); state.pos += K.block<3,3>(0,0) * r; state.p_gps_bias += <p> <a href="https://download.csdn.net/download/qq_61179559/90990548" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询