4. 局部重对齐:利用ICP或NDT将漂移后的点云/位姿重新对齐到局部地图

好,咱们接着聊。上一节我们搞定了全局重对齐,说白了就是让机器人知道自己大概在哪儿。但全局重对齐有个问题——它太慢了,而且对初始位姿要求高。你想想看,如果机器人只是稍微漂了一点,你非得把它拉到全局地图里重新匹配一遍,那计算量可就大了去了。

所以,实际工程里我更常用的是局部重对齐。它的思路很简单:既然漂移量不大,那我就在当前位姿附近,用局部地图做一次精细的配准。嗯,这里要注意,局部重对齐的核心是两个算法——ICP和NDT。我分别讲讲我的使用心得。

4.1 ICP:最经典的点对点配准

ICP,全称Iterative Closest Point,迭代最近点。它的原理说白了就是:把两片点云叠在一起,找最近的点对,然后算一个旋转平移矩阵,让这些点对的距离最小。然后重复这个过程,直到收敛。

我在项目中遇到过一个问题:ICP对初始位姿非常敏感。如果初始偏差太大,它很容易陷入局部最优。所以我的习惯是,先用里程计或者IMU给一个差不多的初值,再用ICP做精细调整。

核心要点:
  • ICP适用于点云密度高、重叠度大的场景
  • 对初始位姿要求高,建议配合滤波或里程计使用
  • 计算量随点云数量线性增长,需要做降采样

下面是一个简单的ICP实现示例,我用的是PCL库:

// 输入:源点云source,目标点云target,初始位姿init_guess
// 输出:优化后的变换矩阵final_transform

pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
icp.setInputSource(source);
icp.setInputTarget(target);
icp.setMaxCorrespondenceDistance(0.5);  // 最大对应距离
icp.setMaximumIterations(50);           // 最大迭代次数
icp.setTransformationEpsilon(1e-8);     // 收敛阈值
icp.setEuclideanFitnessEpsilon(1e-6);   // 均方误差阈值

pcl::PointCloud<pcl::PointXYZ> aligned;
icp.align(aligned, init_guess);

if (icp.hasConverged()) {
    final_transform = icp.getFinalTransformation();
    std::cout << "ICP收敛,得分:" << icp.getFitnessScore() << std::endl;
} else {
    std::cerr << "ICP未收敛,请检查初始位姿" << std::endl;
}
我的小技巧: 实际项目中,我会把ICP的迭代次数设得比较保守(30-50次),然后配合一个粗配准步骤。粗配准可以用NDT或者基于特征的匹配,这样ICP的成功率会高很多。

4.2 NDT:更鲁棒的分布配准

NDT,Normal Distributions Transform,正态分布变换。它和ICP的思路不太一样。ICP是点对点,NDT是把点云划分成网格,每个网格里统计点的正态分布。然后配准时,不是找最近点,而是最大化源点云在目标点云网格中的概率密度。

说白了,NDT更「软」一些。它对噪声和局部形变的容忍度更高。我记得有一次做室外场景的定位,点云里有很多树叶和灌木,ICP死活配不准,换成NDT一下就搞定了。

NDT的优势:
  • 对初始位姿的容忍度比ICP高
  • 计算效率更高,适合实时系统
  • 对噪声和局部形变更鲁棒

NDT的代码实现也很直接:

// 输入:源点云source,目标点云target,初始位姿init_guess
// 输出:优化后的变换矩阵final_transform

pcl::NormalDistributionsTransform<pcl::PointXYZ, pcl::PointXYZ> ndt;
ndt.setInputSource(source);
ndt.setInputTarget(target);
ndt.setResolution(1.0);              // 网格分辨率,单位米
ndt.setStepSize(0.1);                // 步长
ndt.setTransformationEpsilon(1e-8);  // 收敛阈值
ndt.setMaximumIterations(35);        // 最大迭代次数

pcl::PointCloud<pcl::PointXYZ> aligned;
ndt.align(aligned, init_guess);

if (ndt.hasConverged()) {
    final_transform = ndt.getFinalTransformation();
    std::cout << "NDT收敛,得分:" << ndt.getFitnessScore() << std::endl;
} else {
    std::cerr << "NDT未收敛,请调整参数" << std::endl;
}
避坑指南: 我曾经在隧道场景里吃过NDT的亏。隧道里点云结构单一,NDT的网格里全是平面分布,导致优化时在某个方向上自由度不足。后来我加了IMU约束才解决。所以,NDT的网格分辨率一定要根据场景调整,太粗了丢失细节,太细了计算量大。

4.3 ICP vs NDT:怎么选?

这个问题我经常被问到。我的回答是:没有绝对的好坏,只有合不合适。下面这张表是我个人经验总结的:

对比项 ICP NDT
对初始位姿的容忍度 低(需要较好的初值) 高(可以容忍一定偏差)
计算效率 中等(随点云数量线性增长) 高(网格化后计算量可控)
对噪声的鲁棒性 低(点对点匹配,噪声影响大) 高(概率分布,天然抗噪)
适用场景 室内结构化环境、高精度需求 室外非结构化环境、实时系统
参数调优难度 低(参数少,直观) 中等(网格分辨率、步长需要调)

我个人习惯是:室内用ICP,室外用NDT。但也不是绝对的。如果室内场景点云质量不好,我也会切到NDT试试。你想想看,工程上最重要的是稳定,不是炫技。

4.4 局部重对齐的完整流程

好了,理论讲完了,咱们看看实际怎么用。下面这张图是我自己画的局部重对齐流程图,你可以参考一下:

局部重对齐流程图 输入:当前点云 + 初始位姿 降采样(体素滤波) 选择算法:ICP 或 NDT ICP迭代配准 NDT网格配准 输出:优化后的位姿

流程其实不复杂。拿到当前点云后,先做降采样,减少计算量。然后根据场景选ICP还是NDT。配准完成后,检查收敛性和得分。如果得分太高,说明可能配错了,这时候我会回退到上一帧的位姿,或者触发全局重对齐。

工程建议: 实际部署时,我会把ICP和NDT都实现,然后根据场景动态切换。比如在走廊里用ICP,到了开阔区域自动切到NDT。这样既保证了精度,又兼顾了效率。

好了,局部重对齐就讲到这里。核心就是记住:ICP适合精细调整,NDT适合鲁棒配准。具体用哪个,得看你的场景和需求。下一节我们会聊聊怎么把这些技术整合到一个完整的定位系统里。

可可精品资料库