A-LOAM 系统架构深度解析:解耦设计的艺术
2026/6/13 17:29:29 网站建设 项目流程

前言

A-LOAM 最精妙的设计在于将复杂的激光SLAM问题解耦为两个并行的模块——高频(10Hz)低精度的激光里程计 + 低频(1Hz)高精度的建图优化。这种"快慢搭配"的架构思想后来被 LeGO-LOAM、LIO-SAM、Fast-LIO 等大量工作继承。本文将深入剖析 A-LOAM 的源码结构与数据流。


1. 整体架构总览

┌─────────────────────────────────────────────────────────────────┐ │ A-LOAM 系统架构 │ │ │ │ ┌─────────────────────────┐ │ │ │ scanRegistration │ ← 节点1:特征提取 (10Hz) │ │ │ 曲率计算 → 角点/面点分类 │ │ │ └───────────┬─────────────┘ │ │ │ 发布 topic │ │ ┌────────┼────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────┐ │ │ │ laserOdometry │ ← 节点2:激光里程计 (10Hz) │ │ │ Scan-to-Scan 匹配 │ │ │ │ 点云去畸变 + Ceres优化 │ │ │ └───────────┬─────────────┘ │ │ │ 发布 /laser_cloud_corner_last, │ │ │ /laser_cloud_surf_last, │ │ │ /laser_odom_to_init │ │ ▼ │ │ ┌─────────────────────────┐ │ │ │ laserMapping │ ← 节点3:建图优化 (1Hz) │ │ │ Scan-to-Map 匹配 │ │ │ │ 局部子地图 + Ceres优化 │ │ │ └───────────┬─────────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ ▼ ▼ │ │ /laser_cloud_map /aft_mapped_path │ │ (全局点云地图) (高精度轨迹) │ └─────────────────────────────────────────────────────────────────┘

2. 源码文件结构

A-LOAM 的核心代码不到 2000 行,分布在 5 个文件中:

src/ ├── scanRegistration.cpp (333行) 特征提取节点 ├── laserOdometry.cpp (420行) 激光里程计节点 ├── laserMapping.cpp (567行) 建图优化节点 ├── lidarFactor.hpp (215行) Ceres 残差结构体定义 ├── kittiHelper.cpp (可选) KITTI → ROS bag 转换 └── CMakeLists.txt

3. scanRegistration —— 特征提取节点

3.1 节点职责

输入: /velodyne_points (sensor_msgs::PointCloud2) 输出: /laser_cloud_sharp —— 强角点 (每扫描线最多2个/段) /laser_cloud_less_sharp —— 弱角点 (用于 Mapping) /laser_cloud_flat —— 强面点 (每扫描线最多4个/段) /laser_cloud_less_flat —— 弱面点 (Mapping用,体素降采样) /velodyne_cloud_2 —— 全部点云 (带scanID和时间戳)

3.2 处理流程

原始点云 (sensor_msgs::PointCloud2) │ ▼ [1] 转换为 pcl::PointCloud<PointType> │ ▼ [2] 预处理:去除 NaN 和 距离<0.1m 的噪点 │ ▼ [3] 计算扫描线 ID (scanID) 公式:angle = atan2(z, sqrt(x²+y²)) * 180 / π scanID = (angle + 15) / 2 + 0.5 // VLP-16: -15°~15°, 16线 │ ▼ [4] 计算相对时间戳 ratio (存入 intensity 字段) ratio = (scanID * 水平分辨率 + 水平角度偏移) / 360 │ ▼ [5] 计算曲率(连续8行核心代码) │ ▼ [6] 特征分类 ├── 每条扫描线分为 6 个等份 ├── 每份按曲率从大到小排序 ├── 取曲率最大的 2 个 → 角点 ├── 取曲率最小的 4 个 → 面点 └── 避免选取与已选角点相邻的点(隔离保护) │ ▼ [7] 发布5个话题

3.3 曲率计算公式

c=1∣S∣⋅∥XiL∥∥∑j∈S,j≠i(XiL−XjL)∥c = \frac{1}{|S| \cdot \|X_i^L\|} \left\| \sum_{j \in S, j \neq i} (X_i^L - X_j^L) \right\|c=SXiL1jS,j=i(XiLXjL)

其中SSS是当前点前后各 5 个点的邻域集合(共 10 个点)。

// scanRegistration.cpp 核心代码for(inti=5;i<cloudSize-5;i++){floatdiffX=laserCloud->points[i-5].x+...+laserCloud->points[i+5].x-10*laserCloud->points[i].x;floatdiffY=laserCloud->points[i-5].y+...+laserCloud->points[i+5].y-10*laserCloud->points[i].y;floatdiffZ=laserCloud->points[i-5].z+...+laserCloud->points[i+5].z-10*laserCloud->points[i].z;cloudCurvature[i]=diffX*diffX+diffY*diffY+diffZ*diffZ;}

直观理解:如果某点与周围邻域点的位置差异大 → 曲率大 → 是角点;如果与邻域平滑过渡 → 曲率小 → 是面点。

3.4 特征均匀化 —— 分块策略

一条扫描线被分为6段: ┌────┬────┬────┬────┬────┬────┐ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ └────┴────┴────┴────┴────┴────┘ 每段独立取 top2 角点 + top4 面点 → 确保特征在360°视野中均匀分布 → 避免特征集中导致优化退化

4. laserOdometry —— 激光里程计节点

4.1 节点职责

输入: /laser_cloud_sharp, /laser_cloud_flat, /velodyne_cloud_2 输出: /laser_cloud_corner_last —— 上一帧角点(去畸变后) /laser_cloud_surf_last —— 上一帧面点(去畸变后) /laser_odom_to_init —— 里程计累积位姿 /laser_odom_path —— 里程计轨迹(NavPath) /laser_odom_cloud —— 去畸变后的全部点云

4.2 核心算法:Scan-to-Scan 配准

上一帧特征点 cloudCornerLast / cloudSurfLast + 当前帧特征点 cloudCornerCurr / cloudSurfCurr │ ▼ ┌──────────────┐ │ 建立 KD-Tree │ ← 用上一帧点云构建 └──────┬───────┘ │ ▼ ┌──────────────┐ │ 寻找对应关系 │ │ 角点→找2最近邻 │ (构成直线) │ 面点→找3最近邻 │ (构成平面) └──────┬───────┘ │ ▼ ┌──────────────┐ │ Ceres 优化 │ ← 迭代4次,每次更新后重新找对应 │ 求解 T_k+1 │ └──────────────┘

4.3 点到直线距离(角点残差)

当前帧角点 P_curr │ │ distance ▼ ──────●──────── (由上一帧的2个最近邻角点构成的直线) P_a P_b 残差 = |P_curr 到直线(P_a→P_b)的距离|

dE=∣(X~k+1,i−Xˉk,j)×(X~k+1,i−Xˉk,l)∣∣Xˉk,j−Xˉk,l∣d_{\mathcal{E}} = \frac{|(\tilde{X}_{k+1,i} - \bar{X}_{k,j}) \times (\tilde{X}_{k+1,i} - \bar{X}_{k,l})|}{|\bar{X}_{k,j} - \bar{X}_{k,l}|}dE=Xˉk,jXˉk,l(X~k+1,iXˉk,j)×(X~k+1,iXˉk,l)

4.4 点到平面距离(面点残差)

当前帧面点 P_curr │ │ distance ▼ ┌──────────────┐ (由上一帧的3个最近邻面点构成的平面) │ ●P_l │ │ P_j ●P_m │ └──────────────┘ 残差 = |P_curr 到平面(P_j, P_l, P_m)的距离|

dH=∣(X~k+1,i−Xˉk,j)⋅((Xˉk,j−Xˉk,l)×(Xˉk,j−Xˉk,m))∣∣(Xˉk,j−Xˉk,l)×(Xˉk,j−Xˉk,m)∣d_{\mathcal{H}} = \frac{|(\tilde{X}_{k+1,i} - \bar{X}_{k,j}) \cdot ((\bar{X}_{k,j} - \bar{X}_{k,l}) \times (\bar{X}_{k,j} - \bar{X}_{k,m}))|}{|(\bar{X}_{k,j} - \bar{X}_{k,l}) \times (\bar{X}_{k,j} - \bar{X}_{k,m})|}dH=(Xˉk,jXˉk,l)×(Xˉk,jXˉk,m)(X~k+1,iXˉk,j)((Xˉk,jXˉk,l)×(Xˉk,jXˉk,m))

4.5 点云去畸变

激光雷达在扫描过程中自身也在运动,同一帧内不同时刻采集的点参考坐标系不同。

扫描开始 ←────── 360°旋转 ──────→ 扫描结束 │ │ t_start t_end 问题:t_start 时刻采集的点在 LiDAR 坐标系 L_start, t_end 时刻采集的点在 LiDAR 坐标系 L_end, L_start ≠ L_end(因为雷达在运动) 解决(A-LOAM 的做法): 1. 假设两帧之间匀速运动 2. 已知上一帧到当前帧的相对位姿 T 3. 对每个点,根据其时间戳 ratio,线性插值位姿: T_ratio = interpolate(I, T, ratio) 4. 将所有点变换回扫描开始时刻的坐标系

5. laserMapping —— 建图优化节点

5.1 节点职责

输入: /laser_cloud_corner_last (里程计输出去畸变角点) /laser_cloud_surf_last (里程计输出去畸变面点) /laser_odom_to_init (里程计累积位姿) /velodyne_cloud_2 (全部点云) 输出: /laser_cloud_surround (局部子地图) /laser_cloud_map (全局地图) /aft_mapped_to_init (高精度位姿) /aft_mapped_path (高精度轨迹)

5.2 核心算法:Scan-to-Map 配准

与 Odometry 的 Scan-to-Scan 不同,Mapping 将当前帧与历史累积的局部子地图进行匹配,精度更高但计算量更大。

当前帧特征点 │ ▼ ┌──────────────────┐ │ 提取局部子地图 │ 21×21×11 栅格 (每格50m, 共约10km³) │ 以当前位姿为中心 │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ KD-Tree 寻对应 │ │ 角点→5近邻→SVD │ 判断是否为直线 │ 面点→5近邻→SVD │ 判断是否为平面 └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ Ceres 优化 │ 迭代2次 │ 求解全局位姿 │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ 更新全局地图 │ 每20帧做一次体素降采样 └──────────────────┘

5.3 SVD 验证特征类型

为什么要 SVD?在全局地图中,5个最近邻不一定真的构成直线或平面,需要进行验证。

5个最近邻的协方差矩阵 → SVD分解 → 3个特征值 λ1 ≥ λ2 ≥ λ3 角点验证:λ1 >> λ2 ≈ λ3 → 点分布在一条直线上 (√λ1对应直线方向) 面点验证:λ1 ≈ λ2 >> λ3 → 点分布在一个平面上 (√λ3对应法向量方向) 不满足条件 → 放弃这个匹配

点到直线距离(激光建图中的版本):

dE=∣(XL−pa)×(XL−pc)∣∣pa−pc∣d_{\mathcal{E}} = \frac{\left|(X^L - p_a) \times (X^L - p_c)\right|}{|p_a - p_c|}dE=papc(XLpa)×(XLpc)

其中pap_apapcp_cpc是直线上距离最远的两个点(由 SVD 的方向向量确定)。


6. ROS 通信结构完整拓扑

scanRegistration (10Hz) │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ /laser_cloud_ /laser_cloud_ /velodyne_ sharp / flat less_sharp / cloud_2 less_flat │ │ │ └──────┬───────┘ │ ▼ │ laserOdometry (10Hz) ←───────┘ │ ┌───────────┼───────────┐ ▼ ▼ ▼ /laser_cloud_ /laser_cloud_ /laser_odom _corner_last _surf_last _to_init + path │ │ │ └─────┬─────┘ │ ▼ ▼ laserMapping (1Hz) ←──┘ │ ┌─────┴─────┐ ▼ ▼ /laser_cloud_ /laser_cloud_ _surround _map + /aft_mapped_path

关键设计:里程计10Hz 保证实时性,建图1Hz 保证精度。


7. 局部子地图管理

7.1 栅格结构

// 21×21×11 的立方体栅格intlaserCloudCenWidth=21;// X方向 21 格intlaserCloudCenHeight=21;// Y方向 21 格intlaserCloudCenDepth=11;// Z方向 11 格// 每格边长 50m// → 覆盖范围:±525m (X/Y) × ±275m (Z)

7.2 滑动窗口更新

当 LiDAR 移动到栅格边界时,更新子地图窗口: if (当前位置距中心 > 50m) { 移除远离的边界栅格 添加新前进方向的栅格 重新构建 KD-Tree }

8. A-LOAM 与 ORB-SLAM2/3 架构对比

维度A-LOAMORB-SLAM2/3
传感器3D 激光雷达相机
前端曲率特征提取ORB 特征提取
帧间匹配点到线/点到面距离重投影误差
后端优化Ceres LM (自动求导)g2o LM
优化变量6DoF 位姿6/7DoF 位姿 + 地图点
回环检测DBoW2 词袋模型
地图类型点云 (稠密)稀疏地图点
运行频率10Hz + 1Hz帧率 + 后台
线程模型3个独立ROS节点3个并行线程
IMU 支持ORB-SLAM3 有
代码量~2000行~15000-25000行

9. 总结

A-LOAM 架构的三大设计精髓:

  1. 高频里程计 + 低频建图的解耦设计:里程计 10Hz 保证实时跟踪,建图 1Hz 追求极致精度,两者互不阻塞
  2. Scan-to-Scan → Scan-to-Map 的精度递进:先做快速的帧间匹配获得初始位姿,再做慢速的帧与地图匹配精化位姿
  3. 滑动窗口子地图:21×21×11 的栅格子地图,既避免了全局匹配的计算爆炸,又保留了足够的邻域信息

这套架构直接影响了后续几乎所有激光SLAM系统(LeGO-LOAM 加了地面分割,LIO-SAM 加了IMU和GPS因子,Fast-LIO 用了迭代卡尔曼滤波替代优化),是理解激光SLAM的必经之路。

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

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

立即咨询