历史快照
本专题页写于包拆分与 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: "ikd_tree 增量 KD 树" description: >- 增量式 KD 树(61.3KB cpp + 头文件),为 FastLIO 系算法提供动态点云地图的增删与近邻查询结构。
ikd_tree 增量 KD 树
模块定位与文件构成
src/asuka/thirdparty/ikd_tree 是内置的第三方成果(源码头注释注明作者 Yixi Cai,HKU,"ikd-Tree: an incremental k-d tree for robotic applications"),仅由两个文件构成:
ikd_tree.hpp(8.9KB):KD_TREE<PointType>模板类声明、节点结构、手写堆/队列,以及一组编译期宏常量;ikd_tree.cpp(61.3KB):全部模板实现,单文件承载递归建树、增删、检索、降采样与多线程重建逻辑,是本目录的绝对主体。
它在本仓库中的角色是为 FastLIO 系算法充当动态点云地图容器:上游里程计在状态迭代时用它做 k 近邻查询(点面配准的数据关联),在 IMU 预测后用它增量写入新点、按盒删除滑动窗口外的旧点。两处仓库适配痕迹值得注意:cpp 顶部 #include <asuka/core/types.hpp> 打通了与仓库公共点类型的实例化链路;头文件中注释掉的 pcl::PointXYZINormal 别名说明点类型由使用方以模板参数注入,结构本身不绑定具体点云格式。
核心数据结构
KD_TREE_NODE(hpp L44-66):除点值与division_axis(分割轴)外,关键是三类字段——- 子树统计:
TreeSize、invalid_point_num(已删除点数)、down_del_num(降采样删除数); - 懒删除标记四元组:
point_deleted / tree_deleted / point_downsample_deleted / tree_downsample_deleted,配合need_push_down_to_left/right延迟下推; - 平衡与几何:
alpha_bal / alpha_del(重建准则输入)、node_range_x/y/z子树包围盒(剪枝用)、每节点一把push_down_mutex_lock。 Operation_Logger_Type+operation_set:六种操作(ADD_POINT / DELETE_POINT / DELETE_BOX / ADD_BOX / DOWNSAMPLE_DELETE / PUSH_DOWN)的日志单元,是重建期间主线程与重建线程之间的契约。MANUAL_HEAP:基于数组的二叉堆,元素PointType_CMP携带距离,容量写满后push直接丢弃(hpp L111)。MANUAL_Q:容量Q_LEN=1000000的环形队列,专用于Rebuild_Logger;实现上push未见满溢检查,仅靠tail %= Q_LEN回绕,依赖容量足够大。BoxPointType:轴向包围盒(vertex_min/max[3]),盒删除、盒搜索、降采样体素共用。
公开接口面(调用链入口)
| 公开接口 | 内部递归实现 | 用途 |
|---|---|---|
Build |
BuildTree + Update |
全量建树;先删旧树,经哑根 STATIC_ROOT_NODE 挂出 Root_Node(cpp L344-355) |
Add_Points |
Add_by_point / Search_by_range / Delete_by_range |
增量加点,可选体素降采样 |
Delete_Points / Delete_Point_Boxes |
Delete_by_point / Delete_by_range |
按点 / 按盒懒删除 |
Nearest_Search |
Search |
k 近邻(手动堆容量 2k),FastLIO 配准核心依赖 |
Box_Search / Radius_Search |
Search_by_range / Search_by_radius |
区域/半径检索 |
flatten + acquire_removed_points |
— | 按 NOT_RECORD / DELETE_POINTS_REC / MULTI_THREAD_REC 三种模式回收被删点 |
reconstruct、size、validnum、root_alpha、tree_range |
— | 整树重建与状态查询(重建期有降级语义,见下) |
写路径:增量插入与体素降采样
Add_Points(cpp L407 起)是最能体现本结构设计意图的函数,每处理一个点都要走一次"重建分支判断":
downsample_switch = downsample_on && DOWNSAMPLE_SWITCH——宏是全局总开关;- 开启降采样时:把点折算到
downsample_size体素盒(floor(x/size)*size),Search_by_range收集盒内已有点,选离盒中心mid_point最近者为幸存点; - 仅当"盒内点数 > 1 或新点即幸存点"时才实际写树:
Delete_by_range(is_downsample=true)清盒 +Add_by_point插入幸存点——即新点只有"取代旧代表"或"盒空"时才落地; - 关键分叉在"是否正在重建":
Rebuild_Ptr为空或非根时,递归函数以allow_rebuild=true直接执行并可触发重建判定;否则在working_flag_mutex保护下以allow_rebuild=false执行,并在rebuild_flag置位时把等价操作(DOWNSAMPLE_DELETE+ADD_POINT)压入Rebuild_Logger(cpp L443-461),由重建线程事后在新子树上一致重放。
懒删除贯穿所有删除路径:删除只置标记并累加 invalid_point_num / down_del_num,物理摘除推迟到重建或 PUSH_DOWN 下推;run_operation 的 PUSH_DOWN 分支(cpp L325-337)展示了标记合并与统计回填的完整语义。validnum = TreeSize - invalid_point_num。
平衡维护与后台重建线程(本模块的并发核心)
树是否失衡由 Criterion_Check 依据节点 alpha_bal / alpha_del 与两个准则参数(默认 0.5 / 0.7)判定,并受宏 Minimal_Unbalanced_Tree_Size=10(小树不折腾)与 ForceRebuildPercentage=0.2 约束;重建规模由 Multi_Thread_Rebuild_Point_Num=1500 控制。与静态 kd-tree 不同,本类构造即启动重建线程(start_thread 初始化 6 把互斥锁并 pthread_create),析构时 stop_thread 置 termination_flag 后 join,再删节点、清存储——线程生命周期与对象严格同构。
flowchart TD
subgraph MT["主线程:上游 FastLIO 系算法"]
API["Add_Points / Delete_Points / Delete_Point_Boxes"] --> CHK{"Rebuild_Ptr 是否指向根?"}
CHK -- "否(未重建)" --> DIRECT["递归 Add_by_point / Delete_by_range<br/>allow_rebuild = true,可触发重建判定"]
CHK -- "是(重建中)" --> LOCKED["working_flag_mutex 下执行<br/>allow_rebuild = false"]
LOCKED --> LOG{"rebuild_flag = true?"}
LOG -- "是" --> PUSH["向 Rebuild_Logger 压入等价操作日志"]
NS["Nearest_Search"] --> SCHK{"重建目标是否为根?"}
SCHK -- "否" --> S1["直接递归 Search"]
SCHK -- "是" --> S2["search_flag_mutex 协议:<br/>等待 counter != -1,计数 +1 后 Search,结束后 -1"]
end
subgraph RT["重建线程 multi_thread_rebuild"]
LOOP["循环:usleep(100) 轮询 termination_flag"] --> TRY["锁 rebuild_ptr_mutex + working_flag_mutex"]
TRY --> SNAP["缓存 Treesize_tmp / Validnum_tmp / alpha 缓存值"]
SNAP --> COPY["整理子树并复制到 Rebuild_PCL_Storage"]
COPY --> REB["BuildTree 重建并替换 *Rebuild_Ptr"]
REB --> REPLAY["run_operation 以 allow_rebuild=false 重放日志"]
end
PUSH -. "重建完成后补写新子树" .-> REPLAY
S2 -. "counter == -1 表示重建要求独占,读者自旋" .-> LOOP
图中关键节点:
Rebuild_Ptr分叉:它是二级指针(指向Root_Node或某个子树指针),主线程与重建线程都通过它判断"我操作的区域是否正在被重建",这是全部双路径逻辑的单一判定点;Rebuild_Logger重放:重建期间主线程的写操作不丢失,而是记日志由重建线程经run_operation(覆盖六种操作)补写到新子树,保证增量语义在重建前后一致;- 读侧两套协议:
size / validnum / root_alpha / tree_range走trylock快速路径,拿不到锁就返回重建线程缓存的*_tmp值(validnum返回哨兵-1、tree_range返回全零盒);Nearest_Search则走search_mutex_counter计数协议——计数器为-1表示重建线程要求独占,读者自旋让路,否则多读者可并行计数进入(cpp L367-381)。
边界条件与风险点
Build收到空点云直接返回,不分配哑根(cpp L348);- 重建窗口内的状态查询是弱一致读:
validnum可能返回-1、tree_range返回零盒,上游必须处理这些哨兵值; MANUAL_HEAP满容量丢元素、MANUAL_Q无满溢检查,分别依赖"容量 2k 足够"与"日志量远小于Q_LEN"的隐含假设;Nearest_Search实际返回min(k_nearest, 堆内结果数)个点,近邻不足 k 时不报错;- 线程收敛依赖 100µs 轮询 +
join,析构顺序(停线程 → 关闭删除记录 → 删节点 → 清存储)保证了无悬挂引用。
扩展点
- 模板点类型:任何含
x/y/z的 PCL 点类型均可实例化,cpp 中的asuka/core/types.hpp引入即为此服务; - 三个运行时参数:删除准则、平衡准则、降采样盒长,可经构造函数或
Set_delete_criterion_param / Set_balance_criterion_param / set_downsample_param(InitializeKDTree一次设齐)调整,对应各算法配置文件中的地图分辨率等超参; - 宏级调参:
DOWNSAMPLE_SWITCH(降采样总开关)、Minimal_Unbalanced_Tree_Size、Multi_Thread_Rebuild_Point_Num、ForceRebuildPercentage、Q_LEN提供行为级开关; - 被删点回收:
flatten / acquire_removed_points的三种记录模式让需要感知"地图点被删事件"的上层(裁剪统计、可视化等)取回数据,Delete_Storage_Disabled可整体关闭记录以省内存。
上游 FastLIO 系算法对这组接口的具体调用位置(配准时的近邻查询、预测后的地图更新与滑窗裁剪)属于 FastLIO 主流程页的内容,本页只需记住:所有地图动态性都经由上表接口进入,双路径并发协议全部封装在本模块内部,调用方无感知。
Sources: src/asuka/thirdparty/ikd_tree/ikd_tree.hpp#L1-L283
Sources: src/asuka/thirdparty/ikd_tree/ikd_tree.cpp#L1-L620
关联源码
src/asuka/thirdparty/ikd_tree/ikd_tree.cppsrc/asuka/thirdparty/ikd_tree/ikd_tree.hpp