历史快照
本专题页写于包拆分与 FAST-LIVO2 / CustomMsg / 图像输入落地之前,反映当时的单包结构与
五前端状态。已知过时点:目录布局(apps/launch 现属 asuka_ros 包)、共享的
config_sensor.json(外参已改为各前端 odometry 配置的 sensor 块)、ROS 层 acc_scale
等描述、前端数量(现六种,新增 fastlivo)、CustomMsg 与图像输入链路。2026-09 下旬以来:lightning / superodom 前端与 optimization_miao 后端已移除
(现五前端:fastlio / batchlio / superlio / fastlivo / srlivo;smallpointlio 已于 2026-10-23 移除,odometry/10-11 为其历史快照);
freedom 扩展已整体迁出 asuka(移入 nagisa 包);KeyFrame::stamp 统一为扫描
起始时刻;执行器新增连续失败熔断与 wait_for() 排空检测、扩展与执行器有
ok() 健康查询。
现行事实以主文档 (index / architecture / parameters / odometry 等)与源码为准。
title: "SmallPointLIO 预处理与插件注册" description: >- 点云预处理(6.4KB,含特征处理)与极简 IMU 预处理(仅 0.7KB)形成鲜明对比,体现该实现把计算重心放在滤波与配准上的取舍;create.cpp 完成导出。
SmallPointLIO 预处理与插件注册
模块定位与文件构成
SmallPointLIO 子目录的预处理层由三份源文件构成,与其余四种算法实现保持同构布局:
| 文件 | 体量 | 职责 |
|---|---|---|
cloud_preprocess.cpp / .hpp |
6.4KB / 1.4KB | 点云过滤、序号抽稀、可选体素降采样、按时间重排 |
imu_preprocess.cpp / .hpp |
0.7KB / 0.6KB | IMU 循环缓冲 + 时间回退丢弃 |
create.cpp |
193B | extern "C" 工厂,完成插件导出 |
(odometry_estimation.cpp 15KB、eskf.hpp 8KB、so3.hpp 1.4KB 属主流程与滤波,见另页。)
本页最醒目的特征是体量对比:点云预处理 6.4KB,是全仓库各实现中较重的一份;IMU 预处理仅 0.7KB,是所有实现中最轻的。这个不对称并非缺失,而是刻意取舍——该实现把 IMU 积分、前向传播等职责上移到自带的 ESKF(eskf.hpp,目录内最大头文件)与 odometry_estimation.cpp 主循环中,预处理层只负责"数据整形 + 防御性检查"。对比 FastLIO 那份 8.7KB、承担前向传播与时间同步的 IMU 预处理,可以直观看到计算重心被放到了滤波与配准一侧。
点云预处理:CloudPreprocess
CloudPreprocess 继承公共抽象基类 asuka::CloudPreprocess,实现 Livox 与 Robosense 两个 preprocess 重载,二者都委托给私有的 build 重载。构造函数从 GlobalConfig::get_config_path("config_odometry") 解析出的 JSON 中读取 "preprocess" 段参数——即与选择 so_name 的是同一份配置文件,算法参数与实现选择天然绑定。
参数面
参数(preprocess 段) |
默认值 | 作用 |
|---|---|---|
min_distance |
0.5 | 近距剔除阈值(米),以平方形式比较 |
max_distance |
100.0 | 远距剔除阈值(米) |
point_filter_num |
1 | 每 N 个点取 1 个的序号抽稀,构造时钳制为 ≥1 |
space_downsample |
false |
是否启用体素降采样 |
space_downsample_leaf_size |
0.5 | 体素边长(米) |
处理流水线
两个 build 重载共享同一骨架,唯一差异是 Livox 分支多一道 tag 检查:(pt.tag & 0b00111111) != 0 的点被剔除,即按 Livox 点属性位(低 6 位)过滤;Robosense 分支无此步骤。共同步骤为:
- 以第 0 个点的时间戳为帧内基准
timebase,逐点换算相对时间(pt.timestamp - timebase) * 1e-6——原始时间戳按微秒解释,输出为秒。注意接口形参stamp在build可见代码中并未参与计算,仅作为接口签名透传,真正的时间基准来自点云自身; - 剔除非有限坐标(
allFinite())与距离窗外(squaredNorm对平方阈值,避免开方)的点; - 可选
VoxelgridSampling空间降采样,否则整段std::move; - 无条件按相对时间升序
std::sort——这一步对降采样路径尤其关键,因为体素输出序是键序而非时间序; - 填充输出点:
intensity强制为 0,offset写入逐点相对时间。这是该实现与主流程的核心契约——几何上不做边缘/平面特征提取,"特征"体现为offset承载的逐点时间,供下游 ESKF 配准与去畸变使用。
VoxelgridSampling 实现要点
自研体素降采样(非 PCL 依赖):
fast_floor用"cast + 比较修正"替代std::floor,是纯性能手法;- 每轴 21 bit、偏移
1 << 20居中,三轴打包进一个uint64_t体素键(共 63 bit); - 越界坐标标记为
invalid_coord(UINT64_MAX)并在聚簇阶段整点丢弃,而非 clamp; - 排序后相邻同键点归为一簇:位置取质心,时间戳取组内最后累加的点(非均值也非最大值)——后续全局时间排序会按这个时间戳安放质心点;
coord_pt是类成员,但VoxelgridSampling sampler在每次build中临时构造,因此无跨调用共享状态。
IMU 预处理:0.7KB 的极简缓冲
ImuPreprocess 全部逻辑只有两件事:
- 构造:从同一份 config_odometry 的
"odometry"段读imu_buffer_capacity(默认 1000),设定boost::circular_buffer容量——内存有界,满后自动挤掉最旧数据; insert_imu:检测imu->stamp < last_timestamp_imu则打 error 日志并丢弃该帧(时间回退防御,与 ROS 层handle_loop_back构成双保险,相等时间戳允许通过);否则入队并更新last_timestamp_imu(初始 -1.0)。
值得注意的是 imu_deque 是 public 成员:没有额外的消费接口,主流程(odometry_estimation.cpp,另页)直接读取该缓冲。积分、去偏置、前向传播统统不在这里——这正是 0.7KB 与 8.7KB 差距的根源。
预处理流水线总览
flowchart TD
A["ROS 层 insert_frame<br/>(已按 lidar_type 转为 PCL)"] --> B["CloudPreprocess::preprocess<br/>分派到 build 重载"]
B --> C{"点类型?"}
C -->|LivoxPoint| D["tag 低 6 位检查<br/>非零即剔除"]
C -->|RobosensePoint| E["无 tag 检查"]
D --> F["序号抽稀<br/>i % point_filter_num"]
E --> F
F --> G{"allFinite 且<br/>min² ≤ ‖p‖² ≤ max² ?"}
G -->|不通过| H["丢弃该点"]
G -->|通过| I["换算相对时间<br/>(t − t₀) × 1e-6 → 秒"]
I --> J{"space_downsample ?"}
J -->|是| K["VoxelgridSampling<br/>21bit 体素键 → 排序聚簇 → 质心"]
J -->|否| L["整段保留"]
K --> M["按相对时间升序排序"]
L --> M
M --> N["输出 PointCloudT<br/>offset = 相对时间, intensity = 0"]
N --> O["写入异步环形缓冲<br/>由 process_once 消费"]
关键节点说明:
- C(点类型分派):对应 ROS 层
lidar_type分发后的第二级重载分派,两条路径仅在 tag 检查处分叉,其余逻辑逐行对称; - G(距离与有效性窗):距离用平方比较是热路径优化,
allFinite防御 NaN/Inf 污染后续排序与配准; - J(降采样开关):两条密度路线(抽稀保留原始密度 vs 体素质心降密度)都收敛到 M 的时间排序,保证
offset单调递增是无论开关与否都成立的不变量; - N(输出契约):
offset字段承载逐点相对时间是本实现的约定,主流程据此做扫描内时间传播; - O(队列边界):预处理成功是写入异步队列的前提,预处理层与异步执行器在此交接。
create.cpp:六行完成插件导出
extern "C" asuka::OdometryEstimation* create_odometry_estimation() {
return new asuka::smallpointlio::OdometryEstimation();
}
全部 193 字节只做一件事:以 extern "C" 规避 C++ 名字修饰,使 dlopen 后 dlsym("create_odometry_estimation") 可解析,返回裸 new 出的实例(所有权由基类 load_module 侧的智能指针接管,见插件契约页)。OdometryEstimation 构造时会连带创建 CloudPreprocess/ImuPreprocess,二者各自读取 config_odometry——预处理参数因此随插件加载而生效。这份文件与 FastLIO 等其余四种实现完全同构,是"新增算法接入成本恒定"的最小样例。
关键状态一览
ImuPreprocess::last_timestamp_imu(私有,初始 -1.0):单调性守卫,只在insert_imu中推进;ImuPreprocess::imu_deque(public):容量受限的循环缓冲,主流程直接消费;CloudPreprocess的 5 个超参:构造期从 config_odometry 读定,运行期只读;VoxelgridSampling::coord_pt:单次调用内的排序暂存,无跨调用生命周期。
边界条件
- 空点云输入直接返回空输出,后续各阶段均不会触达;
point_filter_num构造时std::max(1, ...)钳制,避免取模为零;- 体素坐标超出 ±2²⁰ 范围的点被整体丢弃而非 clamp,远处稀疏离群点可能因此消失;
- 时间回退的 IMU 帧被丢弃并告警,不会阻塞后续正常数据;
- Livox tag 检查只看低 6 位,若更换 Livox 型号需确认 tag 位语义是否一致;
- 帧内时间基准取第 0 个点的
timestamp,隐含"首点时间有效且点云整体按时间大致有序"的假设; - 降采样质心的时间戳取组内最后累加点,属近似语义,依赖后续全局排序兜底。
扩展点
- 调参不改码:
preprocess段 5 个参数加imu_buffer_capacity覆盖密度、距离窗与缓冲深度; - 新增雷达类型:为
CloudPreprocess增加一对build/preprocess重载,并同步公共抽象基类; - 替换降采样策略:
VoxelgridSampling与主流程仅通过RawPoint向量交互,可整体替换; - 调整 tag 掩码:
0b00111111是 Livox 分支唯一的传感器语义常量; - 接入新算法:复制这份六行
create.cpp模式即可满足框架的 dlopen 契约。
Sources:
- src/asuka/src/asuka/odometry/smallpointlio/cloud_preprocess.cpp#L1-L178
- src/asuka/src/asuka/odometry/smallpointlio/imu_preprocess.cpp#L1-L27
- src/asuka/src/asuka/odometry/smallpointlio/create.cpp#L1-L6
- src/asuka/include/asuka/odometry/smallpointlio/cloud_preprocess.hpp#L1-L56
- src/asuka/include/asuka/odometry/smallpointlio/imu_preprocess.hpp#L1-L29
关联源码
src/asuka/src/asuka/odometry/smallpointlio/cloud_preprocess.cppsrc/asuka/src/asuka/odometry/smallpointlio/imu_preprocess.cppsrc/asuka/src/asuka/odometry/smallpointlio/create.cpp