04-ALGORITHM_03: 回环检测与位姿图优化 / Loop Closure Detection and Pose Graph Optimization
目录 / Table of Contents
- 回环检测概述 / Loop Closure Overview
- BTC描述子流水线 / BTC Descriptor Pipeline
- 场景检索与验证 / Place Retrieval and Verification
- 法向量ICP验证 / Normal ICP Verification
- 位姿图优化 / Pose Graph Optimization
- 多Session回环 / Multi-session Loop Closure
- 回环校正传播 / Loop Correction Propagation
- 关键帧管理 / Keyframe Management
- HBA全局优化 / HBA Global Bundle Adjustment
1. 回环检测概述 / Loop Closure Overview
1.1 系统架构 / System Architecture
Voxel-SLAM 的回环检测模块运行在独立线程 thd_loop_closure() 中,与里程计线程 thd_odometry_localmapping() 并行执行。其核心流程由三个阶段组成:
- BTC 描述子匹配:基于 Binary Triangle Combination (BTC) 描述子进行快速场景检索
- 法向量 ICP 验证:对候选回环进行几何一致性精细验证
- GTSAM 位姿图优化:使用 ISAM2 增量优化器进行全局位姿校正
The loop closure module in Voxel-SLAM runs in a dedicated thread thd_loop_closure(), parallel to the odometry thread. It consists of three stages:
- BTC descriptor matching: Fast place recognition using Binary Triangle Combination descriptors
- Normal ICP verification: Geometric consistency check using point-to-plane ICP with normal constraints
- GTSAM pose graph optimization: Incremental global pose correction using ISAM2
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2617-2618]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1806-1807]
1.2 整体流程图 / Overall Pipeline
里程计线程 --> buf_lba2loop --> 回环检测线程
|
[1] 关键帧累积 (win_size scans)
|
[2] BTC描述子生成 (GenerateSTDescs)
|
[3] 场景检索 (SearchLoop)
|--- candidate_selector()
|--- candidate_verify()
|
[4] ICP验证 (icp_normal)
|
[5] ISAM2优化 (build_graph + isam.update)
|
[6] 校正传播 (loop_update)
1.3 线程间通信 / Inter-thread Communication
里程计线程通过 buf_lba2loop 队列将 ScanPose 传递给回环检测线程:
mtx_loop.lock();
buf_lba2loop.push_back(bl);
mtx_loop.unlock();
回环检测完成后,通过设置 loop_detect = 1 通知里程计线程执行 loop_update():
loop_detect = 1; // 在thd_loop_closure中设置
// ...
if(loop_detect == 1)
{
loop_update(); // 在thd_odometry_localmapping中响应
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1659-1661]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2168]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1478-1480]
1.4 关键数据结构 / Key Data Structures
| 结构 | 文件 | 职责 |
|---|---|---|
STDescManager | BTC.h:228 | BTC描述子管理器 |
STD | BTC.h:73 | 三角形描述子 |
BinaryDescriptor | BTC.h:59 | 二值角点描述子 |
BTCOctoTree | BTC.h:155 | BTC体素八叉树 |
BTCPlane | BTC.h:86 | BTC平面结构 |
ScanPose | loop_refine.hpp:17 | 扫描位姿(含点云指针) |
PGO_Edge | loop_refine.hpp:163 | 位姿图边 |
PGO_Edges | loop_refine.hpp:206 | 位姿图边集合 |
Keyframe | (voxel_map.hpp/loop_refine.hpp) | 关键帧 |
[VERIFY: VoxelSLAM/src/BTC.h:228]
[VERIFY: VoxelSLAM/src/BTC.h:73-84]
[VERIFY: VoxelSLAM/src/BTC.h:59-63]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:17-45]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:163-204]
2. BTC描述子流水线 / BTC Descriptor Pipeline
2.1 STDescManager 架构 / STDescManager Architecture
STDescManager 是描述子管理的核心类,维护一个哈希表 data_base_ 存储所有历史描述子,以及一个 plane_cloud_vec_ 存储每帧的平面点云:
class STDescManager {
ConfigSetting config_setting_;
unsigned int current_frame_id_;
std::unordered_map<STD_LOC, std::vector<STD>> data_base_; // 描述子哈希表
std::vector<pcl::PointCloud<pcl::PointXYZINormal>::Ptr> plane_cloud_vec_; // 平面点云
};
[VERIFY: VoxelSLAM/src/BTC.h:228-253]
哈希表的键 STD_LOC 基于三角形边长的量化值:
class STD_LOC {
int64_t x, y, z, a, b, c;
// hash: ((z * HASH_P) % MAX_N + y) * HASH_P) % MAX_N + x
};
[VERIFY: VoxelSLAM/src/BTC.h:130-153]
2.2 Step 1: init_voxel_map() – 体素化 / Voxelization
将输入点云按 voxel_size_ 划分到体素格中,每个体素对应一个 BTCOctoTree:
void init_voxel_map(const pcl::PointCloud<pcl::PointXYZI>::Ptr &input_cloud,
std::unordered_map<BTCVOXEL_LOC, BTCOctoTree *> &voxel_map)
{
for (uint i = 0; i < plsize; i++) {
Eigen::Vector3d p_c(input_cloud->points[i].x, ...);
double loc_xyz[3];
for (int j = 0; j < 3; j++) {
loc_xyz[j] = p_c[j] / config_setting_.voxel_size_;
if (loc_xyz[j] < 0) loc_xyz[j] -= 1.0;
}
BTCVOXEL_LOC position((int64_t)loc_xyz[0], ...);
// 插入或追加到体素
}
// 对每个体素初始化八叉树
for(const size_t &i: index)
iter_list[i]->second->init_octo_tree();
}
体素大小由 config_setting_.voxel_size_ 控制(默认 1.0m,高空飞行模式 2.0m)。
[VERIFY: VoxelSLAM/src/BTC.cpp:279-321]
[VERIFY: VoxelSLAM/src/BTC.cpp:11]
[VERIFY: VoxelSLAM/src/BTC.cpp:43]
2.3 init_plane(): 平面拟合 / Plane Fitting
当体素内点数超过 voxel_init_num_(默认 10)时,通过 PCA 拟合平面:
void BTCOctoTree::init_plane() {
// 计算协方差矩阵
plane_ptr_->covariance_ = sum(pi * pi^T) / N - center * center^T;
// 特征值分解
Eigen::EigenSolver<Eigen::Matrix3d> es(plane_ptr_->covariance_);
// 最小特征值对应法向量
if (evalsReal(evalsMin) < config_setting_.plane_detection_thre_) {
plane_ptr_->normal_ = eigenvectors.col(evalsMin);
plane_ptr_->is_plane_ = true;
}
}
平面检测阈值 plane_detection_thre_ 默认为 0.01。当最小特征值小于该阈值时,认为该体素中的点近似共面。
[VERIFY: VoxelSLAM/src/BTC.cpp:90-139]
[VERIFY: VoxelSLAM/src/BTC.h:29]
2.4 Step 2: get_plane() – 提取平面 / Extract Planes
遍历体素地图,收集所有 is_plane_ == true 的平面中心和法向量:
void get_plane(const std::unordered_map<BTCVOXEL_LOC, BTCOctoTree *> &voxel_map,
pcl::PointCloud<pcl::PointXYZINormal>::Ptr &plane_cloud) {
for (auto iter = voxel_map.begin(); iter != voxel_map.end(); iter++) {
if (iter->second->plane_ptr_->is_plane_) {
pcl::PointXYZINormal pi;
pi.x = center[0]; pi.y = center[1]; pi.z = center[2];
pi.normal_x = normal[0]; pi.normal_y = normal[1]; pi.normal_z = normal[2];
plane_cloud->push_back(pi);
}
}
}
平面点云的 header.seq 字段被赋值为帧 ID,用于后续回环检测中定位原始帧。
[VERIFY: VoxelSLAM/src/BTC.cpp:323-338]
[VERIFY: VoxelSLAM/src/BTC.cpp:166]
2.5 get_project_plane(): 投影平面提取 / Projection Plane Extraction
从已检测到的平面中选取主要投影平面。通过判断平面的法向量相似性和距离,将相近的平面合并:
void get_project_plane(std::unordered_map<BTCVOXEL_LOC, BTCOctoTree *> &voxel_map,
std::vector<BTCPlane *> &project_plane_list)
合并条件为法向量差的模小于 plane_merge_normal_thre_(默认 0.1)且平面间距离小于 plane_merge_dis_thre_(默认 0.3):
if (normal_diff.norm() < config_setting_.plane_merge_normal_thre_ ||
normal_add.norm() < config_setting_.plane_merge_normal_thre_)
if (dis1 < config_setting_.plane_merge_dis_thre_ &&
dis2 < config_setting_.plane_merge_dis_thre_)
[VERIFY: VoxelSLAM/src/BTC.cpp:340-458]
[VERIFY: VoxelSLAM/src/BTC.cpp:367-370]
2.6 Step 3: binary_extractor() – 二值描述子提取 / Binary Descriptor Extraction
将点云投影到选定的平面上,构建2D栅格图像,提取具有丰富高度分布的角点:
void binary_extractor(const std::vector<BTCPlane *> proj_plane_list,
const pcl::PointCloud<pcl::PointXYZI>::Ptr &input_cloud,
std::vector<BinaryDescriptor> &binary_descriptor_list)
[VERIFY: VoxelSLAM/src/BTC.cpp:571-611]
投影过程 (extract_binary):
- 构建平面的正交坐标系
(x_axis, y_axis, normal) - 将3D点投影到该平面,获取2D坐标和距离
- 将2D空间栅格化(分辨率
proj_image_resolution_= 0.5m) - 对每个栅格,统计不同高度层的占据情况,形成二值占据数组
occupy_array_ summary_记录被占据的高度层总数,作为角点显著性度量
[VERIFY: VoxelSLAM/src/BTC.cpp:613-924]
高度层划分:
int cut_num = (dis_threshold_max - dis_threshold_min) / high_inc;
默认:dis_threshold_max = 5m,dis_threshold_min = 0m,high_inc = 0.1m,因此有 50 个高度层。
[VERIFY: VoxelSLAM/src/BTC.h:33-36]
非极大值抑制 (non_maxi_suppression):
使用 KdTree 在半径 non_max_suppression_radius_(默认 2.0m 或 3.0m)内抑制较弱的角点,只保留 summary_ 最大的:
void non_maxi_suppression(std::vector<BinaryDescriptor> &binary_list) {
// KdTree radius search
if (pre_count_list[i] <= pre_count_list[pointIdxRadiusSearch[j]]) {
is_add_list[i] = false;
}
}
最终保留 useful_corner_num_ 个角点(默认 100/200)。
[VERIFY: VoxelSLAM/src/BTC.cpp:926-977]
[VERIFY: VoxelSLAM/src/BTC.h:23]
2.7 Step 4: generate_std() – 三角形描述子生成 / Triangle Descriptor Generation
从角点中构建三角形组合,形成 STD (Stable Triangle Descriptor):
void generate_std(const std::vector<BinaryDescriptor> &binary_list,
const int &frame_id, std::vector<STD> &std_list)
对于每个角点,使用 KdTree 找到最近的 K = descriptor_near_num_(默认 15)个邻居,从中选取所有满足边长约束的三角形:
if (a > config_setting_.descriptor_max_len_ || ... ||
a < config_setting_.descriptor_min_len_ || ...)
continue;
边长范围:descriptor_min_len_ (2m) 到 descriptor_max_len_ (50m)。
[VERIFY: VoxelSLAM/src/BTC.cpp:979-1126]
[VERIFY: VoxelSLAM/src/BTC.h:43-44]
三角形边长排序:
将三角形三边长排序为 a <= b <= c,并追踪顶点映射关系,确保相同形状的三角形具有一致的描述方式:
if (a > b) { swap; adjust vertex mapping; }
if (b > c) { swap; adjust vertex mapping; }
if (a > b) { swap; adjust vertex mapping; }
退化三角形(近似共线)被过滤:
if (fabs(c - (a + b)) < 0.2) continue;
[VERIFY: VoxelSLAM/src/BTC.cpp:1032-1057]
STD 描述子内容:
typedef struct STD {
Eigen::Vector3d triangle_; // 量化后的边长 (scale * a, scale * b, scale * c)
Eigen::Vector3d angle_; // 顶点角度信息
Eigen::Vector3d center_; // 三角形中心
int frame_number_; // 所属帧号
BinaryDescriptor binary_A_; // 顶点A的二值描述子
BinaryDescriptor binary_B_; // 顶点B的二值描述子
BinaryDescriptor binary_C_; // 顶点C的二值描述子
} STD;
其中 triangle_ 的量化尺度 scale = 1.0 / std_side_resolution_(std_side_resolution_ = 0.2m,因此 scale = 5)。
[VERIFY: VoxelSLAM/src/BTC.h:73-84]
[VERIFY: VoxelSLAM/src/BTC.h:46]
[VERIFY: VoxelSLAM/src/BTC.cpp:982]
2.8 GenerateSTDescs(): 完整流水线 / Complete Pipeline
GenerateSTDescs() 按顺序调用上述步骤:
void GenerateSTDescs(pcl::PointCloud<pcl::PointXYZI>::Ptr &input_cloud,
std::vector<STD> &stds_vec, int id) {
// step1: voxelization and plane detection
init_voxel_map(input_cloud, voxel_map);
get_plane(voxel_map, plane_cloud);
plane_cloud->header.seq = id;
plane_cloud_vec_.push_back(plane_cloud);
// step3: extraction binary descriptors
get_project_plane(voxel_map, proj_plane_list);
merge_plane(proj_plane_list, merge_plane_list);
binary_extractor(merge_plane_list, input_cloud, binary_list);
// step4: generate stable triangle descriptors
generate_std(binary_list, current_frame_id_, stds_vec);
// step5: clear memory
for (auto iter = voxel_map.begin(); ...) delete(iter->second);
}
[VERIFY: VoxelSLAM/src/BTC.cpp:156-203]
2.9 AddSTDescs(): 添加到数据库 / Add to Database
void AddSTDescs(const std::vector<STD> &stds_vec) {
current_frame_id_++;
for (auto &single_std : stds_vec) {
STD_LOC position;
position.x = (int)(single_std.triangle_[0] + 0.5);
position.y = (int)(single_std.triangle_[1] + 0.5);
position.z = (int)(single_std.triangle_[2] + 0.5);
auto iter = data_base_.find(position);
if (iter != data_base_.end())
iter->second.push_back(single_std);
else
data_base_[position] = {single_std};
}
}
每个 STD 按照其边长的四舍五入整数值作为哈希键,存入数据库。这样形状相似的三角形会落入相同或相邻的哈希桶中。
[VERIFY: VoxelSLAM/src/BTC.cpp:258-277]
3. 场景检索与验证 / Place Retrieval and Verification
3.1 SearchLoop() 概述 / SearchLoop Overview
void SearchLoop(std::vector<STD> &stds_vec, std::pair<int, double> &loop_result,
std::pair<Eigen::Vector3d, Eigen::Matrix3d> &loop_transform,
std::vector<std::pair<STD, STD>> &loop_std_pair,
pcl::PointCloud<pcl::PointXYZINormal>::Ptr pl_cur)
[VERIFY: VoxelSLAM/src/BTC.cpp:205-256]
流程:
- 调用
candidate_selector()进行粗匹配,选出候选帧 - 对每个候选帧调用
candidate_verify()进行几何验证 - 选取得分最高且超过
icp_threshold_(默认 0.15)的候选帧作为回环
3.2 candidate_selector(): 候选帧选择 / Candidate Frame Selection
void candidate_selector(std::vector<STD> ¤t_STD_list,
std::vector<STDMatchList> &candidate_matcher_vec)
[VERIFY: VoxelSLAM/src/BTC.cpp:1128-1279]
粗匹配过程:
- 对当前帧的每个 STD 描述子,在哈希表的 3x3x3 邻域中搜索形状相似的历史描述子:
for (auto &voxel_inc : voxel_round) // 27个邻域
{
position.x = (int)(descriptor.triangle_[0] + voxel_inc[0]);
// ...
auto iter = data_base_.find(position);
if (iter != data_base_.end()) {
for (size_t j = 0; j < iter->second.size(); j++) {
// 跳过时间相近的帧
if ((descriptor.frame_number_ - iter->second[j].frame_number_)
> config_setting_.skip_near_num_) {
double dis = (descriptor.triangle_ - iter->second[j].triangle_).norm();
if (dis < dis_threshold) {
// 计算二值描述子相似度
double similarity = (binary_similarity(A, A') +
binary_similarity(B, B') +
binary_similarity(C, C')) / 3;
if (similarity > config_setting_.similarity_threshold_)
// 记录匹配
}
}
}
}
}
[VERIFY: VoxelSLAM/src/BTC.cpp:1159-1211]
skip_near_num_控制跳过时间上相近的帧(默认 30),防止与相邻帧匹配。对于历史 session,该值被设为负数以允许所有匹配:
std_manager->config_setting_.skip_near_num_ = -(std_manager->plane_cloud_vec_.size()+10);
[VERIFY: VoxelSLAM/src/BTC.h:49]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:401]
- 对匹配计数进行投票,选出最多
candidate_num_(默认 20/100)个候选帧(需要至少 5 票):
for (int cnt = 0; cnt < config_setting_.candidate_num_; cnt++) {
double max_vote_index = std::max_element(match_array.begin(), match_array.end()) - match_array.begin();
if (max_vote >= 5) {
// 添加候选帧
match_array[max_vote_index] = 0;
}
}
[VERIFY: VoxelSLAM/src/BTC.cpp:1235-1278]
3.3 binary_similarity(): 二值描述子相似度 / Binary Descriptor Similarity
double binary_similarity(const BinaryDescriptor &b1, const BinaryDescriptor &b2) {
double dis = 0;
for (size_t i = 0; i < b1.occupy_array_.size(); i++) {
if (b1.occupy_array_[i] == true && b2.occupy_array_[i] == true)
dis += 1;
}
return 2 * dis / (b1.summary_ + b2.summary_);
}
使用修改版 Dice 系数:2 * |A ∩ B| / (|A| + |B|),值域为 [0, 1]。相似度阈值 similarity_threshold_ 默认为 0.7。
[VERIFY: VoxelSLAM/src/BTC.cpp:70-80]
[VERIFY: VoxelSLAM/src/BTC.h:52]
3.4 candidate_verify(): 候选帧几何验证 / Candidate Frame Geometric Verification
void candidate_verify(STDMatchList &candidate_matcher, double &verify_score,
std::pair<Eigen::Vector3d, Eigen::Matrix3d> &relative_pose,
std::vector<std::pair<STD, STD>> &sucess_match_list,
pcl::PointCloud<pcl::PointXYZINormal>::Ptr pl_cur)
[VERIFY: VoxelSLAM/src/BTC.cpp:1281-1396]
RANSAC 风格的验证过程:
- 对候选匹配列表中的每对匹配三角形,调用
triangle_solver()计算变换 - 用该变换验证其他匹配对(投票机制),距离阈值 3m:
Eigen::Vector3d A_transform = test_rot * verify_pair.first.binary_A_.location_ + test_t;
if((A_transform - verify_pair.second.binary_A_.location_).norm() >= dis_threshold)
continue;
// ... 类似检查 B 和 C
vote++;
- 选择投票数最多的变换(需至少 4 票)
- 最终通过
plane_geometric_verify()计算平面对齐得分
[VERIFY: VoxelSLAM/src/BTC.cpp:1307-1349]
[VERIFY: VoxelSLAM/src/BTC.cpp:1361]
3.5 triangle_solver(): 三角形求解 / Triangle Transform Solver
使用 SVD 分解计算两个三角形之间的刚体变换:
void triangle_solver(std::pair<STD, STD> &std_pair, Eigen::Vector3d &t, Eigen::Matrix3d &rot) {
Eigen::Matrix3d src, ref;
src.col(0) = std_pair.first.binary_A_.location_ - std_pair.first.center_;
// ... B, C类似
ref.col(0) = std_pair.second.binary_A_.location_ - std_pair.second.center_;
// ... B, C类似
Eigen::Matrix3d covariance = src * ref.transpose();
Eigen::JacobiSVD<Eigen::Matrix3d> svd(covariance, ComputeThinU | ComputeThinV);
rot = V * U.transpose();
if (rot.determinant() < 0) {
rot = V * diag(1,1,-1) * U.transpose();
}
t = -rot * std_pair.first.center_ + std_pair.second.center_;
}
这是经典的 Procrustes 分析/点对配准方法。当 det(R) < 0 时(反射情况),通过翻转最后一个奇异值来修正。
[VERIFY: VoxelSLAM/src/BTC.cpp:1398-1420]
3.6 plane_geometric_verify(): 平面几何验证 / Plane Geometric Verification
计算源平面云经过变换后与目标平面云的匹配比例:
double plane_geometric_verify(
const pcl::PointCloud<pcl::PointXYZINormal>::Ptr &source_cloud,
const pcl::PointCloud<pcl::PointXYZINormal>::Ptr &target_cloud,
const std::pair<Eigen::Vector3d, Eigen::Matrix3d> &transform)
对每个源平面点:
- 变换位置和法向量
- KdTree 最近邻搜索
- 检查法向量一致性和点到面距离
匹配条件:
if ((normal_inc.norm() < normal_threshold ||
normal_add.norm() < normal_threshold) &&
point_to_plane < dis_threshold)
useful_match++;
返回值为匹配比例 useful_match / source_cloud->size()。
[VERIFY: VoxelSLAM/src/BTC.cpp:1422-1479]
[VERIFY: VoxelSLAM/src/BTC.h:55-56]
4. 法向量ICP验证 / Normal ICP Verification
4.1 icp_normal() 概述 / Overview
icp_normal() 是回环检测中最关键的几何验证步骤。在 BTC 描述子提供粗略变换后,ICP 进一步精化对齐并验证匹配质量:
bool icp_normal(pcl::PointCloud<PointType> &pl_src,
pcl::PointCloud<PointType> &pl_tar,
pair<Eigen::Vector3d, Eigen::Matrix3d> &pose,
double icp_eigval)
返回 true 表示回环有效,false 表示回环被拒绝。
[VERIFY: VoxelSLAM/src/loop_refine.hpp:47]
4.2 KdTree 构建 / KdTree Construction
以目标点云构建 KdTree:
pcl::KdTreeFLANN<pcl::PointXYZ> kd_tree;
pcl::PointCloud<pcl::PointXYZ> input_cloud;
for(PointType &ap: pl_tar.points) {
pcl::PointXYZ pi;
pi.x = ap.x; pi.y = ap.y; pi.z = ap.z;
input_cloud.push_back(pi);
}
kd_tree.setInputCloud(input_cloud.makeShared());
[VERIFY: VoxelSLAM/src/loop_refine.hpp:49-57]
4.3 两阶段收敛策略 / Two-Phase Convergence Strategy
ICP 采用从粗到精的两阶段策略:
Phase 1 (Coarse):
Eigen::Vector4d paras(0.2, 0.2, 0.5, 3);
- 法向量差异阈值: 0.2
- 法向量和阈值: 0.2
- 点到面距离阈值: 0.5m
- 点到点距离阈值: 3m
Phase 2 (Fine) – 首次收敛后切换:
if(dxi.head(3).norm() < 1e-3 && dxi.tail(3).norm() < 1e-3) {
if(is_converge)
break;
else {
paras << 0.1, 0.1, 0.1, 1;
is_converge = 1;
}
}
- 法向量差异阈值: 0.1
- 法向量和阈值: 0.1
- 点到面距离阈值: 0.1m
- 点到点距离阈值: 1m
[VERIFY: VoxelSLAM/src/loop_refine.hpp:62]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:120-130]
4.4 匹配条件 / Matching Conditions
Eigen::Vector3d normal_inc = ni - tni;
Eigen::Vector3d normal_add = ni + tni;
double point_to_point_dis = (pi - tpi).norm();
double point_to_plane = fabs(tni.transpose() * (pi - tpi));
if ((normal_inc.norm() < paras[0] || normal_add.norm() < paras[1])
&& point_to_plane < paras[2] && point_to_point_dis < paras[3])
法向量条件使用"或"逻辑:normal_inc 和 normal_add 分别对应法向量同向和反向的情况(因为法向量方向可能翻转)。
[VERIFY: VoxelSLAM/src/loop_refine.hpp:93-97]
4.5 最小二乘求解 / Least Squares Solution
对匹配的点对构建点到面的最小二乘问题:
double rr = tni.dot(pi - tpi);
Eigen::Matrix<double, 6, 1> jac;
jac.head(3) = hat(plocal) * pose.second.transpose() * tni; // 对旋转
jac.tail(3) = tni; // 对平移
Hess += jac * jac.transpose();
JacT += jac * rr;
resi += 0.5 * rr * rr;
Eigen::Matrix<double, 6, 1> dxi = Hess.ldlt().solve(-JacT);
pose.second = pose.second * Exp(dxi.head(3));
pose.first = pose.first + dxi.tail(3);
最多迭代 20 次。
[VERIFY: VoxelSLAM/src/loop_refine.hpp:100-117]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:69]
4.6 特征值验证 / Eigenvalue Validation
法向量矩阵的特征值用于判断约束是否充分:
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> saes(mat_norm);
Eigen::Vector3d eig_vec = saes.eigenvalues();
return eig_vec[0] > icp_eigval && is_converge == 1;
mat_norm 累积了所有匹配法向量的外积 sum(n * n^T)。最小特征值 eig_vec[0] 必须大于 icp_eigval(默认 14),确保三个方向上都有足够的约束。
[VERIFY: VoxelSLAM/src/loop_refine.hpp:139-142]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1813]
4.7 在回环检测中的调用 / Invocation in Loop Detection
if(search_result.first >= 0 && search_result.second > juds[id])
{
if(icp_normal(*(std_manager->plane_cloud_vec_.back()),
*(std_managers[id]->plane_cloud_vec_[search_result.first]),
loop_transform, icp_eigval))
{
// 回环确认,计算漂移并添加边
}
}
只有同时满足 BTC 得分超过阈值(juds[id],默认 0.45)和 ICP 验证通过,才确认回环。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1997-1999]
5. 位姿图优化 / Pose Graph Optimization
5.1 GTSAM ISAM2 架构 / GTSAM ISAM2 Architecture
系统使用 GTSAM 库的 ISAM2 增量优化器进行位姿图优化:
gtsam::ISAM2Params parameters;
parameters.relinearizeThreshold = 0.01;
parameters.relinearizeSkip = 1;
gtsam::ISAM2 isam(parameters);
isam.update(graph, initial);
for(int i = 0; i < 5; i++) isam.update();
gtsam::Values results = isam.calculateEstimate();
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2090-2097]
5.2 add_edge(): 创建BetweenFactor / Creating BetweenFactor
add_edge() 有两个重载版本,分别接受 IMUST 状态或直接的旋转/平移:
版本 1:从两个IMUST状态创建相对变换
void add_edge(int pos1, int pos2, IMUST &x1, IMUST &x2,
gtsam::NonlinearFactorGraph &graph,
gtsam::noiseModel::Diagonal::shared_ptr odometryNoise)
{
gtsam::Point3 tt(x1.R.transpose() * (x2.p - x1.p)); // 相对平移(x1坐标系下)
gtsam::Rot3 RR(x1.R.transpose() * x2.R); // 相对旋转
gtsam::NonlinearFactor::shared_ptr factor(
new gtsam::BetweenFactor<gtsam::Pose3>(pos1, pos2, gtsam::Pose3(RR, tt), odometryNoise));
graph.push_back(factor);
}
[VERIFY: VoxelSLAM/src/loop_refine.hpp:147-153]
版本 2:直接指定旋转和平移
void add_edge(int pos1, int pos2, Eigen::Matrix3d &rot, Eigen::Vector3d &tra,
gtsam::NonlinearFactorGraph &graph,
gtsam::noiseModel::Diagonal::shared_ptr odometryNoise)
{
gtsam::Point3 tt(tra);
gtsam::Rot3 RR(rot);
gtsam::NonlinearFactor::shared_ptr factor(
new gtsam::BetweenFactor<gtsam::Pose3>(pos1, pos2, gtsam::Pose3(RR, tt), odometryNoise));
graph.push_back(factor);
}
[VERIFY: VoxelSLAM/src/loop_refine.hpp:155-161]
5.3 build_graph(): 构建多Session位姿图 / Building Multi-session Pose Graph
void build_graph(gtsam::Values &initial, gtsam::NonlinearFactorGraph &graph,
int cur_id, PGO_Edges &lp_edges,
gtsam::noiseModel::Diagonal::shared_ptr default_noise,
vector<int> &ids, vector<int> &stepsizes, int lpedge_enable)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1741]
构建步骤:
- 连通图发现:通过
lp_edges.connect(cur_id, ids)找到与当前 session 连通的所有 session:
ids.clear();
lp_edges.connect(cur_id, ids);
PGO_Edges::connect() 使用 BFS/DFS 遍历图的连通分量:
void connect(int root, vector<int> &ids) {
ids.clear();
ids.push_back(root);
tras(root, ids); // 递归遍历
sort(ids.begin(), ids.end());
}
[VERIFY: VoxelSLAM/src/loop_refine.hpp:237-243]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1744-1745]
- 计算全局索引偏移:
stepsizes.clear(); stepsizes.push_back(0);
for(int i = 0; i < ids.size(); i++)
stepsizes.push_back(stepsizes.back() + multimap_scanPoses[ids[i]]->size());
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1747-1749]
- 添加里程计边(相邻帧之间的 BetweenFactor):
for(int j = bsize; j < stepsizes[ii+1]; j++) {
initial.insert(j, pose3);
if(j > bsize) {
gtsam::Vector samv6(6);
samv6 = multimap_scanPoses[ids[ii]]->at(j-1-bsize)->v6;
gtsam::noiseModel::Diagonal::shared_ptr v6_noise =
gtsam::noiseModel::Diagonal::Variances(samv6);
add_edge(j-1, j, ..., graph, v6_noise);
}
}
噪声模型使用每个 ScanPose 的 v6 字段,该字段源自 Local BA 的 Hessian 矩阵对角线:
bl->v6 = hess.block<6, 6>(0, DIM).diagonal();
for(int i = 0; i < 6; i++) bl->v6[i] = 1.0 / fabs(bl->v6[i]);
这意味着噪声与 Hessian 对角元素成反比 – Hessian 越大(优化约束越强),噪声越小。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1751-1767]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1657-1658]
- 添加先验因子:对第一个 session 的首个位姿添加强先验:
Eigen::Matrix<double, 6, 1> v6_fixd;
v6_fixd << 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9;
gtsam::noiseModel::Diagonal::shared_ptr fixd_noise =
gtsam::noiseModel::Diagonal::Variances(gtsam::Vector(v6_fixd));
graph.addPrior(0, pose3, fixd_noise);
极小的方差(1e-9)使得第一个位姿几乎被固定。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1770-1783]
- 添加回环边(如果
lpedge_enable == 1):
if(lpedge_enable == 1)
for(PGO_Edge &edge: lp_edges.edges) {
vector<int> step(2);
if(edge.is_adapt(ids, step)) {
for(int i = 0; i < edge.rots.size(); i++) {
int id1 = mp[0] + edge.ids1[i];
int id2 = mp[1] + edge.ids2[i];
add_edge(id1, id2, edge.rots[i], edge.tras[i], graph, default_noise);
}
}
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1786-1801]
5.4 PGO_Edge / PGO_Edges: 位姿图边管理 / Pose Graph Edge Management
struct PGO_Edge {
int m1, m2; // 两个session的ID
vector<int> ids1, ids2; // 帧ID列表
PLM(3) rots; // 相对旋转列表
PLV(3) tras; // 相对平移列表
PLV(6) covs; // 协方差列表
};
PGO_Edges 维护所有边并提供连通性查询:
struct PGO_Edges {
vector<PGO_Edge> edges;
vector<vector<int>> mates; // 邻接表
void push(m1, m2, id1, id2, rot, tra, v6); // 添加边
void connect(root, ids); // 查找连通分量
};
[VERIFY: VoxelSLAM/src/loop_refine.hpp:163-204]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:206-267]
5.5 优化结果应用 / Applying Optimization Results
优化完成后,将 ISAM2 的结果写回到各个 session 的 ScanPose:
gtsam::Values results = isam.calculateEstimate();
for(int ii = 0; ii < idsize; ii++) {
int tip = ids[ii];
for(int j = stepsizes[ii]; j < stepsizes[ii+1]; j++) {
int ord = j - stepsizes[ii];
multimap_scanPoses[tip]->at(ord)->set_state(results.at(j).cast<gtsam::Pose3>());
}
}
ScanPose::set_state() 不仅更新位姿,还旋转速度向量:
void set_state(const gtsam::Pose3 &pose) {
Eigen::Matrix3d rot = pose.rotation().matrix();
rot = rot * x.R.transpose(); // 计算修正旋转
x.R = pose.rotation().matrix();
x.p = pose.translation();
x.v = rot * x.v; // 相应旋转速度
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2100-2112]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:36-43]
6. 多Session回环 / Multi-session Loop Closure
6.1 previous_map_read(): 加载历史Session / Loading Historical Sessions
void previous_map_read(vector<STDescManager*> &std_managers,
vector<vector<ScanPose*>*> &multimap_scanPoses,
vector<vector<Keyframe*>*> &multimap_keyframes,
ConfigSetting &config_setting, PGO_Edges &edges,
ros::NodeHandle &n, vector<string> &fnames,
vector<double> &juds, string &savepath, int win_size)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:307]
加载流程:
- 读取位姿文件:
read_lidarstate(fname+"/alidarState.txt", *bl_tem) - 加载 PCD 点云并组装关键帧(每
win_size个扫描合并一个关键帧) - 为每个关键帧生成 BTC 描述子
- 设置
skip_near_num_为负数以允许跨 session 完全匹配
[VERIFY: VoxelSLAM/src/voxelslam.cpp:313-404]
关键帧组装:
for(int j = 0; j < win_size; j++) {
Eigen::Vector3d delta_p = xc.R.transpose() * (xxbuf[j].p - xc.p);
Eigen::Matrix3d delta_R = xc.R.transpose() * xxbuf[j].R;
for(pcl::PointXYZI pp: plbuf[j]->points) {
Eigen::Vector3d v3(pp.x, pp.y, pp.z);
v3 = delta_R * v3 + delta_p;
// ...
}
}
将 win_size 个扫描的点变换到最后一个扫描的坐标系下合并。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:348-360]
BTC 描述子生成(每 mgsize 个关键帧一组):
for(int i = 0; i+acsize < subsize; i += mgsize) {
int up = i + acsize;
// 合并acsize个关键帧的点云
std_manager->GenerateSTDescs(pl_btc, stds_vec, keyframes_tem->at(up-1)->id);
std_manager->AddSTDescs(stds_vec);
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:377-400]
6.2 跨Session回环检测 / Cross-session Loop Detection
在 thd_loop_closure() 的主循环中,对所有 session(包括当前和历史 session)执行检索:
for(int id = 0; id <= cur_id; id++) {
std_managers[id]->SearchLoop(stds_vec, search_result, loop_transform, loop_std_pair,
std_manager->plane_cloud_vec_.back());
if(search_result.first >= 0 && search_result.second > juds[id]) {
if(icp_normal(...)) {
// 根据 id == cur_id 判断是自身回环还是跨session回环
if(id == cur_id) {
// 自身session回环,检查漂移比
double span = smp->jour - keyframes->at(search_result.first)->jour;
if(drift_p / span < ratio_drift)
isPush = true;
} else {
// 跨session回环,需要重建图
isGraph = true;
isOpt = true;
g_update = 1;
}
}
}
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1987-2079]
6.3 漂移检测 / Drift Detection
对自身 session 的回环,使用漂移比阈值来过滤误匹配:
double drift_p = (xx.R * loop_transform.first + xx.p - xc.p).norm();
// ...
if(drift_p / span < ratio_drift) // ratio_drift 默认 0.05
isPush = true;
漂移距离与行驶距离之比小于 5% 才接受。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2004]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2012-2014]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1813]
对历史 session 的回环,使用累积距离 jours[id] 作为判断依据:
if(drift_p / jours[id] < 0.05)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2044]
6.4 频率控制 / Frequency Control
通过 relc_counts 计数器控制回环优化的频率:
vector<int> relc_counts(std_managers.size(), prev_halt);
// ...
if(relc_counts[id] > curr_halt && drift_p > 0.10) {
isOpt = true;
for(int &cnt: relc_counts) cnt = 0;
}
// ...
for(int &it: relc_counts) it++;
curr_halt = 10(自身session)和 prev_halt = 30(历史session)控制两次优化之间的最小关键帧间隔。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1814-1815]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2018-2021]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2080]
7. 回环校正传播 / Loop Correction Propagation
7.1 loop_update() 概述 / Overview
当回环检测线程设置 loop_detect = 1 后,里程计线程在下一次循环中调用 loop_update() 来应用校正:
void loop_update()
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1101]
7.2 dx 变换计算 / Computing dx Transform
在回环检测线程中,优化完成后计算当前帧前后的变换差:
IMUST x1 = scanPoses->at(buf_base-1)->x; // 优化前的位姿
// ... ISAM2优化 ...
IMUST x3 = scanPoses->at(buf_base-1)->x; // 优化后的位姿
dx.p = x3.p - x3.R * x1.R.transpose() * x1.p;
dx.R = x3.R * x1.R.transpose();
dx 表示从优化前坐标系到优化后坐标系的刚体变换。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2100]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2126-2128]
7.3 体素地图重建 / Voxel Map Reconstruction
// 清除当前体素地图
for(auto iter = surf_map.begin(); iter != surf_map.end(); iter++) {
iter->second->tras_ptr(octos_release);
iter->second->clear_slwd(sws[0]);
delete iter->second;
}
surf_map.clear(); surf_map_slide.clear();
// 使用回环检测线程预构建的新地图
surf_map = map_loop;
map_loop.clear();
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1105-1114]
7.4 滑动窗口状态校正 / Sliding Window State Correction
对已发送到回环检测但尚未优化的帧(buf_lba2loop)应用 ScanPose::update(dx):
for(ScanPose *bl: buf_lba2loop) {
bl->update(dx);
}
ScanPose::update() 实现:
void update(IMUST dx) {
x.v = dx.R * x.v;
x.p = dx.R * x.p + dx.p;
x.R = dx.R * x.R;
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1129-1131]
[VERIFY: VoxelSLAM/src/loop_refine.hpp:29-34]
对当前滑动窗口中的帧直接变换:
for(int i = 0; i < win_count; i++) {
IMUST &x = x_buf[i];
x.v = dx.R * x.v;
x.p = dx.R * x.p + dx.p;
x.R = dx.R * x.R;
if(g_update == 1)
x.g = dx.R * x.g;
}
如果是跨 session 回环(g_update == 1),还需要旋转重力向量。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1138-1149]
7.5 地图重建与填充 / Map Rebuild and Population
校正后将所有帧的点重新插入体素地图:
// 插入 buf_lba2loop 中的点
for(ScanPose *bl: buf_lba2loop) {
IMUST xx = bl->x;
PVec pvec_tem = *(bl->pvec);
for(pointVar &pv: pvec_tem)
pv.pnt = xx.R * pv.pnt + xx.p;
cut_voxel(surf_map, pvec_tem, win_size, 0);
}
// 插入滑动窗口中的点
for(int i = 0; i < win_count; i++) {
pwld.clear();
for(pointVar &pv: *pvec_buf[i])
pwld.push_back(x_buf[i].R * pv.pnt + x_buf[i].p);
cut_voxel(surf_map, pvec_buf[i], i, surf_map_slide, win_size, pwld, sws[0]);
}
// 重新切割体素
for(auto iter = surf_map.begin(); iter != surf_map.end(); ++iter)
iter->second->recut(win_count, x_buf, sws[0]);
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1161-1180]
7.6 重力更新状态机 / Gravity Update State Machine
g_update 使用三态机制:
g_update = 0: 正常模式g_update = 1: 跨session回环触发,需要旋转重力g_update = 2: loop_update() 完成后设置,触发 Local BA 中的重力优化
if(g_update == 1) g_update = 2; // loop_update末尾
// ...
if(g_update == 2) {
LI_BA_OptimizerGravity opt_lsv;
opt_lsv.damping_iter(x_buf, voxhess, imu_pre_buf, resis, &hess, 5);
g_update = 0;
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1182]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1641-1649]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2038]
8. 关键帧管理 / Keyframe Management
8.1 关键帧累积策略 / Keyframe Accumulation Strategy
在回环检测线程中,每 win_size 个扫描帧累积为一个关键帧:
if(bl_local.size() < win_size) continue;
同时还需要满足角度/距离阈值(防止低运动时过于频繁地创建关键帧):
double ang = Log(x_key.R.transpose() * xc.R).norm() * 57.3;
double len = (xc.p - x_key.p).norm();
if(ang < 5 && len < 0.1 && buf_base > win_size)
{
bl_local.front()->pvec = nullptr;
bl_local.pop_front();
continue;
}
旋转超过 5 度或平移超过 0.1m 时创建新关键帧。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1931-1939]
8.2 关键帧结构 / Keyframe Structure
Keyframe {
IMUST x0; // 原始位姿
int id; // 在ScanPose序列中的索引
double jour; // 累积行驶距离
int mp; // 所属session ID (用于HBA)
pcl::PointCloud<PointType>::Ptr plptr; // 降采样点云
bool exist; // 是否已加载到地图
}
关键帧的点云通过 down_sampling_pvec() 降采样后存储:
down_sampling_pvec(*pptr, voxel_size/10, *(smp->plptr));
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1962-1963]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1965]
8.3 关键帧合并点云 / Keyframe Point Cloud Merging
对于 win_size 个扫描帧,将它们变换到最后一帧的坐标系下合并:
for(int i = 0; i < win_size; i++) {
ScanPose &bl = *bl_local[i];
Eigen::Vector3d delta_p = xc.R.transpose() * (bl.x.p - xc.p);
Eigen::Matrix3d delta_R = xc.R.transpose() * bl.x.R;
for(pointVar pv: *(bl.pvec)) {
pv.pnt = delta_R * pv.pnt + delta_p;
pptr->push_back(pv);
}
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1945-1955]
8.4 keyframe_loading(): KdTree历史关键帧加载 / Historical Keyframe Loading
keyframe_loading() 使用 KdTree 在关键帧位置上进行半径搜索,将附近的历史关键帧加载到当前体素地图中:
void keyframe_loading(double jour)
{
if(history_kfsize <= 0) return;
PointType ap_curr;
ap_curr.x = x_curr.p[0]; ap_curr.y = x_curr.p[1]; ap_curr.z = x_curr.p[2];
vector<int> vec_idx;
vector<float> vec_dis;
kd_keyframes.radiusSearch(ap_curr, 10, vec_idx, vec_dis); // 半径10m
for(int id: vec_idx) {
if(keyframes->at(id)->exist) {
// 加载点云到体素地图
Keyframe &kf = *(keyframes->at(id));
// ... 变换并插入 cut_voxel
kf.exist = 0;
history_kfsize--;
break; // 每次只加载一个
}
}
}
每次只加载一个最近的关键帧,避免一次性加载过多导致延迟。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1189-1228]
8.5 KdTree 更新 / KdTree Update
回环优化后更新 KdTree:
pl_kdmap->clear();
for(int i = 0; i < subsize-init_num; i++) {
Keyframe &kf = *(keyframes->at(i));
kf.exist = 1;
PointType pp;
pp.x = kf.x0.p[0]; pp.y = kf.x0.p[1]; pp.z = kf.x0.p[2];
pp.intensity = cur_id; pp.curvature = i;
pl_kdmap->push_back(pp);
}
kd_keyframes.setInputCloud(pl_kdmap);
history_kfsize = pl_kdmap->size();
最近的 init_num(= 5)个关键帧不进入 KdTree(它们的点已在 map_loop 中预构建)。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2152-2167]
9. HBA全局优化 / HBA Global Bundle Adjustment
9.1 概述 / Overview
Voxel-SLAM 采用 Hierarchical Bundle Adjustment (HBA) 方法进行全局优化。HBA 运行在独立线程 thd_globalmapping() 中,分为两层:
- Bottom-up (自底向上):在局部关键帧窗口中进行 point-to-plane BA,提取帧间约束
- Top-down (自顶向下):利用提取的约束进行位姿图优化
main()
|-> thread thd_loop_closure // 回环检测
|-> thread thd_globalmapping // HBA全局优化
|-> thd_odometry_localmapping // 里程计 (主线程)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2617-2619]
9.2 Bottom-up: HBA_add_edge() / Bottom-up Edge Extraction
void HBA_add_edge(vector<IMUST> &p_xs, vector<Keyframe*> &p_smps,
PGO_Edges &gba_edges, vector<int> &maps,
int max_iter, int thread_num,
pcl::PointCloud<PointType>::Ptr plptr = nullptr)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2320]
流程:
- 将关键帧的点云放入
OctreeGBA体素地图 - 对体素进行
recut提取平面约束 - 运行
Lidar_BA_Optimizer::damping_iter()优化位姿 - 从优化后的 Hessian 矩阵提取帧间约束
约束提取:
for(int i = 0; i < wdsize - 1; i++)
for(int j = i+1; j < wdsize; j++) {
bool isAdd = true;
Eigen::Matrix<double, 6, 1> v6;
for(int k = 0; k < 6; k++) {
double hc = fabs(hess(6*i+k, 6*j+k));
if(hc < 1e-6) { isAdd = false; break; }
v6[k] = 1.0 / hc;
}
if(isAdd) {
Eigen::Vector3d tra = xs[i].R.transpose() * (xs[j].p - xs[i].p);
Eigen::Matrix3d rot = xs[i].R.transpose() * xs[j].R;
gba_edges.push(s1.mp, s2.mp, s1.id, s2.id, rot, tra, v6);
}
}
Hessian 的 off-diagonal 块 hess(6*i+k, 6*j+k) 的绝对值必须大于 1e-6,否则说明两帧之间缺乏足够的共视约束。噪声为 Hessian 对角的倒数。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2405-2427]
9.3 OctreeGBA: HBA体素地图 / HBA Voxel Map
OctreeGBA 是专为 HBA 设计的八叉树结构,支持多帧点云的联合管理:
class OctreeGBA {
vector<PLV(3)> locals, worlds; // 每帧的局部和世界坐标点
PointCluster pcr_add; // 聚合统计量
int layer, octo_state, wdsize;
OctreeGBA* leaves[8];
};
[VERIFY: VoxelSLAM/src/loop_refine.hpp:273-296]
recut() 方法判断平面并提取优化约束:
void recut(LidarFactor &vox_opt, vector<OctreeGBA*> &oct_buf) {
if(pcr_add.N <= 10) return;
Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> saes(pcr_add.cov());
is_plane = plane_judge(eig_value);
if(is_plane) {
// 提取约束
vox_opt.push_voxel(pcrs, pcr_fix, coe, eig_value, eig_vector, pcr_add);
} else if(layer < max_layer) {
subdivide(oct_buf); // 继续细分
}
}
[VERIFY: VoxelSLAM/src/loop_refine.hpp:358-404]
多线程 recut:
void OctreeGBA_multi_recut(unordered_map<VOXEL_LOC, OctreeGBA*> &feat_map,
LidarFactor &voxhess, int thd_num)
[VERIFY: VoxelSLAM/src/loop_refine.hpp:483-537]
9.4 Top-down: topDownProcess() / Top-down Optimization
void topDownProcess(gtsam::Values &initial, gtsam::NonlinearFactorGraph &graph,
vector<int> &ids, vector<int> &stepsizes)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2231]
流程:
- 等待 bottom-up 完成(
while(gba_flag)) - 将
gba_edges1(单session内)和gba_edges2(跨session)的边添加到图中 - 运行 ISAM2 优化
- 将结果应用到所有 ScanPose 和 Keyframe
for(PGO_Edge &edge: gba_edges1.edges) {
// 添加单session内的HBA约束
}
for(PGO_Edge &edge: gba_edges2.edges) {
// 添加跨session的HBA约束
}
gtsam::ISAM2 isam(parameters);
isam.update(graph, initial);
for(int i = 0; i < 5; i++) isam.update();
gtsam::Values results = isam.calculateEstimate();
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2247-2287]
9.5 thd_globalmapping(): 全局建图线程 / Global Mapping Thread
void thd_globalmapping(ros::NodeHandle &n)
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2485]
该线程持续监听关键帧的生成,每 wdsize(= 10)个关键帧执行一次 bottom-up 优化:
localID.push_back(buf_base);
if(localID.size() < wdsize) continue;
// 提取局部帧
vector<IMUST> xs;
vector<Keyframe*> smp_local;
for(int i: localID) {
xs.push_back(multimap_keyframes[smp_mp]->at(i)->x0);
smp_local.push_back(multimap_keyframes[smp_mp]->at(i));
}
// 执行HBA_add_edge
HBA_add_edge(xs, smp_local, gba_edges1, mps, 1, 2, gba_smp->plptr);
滑动窗口步长为 mgsize(= 5),因此有 50% 的重叠。
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2530-2557]
当系统结束(is_finish && gba_size <= buf_base)时,执行最终的跨session全局优化:
if(total_ba == 1) {
HBA_add_edge(xs, gba_submaps, gba_edges2, cnct_map, total_max_iter, thread_num);
gba_flag = 0;
}
[VERIFY: VoxelSLAM/src/voxelslam.cpp:2559-2581]
附录 A: 配置参数汇总 / Appendix A: Configuration Parameters
BTC 描述子参数 / BTC Descriptor Parameters
| 参数 | 默认值 | 高空飞行值 | 说明 |
|---|---|---|---|
| useful_corner_num_ | 100 | 200 | 保留的角点数 |
| voxel_size_ | 1.0 | 2.0 | BTC体素大小(m) |
| plane_detection_thre_ | 0.01 | 0.05 | 平面检测阈值 |
| proj_plane_num_ | 2 | 1 | 投影平面数 |
| proj_image_resolution_ | 0.5 | 0.5 | 2D投影分辨率(m) |
| proj_dis_max_ | 5 | 10 | 最大投影距离(m) |
| descriptor_near_num_ | 15 | 15 | KNN邻居数 |
| descriptor_min_len_ | 2 | 3 | 最小三角形边长(m) |
| descriptor_max_len_ | 50 | 50 | 最大三角形边长(m) |
| non_max_suppression_radius_ | 2 | 3 | NMS半径(m) |
| std_side_resolution_ | 0.2 | 0.2 | 边长量化分辨率(m) |
[VERIFY: VoxelSLAM/src/BTC.cpp:7-34]
[VERIFY: VoxelSLAM/src/BTC.cpp:39-66]
回环检测参数 / Loop Detection Parameters
| 参数 | 配置键 | 默认值 | 说明 |
|---|---|---|---|
| skip_near_num_ | (硬编码) | 30 | 跳过近邻帧数 |
| candidate_num_ | (硬编码) | 20/100 | 候选帧数量 |
| rough_dis_threshold_ | (硬编码) | 0.01 | 粗匹配距离比例 |
| similarity_threshold_ | (硬编码) | 0.7/0.5 | 二值相似度阈值 |
| icp_threshold_ | (硬编码) | 0.15 | 平面ICP分数阈值 |
| jud_default | Loop/jud_default | 0.45 | BTC匹配分数阈值 |
| icp_eigval | Loop/icp_eigval | 14 | ICP特征值阈值 |
| ratio_drift | Loop/ratio_drift | 0.05 | 漂移比阈值 |
| curr_halt | Loop/curr_halt | 10 | 自身session冷却期 |
| prev_halt | Loop/prev_halt | 30 | 历史session冷却期 |
[VERIFY: VoxelSLAM/src/BTC.cpp:28-34]
[VERIFY: VoxelSLAM/src/voxelslam.cpp:1812-1821]
附录 B: 代码文件索引 / Appendix B: Source File Index
| 文件 | 行数 | 主要内容 |
|---|---|---|
VoxelSLAM/src/BTC.h | 1-331 | BTC数据结构定义、STDescManager类声明 |
VoxelSLAM/src/BTC.cpp | 1-1480 | BTC描述子生成/检索/验证完整实现 |
VoxelSLAM/src/loop_refine.hpp | 1-539 | icp_normal、add_edge、PGO_Edge/Edges、OctreeGBA |
VoxelSLAM/src/voxelslam.cpp:1741-1802 | - | build_graph 位姿图构建 |
VoxelSLAM/src/voxelslam.cpp:1806-2228 | - | thd_loop_closure 回环检测主线程 |
VoxelSLAM/src/voxelslam.cpp:1101-1186 | - | loop_update 回环校正传播 |
VoxelSLAM/src/voxelslam.cpp:1189-1228 | - | keyframe_loading 关键帧加载 |
VoxelSLAM/src/voxelslam.cpp:2231-2317 | - | topDownProcess HBA顶层优化 |
VoxelSLAM/src/voxelslam.cpp:2320-2482 | - | HBA_add_edge 底层约束提取 |
VoxelSLAM/src/voxelslam.cpp:2485-2595 | - | thd_globalmapping HBA建图线程 |
附录 C: 完整调用关系 / Appendix C: Complete Call Graph
main()
|
|-> thread: thd_loop_closure()
| |-> previous_map_read() // 加载历史session
| | |-> read_lidarstate() // 读取位姿
| | |-> GenerateSTDescs() // 生成BTC描述子
| | |-> AddSTDescs() // 添加到数据库
| |
| |-> [主循环] for each ScanPose from buf_lba2loop:
| |-> 关键帧累积 (win_size scans)
| |-> GenerateSTDescs() // 当前帧BTC描述子
| |-> SearchLoop() // 场景检索
| | |-> candidate_selector() // 粗匹配投票
| | |-> candidate_verify() // 几何验证
| | |-> triangle_solver() // SVD求解
| | |-> plane_geometric_verify() // 平面ICP评分
| |
| |-> icp_normal() // 法向量ICP精细验证
| | |-> KdTree nearest neighbor
| | |-> Point-to-plane least squares
| | |-> Eigenvalue validation
| |
| |-> add_edge() // 添加回环边
| |-> build_graph() // 构建位姿图
| |-> ISAM2::update() // GTSAM优化
| |-> set_state() // 应用结果
| |-> loop_detect = 1 // 通知里程计线程
|
|-> thread: thd_globalmapping()
| |-> [主循环] for each Keyframe group:
| |-> OctreeGBA::cut_voxel() // 体素化
| |-> OctreeGBA_multi_recut() // 平面提取
| |-> HBA_add_edge() // 底层约束提取
| | |-> Lidar_BA_Optimizer::damping_iter()
| | |-> Hessian对角线提取边
| |
| |-> [系统结束时] topDownProcess()
| |-> gba_edges1/2 添加到图
| |-> ISAM2优化
| |-> 应用结果到所有session
|
|-> thd_odometry_localmapping() [主线程]
|-> if(loop_detect == 1):
|-> loop_update()
|-> 清除并替换体素地图
|-> 应用dx变换到滑动窗口
|-> 重建体素地图
&spm=1001.2101.3001.5002&articleId=160648902&d=1&t=3&u=9b7a2b238de54ac0a4fe081f85f821c5)
1990

被折叠的 条评论
为什么被折叠?



