FAST-LIO src/IMU_Processing.hpp 完整详细讲解
这份IMU_Processing.hpp是 FAST-LIO 前端里最关键的模块之一。它不负责 ikd-Tree 最近邻搜索也不直接构造点到平面残差它负责在 LiDAR 匹配前完成两件基础工作1利用高频 IMU把上一帧结束后的状态传播到当前 LiDAR 扫描结束时刻。 2利用扫描期间的 IMU 运动轨迹把一帧中不同时间采到的 LiDAR 点 统一补偿到“扫描结束时刻”的 LiDAR 坐标系。也就是sync_packages() ↓ 得到当前 LiDAR 帧 对应 IMU 数据 ↓ ImuProcess::Process() ↓ IMU 初始化 或 IMU 前向传播 ↓ UndistortPcl() ↓ 去畸变点云 feats_undistort ↓ laserMapping.cpp ↓ ikd-Tree 匹配 IESKF LiDAR 修正你给的博客把laserMapping.cpp主链路拆成“同步 → IMU 传播与去畸变 → LiDAR 匹配 → IESKF → 地图更新”这个IMU_Processing.hpp正好就是其中第二段的完整实现。1ImuProcess类整体职责类定义中的核心接口是void Process( const MeasureGroup meas, esekfom::esekfstate_ikfom, 12, input_ikfom kf_state, PointCloudXYZI::Ptr pcl_un_);三个输入分别是meas sync_packages() 打好的当前数据包。 里面至少包含当前 LiDAR、LiDAR 起止时间、当前帧需要的 IMU。 kf_state FAST-LIO 的 IESKF 状态。 进入本函数时它保存的是上一帧 LiDAR 优化后的状态 离开本函数时它变成当前扫描结束时刻的 IMU 预测状态。 pcl_un_ 输出去畸变点云。 后续 laserMapping.cpp 会将它作为 feats_undistort 使用。Process()的工作流很简单当前帧 MeasureGroup ↓ 没有 IMU ↓ 是 直接 return 仍处于初始化阶段 ↓ 是 IMU_init() 初始化重力、gyro bias、协方差 ↓ return 初始化完成 ↓ 是 UndistortPcl() IMU 前向传播 逐点去畸变对应源码主逻辑if(meas.imu.empty()) {return;}; if (imu_need_init_) { IMU_init(meas, kf_state, init_iter_num); if (init_iter_num MAX_INI_COUNT) { imu_need_init_ false; } return; } UndistortPcl(meas, kf_state, *cur_pcl_un_);这意味着原版这条流程没有“没有 IMU 就自动使用纯 LiDAR 去畸变”的备用逻辑。没有 IMU 时它不会做本帧预测也不会做逐点去畸变。2输入MeasureGroup到底包含什么虽然MeasureGroup定义在其他文件中但从本文件使用方式可以确定它至少包含meas.lidar 当前帧经过 preprocess.cpp 处理后的 LiDAR 点云。 meas.imu 当前 LiDAR 扫描相关的 IMU 队列。 meas.lidar_beg_time 当前 LiDAR 扫描开始时刻。 meas.lidar_end_time 当前 LiDAR 扫描结束时刻。其时间关系是LiDAR 当前帧 t_begin -------------------------------- t_end IMU u0 --- u1 --- u2 --- ... --- un 要求 最后到达的 IMU 时间已经覆盖 t_end 但当前打包进 meas.imu 的最后一条 IMU 时间通常不超过 t_end。所以在UndistortPcl()中最后一条实际取出的 IMU 很可能略早于扫描结束时间。随后代码会使用这最后一段 IMU 的运动模型把状态短时间传播到精确的t_end。最后一条取出的 IMU t_imu_last ≤ t_end 随后 kf_state.predict(t_end - t_imu_last, Q, in) 得到 扫描结束时刻的预测状态。这就是你前面问的“最后取出的 IMU 时间仍然比 end 小后面怎么办”的代码实现位置。3三个核心坐标系与外参整个IMU_Processing.hpp只要把三个坐标系理清后面去畸变公式就不乱。LLiDAR 坐标系 IIMU 坐标系 / 车体坐标系 G世界坐标系 FAST-LIO 中通常叫 camera_init代码中的外参变量M3D Lidar_R_wrt_IMU; V3D Lidar_T_wrt_IMU;含义是^I R_L LiDAR 坐标系旋转到 IMU 坐标系的旋转外参。 ^I t_L LiDAR 原点在 IMU 坐标系中的位置。对应关系LiDAR 点在采样时刻 ti ^L_ti p_i LiDAR → IMU ^I p_i ^I R_L · ^L_ti p_i ^I t_L变量 ^L_ti p_i 第 i 个原始 LiDAR 点 坐标表达在该点真正采样时刻的 LiDAR 坐标系。 ^I R_L LiDAR 到 IMU 的旋转外参。 ^I t_L LiDAR 原点相对 IMU 原点的平移外参。 ^I p_i 同一个点在 IMU 坐标系中的表示。为什么平移外参也必须处理因为 LiDAR 与 IMU 通常并不重合。比如 LiDAR 在 IMU 前方 20 cm车辆原地旋转时LiDAR 原点会绕 IMU 原点做圆周运动若忽略这一段杆臂近距离墙面和货架边缘会出现明显偏移。4FAST-LIO 在 IMU 中传播的状态本文件使用esekfom::esekfstate_ikfom, 12, input_ikfom kf_state;这里的12不是状态维度而是过程噪声维度。根据代码中状态字段与协方差索引完整状态可理解为x [ ^G p_I, ^G R_I, ^I R_L, ^I t_L, ^G v_I, b_g, b_a, ^G g ]变量 ^G p_I IMU 在世界坐标系中的位置3 维。 ^G R_I IMU 到世界坐标系的姿态局部误差 3 维。 ^I R_L LiDAR 到 IMU 的旋转外参3 维。 ^I t_L LiDAR 原点在 IMU 系中的位置3 维。 ^G v_I IMU 在世界系中的速度3 维。 b_g 陀螺仪零偏3 维。 b_a 加速度计零偏3 维。 ^G g 世界系重力方向。 重力大小固定所以本质只有 2 个自由度。总自由度是3 3 3 3 3 3 3 2 23过程噪声是 12 维w [ n_g, n_a, n_bg, n_ba ]变量 n_g 陀螺仪白噪声3 维。 n_a 加速度计白噪声3 维。 n_bg 陀螺仪零偏随机游走3 维。 n_ba 加速度计零偏随机游走3 维。本文件中的Q就是这 12 维过程噪声协方差。5构造函数默认配置了什么构造函数如下ImuProcess::ImuProcess() : b_first_frame_(true), imu_need_init_(true), start_timestamp_(-1) { init_iter_num 1; Q process_noise_cov(); cov_acc V3D(0.1, 0.1, 0.1); cov_gyr V3D(0.1, 0.1, 0.1); cov_bias_gyr V3D(0.0001, 0.0001, 0.0001); cov_bias_acc V3D(0.0001, 0.0001, 0.0001); mean_acc V3D(0, 0, -1.0); mean_gyr V3D(0, 0, 0); angvel_last Zero3d; Lidar_T_wrt_IMU Zero3d; Lidar_R_wrt_IMU Eye3d; last_imu_.reset(new sensor_msgs::Imu()); }含义是imu_need_init_ true 系统刚启动时先进行 IMU 初始化。 mean_acc (0, 0, -1) 加速度均值初值。 后续会被真实 IMU 数据覆盖。 mean_gyr (0, 0, 0) 陀螺仪均值初值。 Lidar_T_wrt_IMU 0 Lidar_R_wrt_IMU I 默认假设 LiDAR 与 IMU 重合、轴方向一致。 实际运行前必须通过 set_extrinsic() 写入真实外参。外参有三种设置方式set_extrinsic(transl, rot); set_extrinsic(transl); set_extrinsic(T_4x4);第一种 直接传平移 旋转。 第二种 只传平移默认旋转为单位阵。 第三种 从 4×4 齐次变换矩阵中取旋转和平移。这些设置不会立刻改变滤波器状态真正写入state_ikfom的位置在初始化阶段init_state.offset_T_L_I Lidar_T_wrt_IMU; init_state.offset_R_L_I Lidar_R_wrt_IMU;6IMU_init()启动时到底初始化什么初始化函数的目标源码注释已经写得很明确/** 1. initializing the gravity, gyro bias, ** acc and gyro covariance ** 2. normalize the acceleration measurements ** to unit gravity **/它做四类事情1统计平均加速度。 2统计平均角速度。 3根据平均加速度初始化重力方向。 4根据平均角速度初始化陀螺仪零偏。初始化阶段隐含前提是机器人启动后应静止至少不能有明显持续加速、持续旋转或强烈震动。否则平均加速度里会混进真实线加速度平均角速度里会混进真实转动导致重力与陀螺仪偏置初值不可靠。6.1 第一帧初始化第一次进入IMU_init()时if (b_first_frame_) { Reset(); N 1; b_first_frame_ false; const auto imu_acc meas.imu.front()-linear_acceleration; const auto gyr_acc meas.imu.front()-angular_velocity; mean_acc imu_acc.x, imu_acc.y, imu_acc.z; mean_gyr gyr_acc.x, gyr_acc.y, gyr_acc.z; first_lidar_time meas.lidar_beg_time; }这里的作用是第一条 IMU 作为均值统计的起点。 first_lidar_time 记录第一帧 LiDAR 时间。 主要用于调试或输出日志。之后遍历当前meas.imu中所有 IMUfor (const auto imu : meas.imu) { ... }6.2 平均加速度、平均角速度怎么更新代码使用在线均值mean_acc (cur_acc - mean_acc) / N; mean_gyr (cur_gyr - mean_gyr) / N;数学形式μ_N μ_(N-1) (x_N - μ_(N-1)) / N变量 μ_(N-1) 前 N-1 个 IMU 样本的均值。 x_N 当前新增 IMU 样本。 μ_N 更新后的均值。对于本代码mean_acc 启动阶段加速度计平均测量。 mean_gyr 启动阶段陀螺仪平均测量。这样不需要保存全部历史 IMU 数据也可以不断更新均值。6.3 初始化重力方向初始化结束前代码写入init_state.grav S2(- mean_acc / mean_acc.norm() * G_m_s2);可理解为^G g_init - mean_acc / ||mean_acc|| · g变量 mean_acc 静止初始化期间的平均加速度计读数。 ||mean_acc|| 其模长。 G_m_s2 标准重力大小通常约 9.81 m/s²。 负号 因为静止时加速度计测到的是与重力方向相反的比力。举例假设 IMU 静止时测得mean_acc ≈ (0, 0, 9.81)则代码会把重力设为grav ≈ (0, 0, -9.81)这意味着世界坐标系中重力向下。这里初始化的是重力方向不是完整的绝对姿态。尤其 yaw 方向不能仅由加速度计确定重力只能约束俯仰和横滚无法决定朝向角。6.4 初始化陀螺仪零偏代码init_state.bg mean_gyr;即b_g_init mean_gyr理由是静止时ω_m ω_true b_g n_g 静止时 ω_true ≈ 0 因此 mean_gyr ≈ b_g如果初始化期间小车正在转弯或人工转动雷达真实角速度会被误认为陀螺仪 bias。结果是后续姿态积分刚开始就会偏LiDAR 匹配可能要花多帧才能把这个错误修回来。6.5 初始化协方差P代码把初始协方差设为单位阵再对部分状态赋较小方差init_P.setIdentity(); init_P(6,6) init_P(7,7) init_P(8,8) 0.00001; init_P(9,9) init_P(10,10) init_P(11,11) 0.00001; init_P(15,15) init_P(16,16) init_P(17,17) 0.0001; init_P(18,18) init_P(19,19) init_P(20,20) 0.001; init_P(21,21) init_P(22,22) 0.00001;按代码索引可理解为0 ~ 2 位置误差。 3 ~ 5 姿态误差。 6 ~ 8 旋转外参误差。 9 ~ 11 平移外参误差。 12 ~ 14 速度误差。 15 ~ 17 陀螺仪 bias 误差。 18 ~ 20 加速度计 bias 误差。 21 ~ 22 重力方向误差。这表达的是位置、姿态、速度 初始不确定性相对较大。 外参 更相信配置值。 gyro bias 静止均值估计后相对有一定信心。 acc bias 也有初始约束但不如 gyro bias 紧。 重力方向 经过均值加速度确定后初始不确定性较小。不过协方差不是“越小越好”。若初始P设置得过小滤波器会过度相信错误初值后续 LiDAR 很难把状态拉回来。6.6 初始化结束条件代码#define MAX_INI_COUNT (10) if (init_iter_num MAX_INI_COUNT) { imu_need_init_ false; }这里的10指的是累计用于初始化统计的 IMU 样本数量不是 10 帧 LiDAR。例如IMU200 Hz LiDAR10 Hz 一帧 LiDAR 大约持续 100 ms 该帧内 IMU 数量大约 20 条这种情况下初始化可能只经过一帧或很少几帧就结束。因此这套初始化更像“快速起步初始化”不是严格长时间静止标定。实车启动时最好给系统留一段静止时间。7初始化结束时噪声的实际处理初始化结束时源码先执行cov_acc * pow( G_m_s2 / mean_acc.norm(), 2 );看起来是在根据平均重力模长调整加速度计噪声。但紧接着又执行cov_acc cov_acc_scale; cov_gyr cov_gyr_scale;所以最终运行时真正使用的cov_acc、cov_gyr主要来自外部配置调用set_acc_cov(...) set_gyr_cov(...)因此要注意启动统计得到的 cov_acc、cov_gyr 并不是最终一定会被使用。 最终是否采用它们 取决于后续是否被 cov_acc_scale、 cov_gyr_scale 覆盖。当前构造函数中并没有显式给cov_acc_scale cov_gyr_scale赋默认值。因此正常运行依赖外部初始化代码调用p_imu-set_acc_cov(...) p_imu-set_gyr_cov(...)若外部没有正确配置初始化结束时将未明确初始化的 scale 值写回cov_acc、cov_gyr会有风险。这是从当前文件可直接看到的配置依赖点。8UndistortPcl()完整去畸变流程初始化完成后Process()调用UndistortPcl(meas, kf_state, *cur_pcl_un_);它不是一上来就逐点变换而是分成三大阶段阶段 A 建立扫描期间的 IMU 连续轨迹。 阶段 B 将状态传播到 LiDAR 精确结束时刻。 阶段 C 对每一个 LiDAR 点 根据自己的 curvature 找到采样时刻 补偿到扫描结束时刻。8.1 为什么把上一帧末尾 IMU 插到当前帧前面代码auto v_imu meas.imu; v_imu.push_front(last_imu_);作用是补齐 LiDAR 扫描开始附近的 IMU 时间连续性。时间轴如下上一帧最后 IMU 本帧 LiDAR 开始 本帧第一条 IMU t0 t_begin t1 ●----------------------|------------------------●当前帧meas.imu可能从t1才开始但去畸变和传播需要知道t_begin附近的运动状态。因此把上一帧保存的last_imu_放到当前帧队首形成连续 IMU 区间。上一帧末尾 IMU ↓ last_imu_ ↓ 插入当前帧 IMU 队首 ↓ 当前扫描开始附近不会缺 IMU 区间这也是last_imu_每帧结束都要更新的原因。8.2 当前 LiDAR 的起止时间常规 LiDARdouble pcl_beg_time meas.lidar_beg_time; double pcl_end_time meas.lidar_end_time;即pcl_beg_time t_begin pcl_end_time t_endMARSIM 有特殊逻辑if (lidar_type MARSIM) { pcl_beg_time last_lidar_end_time_; pcl_end_time meas.lidar_beg_time; }它不把当前点云当成真实扫描内逐点形成的点云而是用相邻 LiDAR 消息之间的时间差进行状态传播。后面的逐点去畸变也会被跳过。MARSIM 传播状态 上一帧 LiDAR 时间 ↓ 当前 LiDAR 消息时间 逐点补偿 跳过这隐含假设是仿真每帧点云是瞬时快照不存在真实机械 LiDAR 的扫描内畸变。8.3 为什么要按curvature排序代码pcl_out *(meas.lidar); sort( pcl_out.points.begin(), pcl_out.points.end(), time_list );比较器是const bool time_list( PointType x, PointType y) { return (x.curvature y.curvature); };curvature在这里不是几何曲率而是当前点相对扫描开始的时间偏移单位毫秒。第 i 个点真实采样时刻 t_i t_begin curvature_i / 1000排序后点云开始部分 curvature ≈ 0 ms 点云中间部分 curvature ≈ Tscan / 2 点云末尾部分 curvature ≈ Tscan后面代码从点云尾部倒序处理正好对应从扫描结束点回到扫描开始点。9IMU 前向传播扫描期间轨迹如何生成进入UndistortPcl()后先保存当前状态state_ikfom imu_state kf_state.get_x();然后建立IMUposeIMUpose.clear(); IMUpose.push_back( set_pose6d( 0.0, acc_s_last, angvel_last, imu_state.vel, imu_state.pos, imu_state.rot.toRotationMatrix() ) );IMUpose可以理解为扫描期间的离散轨迹表IMUpose[k] offset_time 该轨迹节点相对扫描开始的时间。 rot 该时刻 IMU 世界姿态。 pos 该时刻 IMU 世界位置。 vel 该时刻 IMU 世界速度。 acc 世界坐标系下的线加速度。 gyr 去掉 gyro bias 后的角速度。这张表后面用于“某个 LiDAR 点在第几毫秒采到就查询或插值得到该时刻 IMU 位姿”。9.1 两条相邻 IMU 如何构成一个传播区间代码遍历for ( auto it_imu v_imu.begin(); it_imu (v_imu.end() - 1); it_imu )每次拿一对head当前区间开始 IMU tail当前区间结束 IMUhead tail t_k ----------------------------- t_k1对两端 IMU 取中值angvel_avr 0.5 * ( head-angular_velocity.x tail-angular_velocity.x ), ...; acc_avr 0.5 * ( head-linear_acceleration.x tail-linear_acceleration.x ), ...;数学形式ω_avg (ω_k ω_k1) / 2 a_avg (a_k a_k1) / 2变量 ω_k、ω_k1 相邻两条 IMU 的角速度测量。 a_k、a_k1 相邻两条 IMU 的加速度计测量。 ω_avg、a_avg 当前短时间区间内使用的平均量。这是中值积分近似。它假设一个极短 IMU 区间内角速度和加速度不会剧烈变化。9.2 为什么要做加速度模长缩放代码acc_avr acc_avr * G_m_s2 / mean_acc.norm();其作用是将当前加速度计读数按初始化阶段的平均重力模长缩放 尽量使静止时加速度模长接近标准重力大小。公式可写为a_scaled a_raw · g / ||mean_acc||变量 a_raw 当前 IMU 原始加速度读数。 mean_acc 初始化阶段平均加速度。 g 标准重力大小。 a_scaled 经过尺度归一化后的加速度计读数。这里不能把a_scaled直接理解为世界系车辆加速度。它仍然是 IMU 坐标系下的比力。后面状态传播时才会通过姿态、bias 和重力变成世界系线加速度。9.3 IMU 的连续状态方程从滤波意义看kf_state.predict(dt, Q, in)使用的核心模型可理解为p_dot v R_dot R · [ω_m - b_g - n_g]_x v_dot R · (a_m - b_a - n_a) g变量 p IMU 世界位置。 v IMU 世界速度。 R IMU 到世界坐标系的旋转。 ω_m 陀螺仪测得角速度。 a_m 加速度计测得比力。 b_g gyro bias。 b_a acc bias。 g 世界系重力。 n_g、n_a IMU 白噪声。离散化后可近似理解为姿态传播 R_(k1) R_k · Exp( (ω_m - b_g) · dt )速度传播 v_(k1) v_k [ R_k · (a_m - b_a) g ] · dt位置传播 p_(k1) p_k v_k · dt 0.5 · [ R_k · (a_m - b_a) g ] · dt²真实代码由 IKFoM 内部状态模型执行当前文件负责提供dt、IMU 输入in和过程噪声Q。9.4 过程噪声Q怎么写入每个 IMU 区间都写Q.block3, 3(0, 0).diagonal() cov_gyr; Q.block3, 3(3, 3).diagonal() cov_acc; Q.block3, 3(6, 6).diagonal() cov_bias_gyr; Q.block3, 3(9, 9).diagonal() cov_bias_acc;即Q diag( cov_gyr, cov_acc, cov_bias_gyr, cov_bias_acc )变量 cov_gyr 陀螺仪测量噪声方差。 cov_acc 加速度计测量噪声方差。 cov_bias_gyr 陀螺仪零偏随机游走方差。 cov_bias_acc 加速度计零偏随机游走方差。随后kf_state.predict(dt, Q, in);不仅更新状态也更新协方差P_(k1) F_k · P_k · F_k^T G_k · Q_k · G_k^T变量 P_k 当前时刻状态协方差。 F_k 状态转移 Jacobian。 G_k 过程噪声 Jacobian。 Q_k IMU 过程噪声协方差。 P_(k1) 传播后的协方差。直观上连续只靠 IMU 传播时位置、姿态、速度和 bias 的不确定性会不断增加这就是之后 LiDAR 匹配必须介入修正的原因。10为什么要跳过已经处理过的旧 IMU代码if ( tail-header.stamp.toSec() last_lidar_end_time_ ) { continue; }以及if ( head-header.stamp.toSec() last_lidar_end_time_ ) { dt tail-header.stamp.toSec() - last_lidar_end_time_; } else { dt tail-header.stamp.toSec() - head-header.stamp.toSec(); }原因是上一帧已经传播到last_lidar_end_time_当前帧队首又插入了上一帧末尾 IMU因此新旧帧之间可能有时间重叠。上一帧已经积分过 ------|------------------ last_lidar_end_time 当前 v_imu old imu --- head --- tail --- ...若tail仍然在上一帧结束之前说明这一整段已经被积分过必须跳过。若head last_lidar_end_time tail则只积分尚未处理的后半段dt t_tail - last_lidar_end_time这避免重复积分否则位置、速度和姿态会被重复推进。11每个 IMU 时刻为什么都保存到IMUpose每次predict()后代码执行imu_state kf_state.get_x(); angvel_last angvel_avr - imu_state.bg; acc_s_last imu_state.rot * (acc_avr - imu_state.ba); for(int i 0; i 3; i) { acc_s_last[i] imu_state.grav[i]; } double offs_t tail-header.stamp.toSec() - pcl_beg_time; IMUpose.push_back( set_pose6d( offs_t, acc_s_last, angvel_last, imu_state.vel, imu_state.pos, imu_state.rot.toRotationMatrix() ) );这一步保存的是当前 IMU 时刻的 姿态 位置 速度 世界系加速度 去偏角速度 相对扫描开始时间后续 LiDAR 点很多例如一帧 10 万点系统不可能为每个点都从扫描开始重新积分所有 IMU。正确做法是先为每个 IMU 时刻保存一组离散状态 ↓ 点 i 根据自己的 curvature 找到它所在的 IMU 时间段 ↓ 在这一小段内插值 / 短时间传播这就是IMUpose的作用。12传播到精确扫描结束时刻当前meas.imu中最后一条 IMU 通常略早于pcl_end_time。源码double note pcl_end_time imu_end_time ? 1.0 : -1.0; dt note * (pcl_end_time - imu_end_time); kf_state.predict(dt, Q, in);正常情况下imu_end_time ≤ pcl_end_time所以dt pcl_end_time - imu_end_time随后用最后一个 IMU 区间的输入继续传播到精确扫描结束时刻。最后一条取出的 IMU t_imu_last 10.095 s 扫描结束 t_end 10.0987 s 补传播 dt 0.0037 s最终得到^G R_I(t_end) ^G p_I(t_end) ^G v_I(t_end) P^-这就是后续 LiDAR IESKF 优化前的先验状态。注意代码中note会使dt始终非负。正常sync_packages()设计下最后取出的 IMU 时间不应晚于扫描结束因此它通常等价于正向补传播。若未来代码错误地把晚于t_end的 IMU 放进meas.imu这里就不是严格的“回退传播”而会得到不符合物理语义的正时间预测因此sync_packages()的时间边界必须保持正确。13逐点去畸变真正如何补偿每个 LiDAR 点前面只是建立了扫描期间 IMU 轨迹并把状态推进到扫描结束时刻。真正逐点去畸变从这里开始if(lidar_type ! MARSIM) { auto it_pcl pcl_out.points.end() - 1; for ( auto it_kp IMUpose.end() - 1; it_kp ! IMUpose.begin(); it_kp-- ) { ... } }它从最后一个 LiDAR 点开始倒序处理也从最后一个 IMU 轨迹节点开始倒序处理。原因是点云已按 curvature 从小到大排序。 最后一个 LiDAR 点 最接近扫描结束时刻。 最后一个 IMUpose 最接近扫描结束时刻。 因此从后往前处理 可以快速让每个点进入对应 IMU 时间区间。13.1 一个 LiDAR 点如何找到对应 IMU 区间对某个点t_point_offset curvature / 1000代码判断for( ; it_pcl-curvature / double(1000) head-offset_time; it_pcl-- )可以理解为当前点 head.offset_time t_point_offset ≤ tail.offset_time即该点位于当前 IMU 小区间IMU head 点 i IMU tail t_h --------------------- t_i ------------------- t_t这时head 作为当前点插值的起始状态。 tail 提供该时间段内的加速度、角速度近似值。13.2 点采样时刻姿态怎么得到代码dt it_pcl-curvature / double(1000) - head-offset_time; M3D R_i( R_imu * Exp(angvel_avr, dt) );对应公式^G R_I(t_i) ^G R_I(t_h) · Exp( ω · Δt_i )变量 ^G R_I(t_h) 当前 IMU 区间起点姿态。 ω 代码保存的去偏角速度。 Δt_i 点时刻相对区间起点的时间。 ^G R_I(t_i) 第 i 个点采样时刻的 IMU 世界姿态。这里使用的是短时间近似一个 IMU 小区间内认为角速度近似恒定。13.3 点采样时刻位置怎么得到代码V3D T_ei( pos_imu vel_imu * dt 0.5 * acc_imu * dt * dt - imu_state.pos );前半部分是点时刻的位置估计^G p_I(t_i) ≈ ^G p_I(t_h) ^G v_I(t_h) · Δt_i 0.5 · ^G a_I · Δt_i²再减去扫描结束位置T_ei ^G p_I(t_i) - ^G p_I(t_e)变量 ^G p_I(t_i) 点采样时刻 IMU 世界位置。 ^G p_I(t_e) 扫描结束时刻 IMU 世界位置。 T_ei 点时刻 IMU 原点相对结束时刻 IMU 原点的位移 表达在世界坐标系中。这一步补偿的是扫描期间的平移。如果只补姿态、不补位置车辆前进时墙面仍会拉宽、近处货架仍会重影。13.4 完整去畸变变换核心代码V3D P_compensate imu_state.offset_R_L_I.conjugate() * ( imu_state.rot.conjugate() * ( R_i * ( imu_state.offset_R_L_I * P_i imu_state.offset_T_L_I ) T_ei ) - imu_state.offset_T_L_I );它实际完成的是下面四次坐标变换。第 1 步LiDAR_ti → IMU_ti ^I p_i ^I R_L · ^L_ti p_i ^I t_L第 2 步IMU_ti → 世界系 ^G p_i ^G R_I(t_i) · ^I p_i ^G p_I(t_i)第 3 步世界系 → IMU_te ^I_te p_i (^G R_I(t_e))^T · ( ^G p_i - ^G p_I(t_e) )第 4 步IMU_te → LiDAR_te ^L_te p_i (^I R_L)^T · ( ^I_te p_i - ^I t_L )合并后^L_te p_i (^I R_L)^T · [ (^G R_I(t_e))^T · ( ^G R_I(t_i) · ( ^I R_L · ^L_ti p_i ^I t_L ) ^G p_I(t_i) - ^G p_I(t_e) ) - ^I t_L ]变量 ^L_ti p_i 第 i 个点原始坐标 位于其实际采样时刻 ti 的 LiDAR 系。 ^L_te p_i 去畸变后坐标 统一到扫描结束时刻 te 的 LiDAR 系。 ^I R_L、^I t_L LiDAR-IMU 外参。 ^G R_I(t_i)、^G p_I(t_i) 点采样时刻 IMU 位姿。 ^G R_I(t_e)、^G p_I(t_e) 扫描结束时刻 IMU 位姿。最终it_pcl-x P_compensate(0); it_pcl-y P_compensate(1); it_pcl-z P_compensate(2);这一帧所有点都会统一表达在扫描结束 LiDAR 坐标系。14源码注释“只用旋转”为什么不准确代码旁边有注释/* Transform to the end frame, using only the rotation */但从实际计算看代码并不是只用旋转。它显式使用R_i 点采样时刻姿态。 imu_state.rot.conjugate() 扫描结束时刻逆姿态。 T_ei 扫描期间平移补偿。 offset_T_L_I LiDAR-IMU 杆臂平移外参。所以真实补偿包含扫描期间旋转补偿 扫描期间平移补偿 LiDAR-IMU 杆臂补偿源码末尾还写了// not accurate!这是作者对近似模型的提醒。近似来源包括1IMU 小区间内角速度近似恒定。 2IMU 小区间内世界系加速度近似恒定。 3点时刻位置使用二阶运动学公式。 4每个点不重新跑完整 IESKF 传播 而是在附近 IMU 轨迹节点间做短时间估计。在低速、IMU 高频、时间同步准确时这种近似通常足够急转、急加速、剧烈震动、外参误差或时间偏差较大时去畸变误差会明显变大。15没有逐点时间、没有 IMU、MARSIM 三种情况15.1 有 IMU但点云没有逐点时间本文件不会帮你补时间。它只读取it_pcl-curvature / double(1000)所以进入这里之前preprocess.cpp必须已经写好curvature_i 第 i 个点相对扫描开始的时间单位 ms机械 LiDAR 若没有原始point.time可在前处理阶段用每条 ring 的首点 yaw 当前点 yaw SCAN_RATE估计时间。omega_l 0.361 × SCAN_RATE curvature_i (yaw_first - yaw_i) / omega_l15.2 没有 IMU代码if(meas.imu.empty()) {return;};因此没有 IMU ↓ 本帧不初始化 不预测状态 不建立 IMUpose 不逐点去畸变它不会自动切换为 ICP、NDT、纯 LiDAR odometry 或匀速去畸变。15.3 MARSIMMARSIM 分支仍进行 IMU 状态传播。 但不进行逐点 LiDAR 坐标补偿。前提是仿真点云为瞬时快照。如果仿真点云实际上模拟了扫描过程而curvature又全为 0车辆运动时仍会出现扫描内畸变。16当前代码中值得特别注意的实现细节下面是从你贴出的这份文件中能直接看出的注意点。1last_lidar_end_time_ 在可见构造函数与 Reset() 中没有显式赋值。 它后面参与 tail_time last_lidar_end_time_ 以及 pcl_beg_time last_lidar_end_time_ 若对象不是静态零初始化、或外部未先写入该值 首帧正常传播前可能存在未定义初值风险。2acc_s_last 在可见构造函数和 Reset() 中也没有明确置零。 但首次正常 UndistortPcl() 会把它放入 IMUpose.push_back( set_pose6d(0.0, acc_s_last, ...) ) 若没有其他位置提前设置它 第一帧的初始轨迹节点可能带入未初始化数据。3cov_acc_scale、cov_gyr_scale 没有在构造函数内显示初始化。 初始化完成后 cov_acc cov_acc_scale; cov_gyr cov_gyr_scale; 因此外层必须调用 set_acc_cov() set_gyr_cov() 否则运行噪声参数可能不可靠。4点云中 curvature 0 的点不会进入最后一段补偿循环。 循环条件是 curvature / 1000 head-offset_time 第一段 head-offset_time 通常为 0。 因此 curvature 恰好为 0 的点不会被变换 仍保留扫描开始时刻 LiDAR 坐标。 若这种点数量很多且机器人扫描期间运动明显 这是需要关注的边界行为。5Reset(double start_timestamp, lastimu) 在类中声明 但你贴出的内容中没有看到对应实现。 若工程其他文件也没有实现、且外部调用它 会产生链接错误。 是否实际有问题 还需要查完整工程调用关系。6MARSIM 的时间语义与普通 LiDAR 不同。 普通 LiDAR 扫描时间由 t_begin 到 t_end。 MARSIM 传播时间改为上一帧结束到当前帧开始 并且不做逐点补偿。 不能把普通雷达的时间逻辑直接套到 MARSIM。总结src/IMU_Processing.hpp的本质不是“读 IMU 然后算一个位姿”而是建立一套完整的扫描内连续运动模型。LiDAR 一帧点云不是同一时刻形成的10 Hz 雷达的一帧通常跨越约 100 ms扫描开始的点和扫描结束的点之间机器人可能已经旋转、平移、加速LiDAR 原点本身的位置和朝向都变了。若把这些点直接视为同一时刻点云送去匹配静止时可能看不出问题但车辆一转弯、加速、经过颠簸区域墙面会变厚、柱子会出现双边、直线会弯曲后面的 ikd-Tree 搜索和 IESKF 也只能建立在已经畸变的点云上。这份文件先在启动阶段做快速 IMU 初始化。它用静止期的平均加速度估计重力方向用平均角速度估计陀螺仪零偏并初始化外参、噪声与状态协方差。初始化过程隐含“设备应相对静止”的前提因为一旦小车刚启动就加速或旋转真实运动会混入平均值导致系统把真实角速度误当成 bias把真实线加速度混入重力估计。代码中的MAX_INI_COUNT10统计的是 IMU 样本数量不是 10 帧 LiDAR对于高频 IMU这一过程实际可能非常快所以工程上最好让雷达和 IMU 上电后静止一小段时间。初始化完成后UndistortPcl()才进入正常工作。它先把上一帧最后一条 IMU 放进当前帧 IMU 队列前端避免 LiDAR 扫描开始附近没有可用 IMU 区间接着按curvature对当前点云排序。这里的curvature不是传统意义上的点云几何曲率而是 preprocess.cpp 写入的“点相对扫描开始时刻的时间偏移”单位通常是毫秒。每个点的真实采样时间由t_begin curvature / 1000得到。没有这个逐点时间IMU 虽然知道小车怎么运动却不知道每一个 LiDAR 点具体是在扫描的第几毫秒采到因此也无法做严格逐点去畸变。随后系统将扫描期间相邻 IMU 组成小时间段对每一段取角速度和加速度中值调用kf_state.predict()传播位置、姿态、速度、bias、重力和协方差。每传播到一个 IMU 时刻代码都会把该时刻的姿态、位置、速度、世界加速度和去偏角速度保存到IMUpose。这相当于建立了一条离散的扫描内 IMU 轨迹。当前帧最后一条被取出的 IMU 通常早于 LiDAR 精确结束时间因此代码会继续用最后一段 IMU 输入短时间预测到pcl_end_time得到 LiDAR 匹配前的先验状态x^-和预测协方差P^-。真正逐点去畸变时代码从扫描末尾点开始倒序遍历。对于每个点它先根据curvature找到对应的 IMU 区间然后使用该区间起始姿态、速度、位置加上短时间的恒角速度、恒加速度近似估计点采样时刻的 IMU 姿态和位置。接下来点先从采样时刻 LiDAR 坐标系通过外参变到采样时刻 IMU 坐标系再变到世界坐标系再转换到扫描结束时刻 IMU 坐标系最后通过外参回到扫描结束时刻 LiDAR 坐标系。这样整帧点云最终都变成“扫描结束瞬间 LiDAR 所看到的环境”后面的laserMapping.cpp就能把它当作近似同步点云来做体素降采样、ikd-Tree 最近邻搜索、局部平面拟合和 IESKF 优化。从系统职责上看IMU 在 FAST-LIO 中提供的是高频连续运动信息但它会随时间漂移LiDAR 提供的是低频但几何约束强的环境信息用于反向修正 IMU 漂移。IMU_Processing.hpp的输出正是二者之间的桥梁它输出一帧时空统一的去畸变点云以及扫描结束时刻的 IMU 预测状态。后续 LiDAR 匹配不是替代 IMU而是在 IMU 提供的先验上进行几何修正。只要逐点时间、LiDAR-IMU 时间同步、外参和噪声模型其中一项严重错误去畸变就会先出问题进而影响后面的匹配、优化和地图质量。