1. 为什么多激光雷达标定这么“磨人”?

大家好,我是老张,在机器人感知这行摸爬滚打了十来年,经手调试过的激光雷达少说也有上百台。今天想和大家掏心窝子聊聊一个让无数工程师头疼的问题——多激光雷达的标定。你可能已经试过网上能找到的各种开源工具包,比如直接用两帧点云做ICP(迭代最近点)配准,或者用NDT(正态分布变换)方法。上手跑起来好像挺简单,但真到了实际项目里,尤其是在自动驾驶小车或者大型移动机器人上,是不是经常发现标出来的结果“飘忽不定”?这次准了,下次重启系统又偏了;在实验室里好好的,一到走廊或者空旷场地就完全对不上了。

这背后的核心痛点,其实在于单帧点云的“信息量不足”和“特征稀疏”。想象一下,你手里有两张用不同相机拍的、角度还不太一样的照片,每张照片都只拍到了物体的一小部分,而且画面还有点模糊。现在让你只靠这两张照片,去精确计算出两个相机之间的位置和角度关系,是不是感觉非常困难,甚至有点“瞎猜”的成分?单帧激光点云面临的就是类似的问题。尤其是在使用VLP-16这类16线雷达时,每一帧扫描到的三维点本来就少,如果环境再空旷一点(比如只有平坦的地面和几面墙),点云里能用来做匹配的角点、边缘等稳定特征就更少了。这时候强行做ICP,算法很容易陷入局部最优,给你一个看起来合理但实际上误差很大的外参矩阵。

我最初也是在这个坑里摔了好几次。后来发现,与其在“贫瘠”的单帧数据上死磕,不如换个思路:我们能不能先给每个雷达各自建一个更丰富、更稳定的“地图”出来,然后用这两张“高清地图”来做匹配? 这就是我们今天要深入探讨的“基于LOAM与ICP融合”的核心思想。简单说,就是先让每个雷达“自己玩一会”,用LOAM这类算法,通过一小段时间的扫描,构建出一个融合了多帧信息的、富含线和面特征的局部稠密地图。这个地图,就像是给每个雷达准备了一份详尽的“自我介绍”。然后,我们再让这两份“自我介绍”坐下来好好对一对,找出它们之间的空间关系。实测下来,这个方法的稳定性和精度提升,真的不是一点半点。

2. 核心武器:LOAM建图如何为ICP“打好地基”?

那么,LOAM到底做了什么,让它生成的地图比单帧点云更适合做标定呢?这得从LOAM(Lidar Odometry and Mapping)的原理说起。它不是简单地把点云堆叠起来,而是包含了一个精巧的两步走策略:高频的里程计估计和低频的建图优化。

2.1 LOAM的特征提取与地图构建

LOAM在每一帧点云进来时,会干一件非常重要的事:提取线特征和面特征。它会计算每个点的曲率,把曲率大的点(比如墙角、桌沿)归类为边缘点(线特征),把曲率小的点(比如墙面、地面)归类为平面点(面特征)。这个过程,相当于从一堆杂乱的三维点中,挑出了那些最稳定、最具代表性的“关键点”。

接下来,在里程计部分,LOAM会快速估计雷达自身的运动,主要是通过当前帧的特征点和上一帧的特征点进行匹配。而在建图部分,它则会把当前帧的特征点,去和前面所有帧累积形成的一个“小地图”进行匹配。这个“小地图”才是我们标定的宝藏。因为它不是某一瞬间的 snapshot,而是一小段路径上(即使机器人是静止的,雷达自身微小的振动和扫描也能形成微小运动)所有优质特征的融合。一个墙角特征可能在多帧数据里都被重复观测到,从而在地图中被强化;一些偶然的噪声点则会被自然地过滤掉。

最终,我们为每个雷达得到的地图(map1和map2),是一个特征更丰富、结构更清晰、噪声更低的局部点云。你可以把它理解成一张高精度的“素描图”,上面清晰地画出了环境中的线条和块面。用这张“素描图”去和另一张“素描图”做比对,自然比用两张模糊的“速写”(单帧点云)要容易得多,也准确得多。

2.2 实际操作:用A-LOAM快速获取标定用地

理论说完了,咱们上点干货。怎么快速得到这两张高质量的“素描图”呢?我强烈推荐使用 A-LOAM,这是一个非常简洁高效的LOAM开源实现,对新手非常友好。

假设我们有两个Velodyne VLP-16雷达,话题名分别是 /velodyne_points1/velodyne_points2。操作流程其实很清晰:

  1. 分别录制数据:最好让机器人或设备在标定场地保持静止几十秒。分别录制两个雷达的数据包。

    # 录制雷达1数据
    rosbag record /velodyne_points1 -O lidar1.bag
    # 录制雷达2数据
    rosbag record /velodyne_points2 -O lidar2.bag
    

    注意,虽然是静止,但雷达内部的旋转镜片在动,所以依然能扫描到周围环境,形成微小视差,这对于LOAM建图是足够的。

  2. 分别运行A-LOAM建图:我们需要修改A-LOAM的启动文件,使其订阅对应的雷达话题。以雷达1为例,通常修改 laserMapping.cpp 或对应的launch文件中的点云话题。

    # 启动A-LOAM,并指定参数,假设我们修改了launch文件叫vlp16_1.launch
    roslaunch aloam_velodyne vlp16_1.launch
    # 回放数据包
    rosbag play lidar1.bag --clock
    

    播放结束后,A-LOAM会输出最后的全局地图。我们需要将这个地图保存下来。A-LOAM通常会在终端打印出保存路径,或者你可以用ROS的 pcl_ros 工具包来保存点云。

    # 假设A-LOAM发布的地图话题是 /laser_cloud_map
    rosrun pcl_ros pointcloud_to_pcd input:=/laser_cloud_map _prefix:=./map1_
    

    这样会生成一系列 map1_xxxx.pcd 文件,最后一个就是完整的局部地图。对雷达2重复上述步骤,得到 map2.pcd

注意:这里有个小坑。A-LOAM默认是给移动状态设计的,在完全静止时,里程计部分可能因为缺乏运动激励而出问题。我的经验是,可以轻微地晃动一下设备(或者如果设备可移动,让它做非常微小的平移旋转),或者直接使用其建图功能而不过度依赖其前端里程计。也有同行会选用LeGO-LOAM,因为它对地面分割更友好,在平坦环境下更稳健。

3. ICP配准:如何让两张“地图”完美对齐?

拿到 map1.pcdmap2.pcd 后,我们就进入了关键的第二步:配准。这一步的目标是找到一个最优的刚体变换矩阵 T,使得将 map2 通过 T 变换后,能与 map1 尽可能重合。这就是ICP算法的本职工作。

3.1 理解ICP的精髓与“死穴”

ICP的原理不复杂,它迭代执行两个步骤:1. 找对应点:对于变换后的map2中的每个点,在map1里找最近的点,组成点对。2. 求解最优变换:基于这些点对,计算出一个能使整体距离最小的旋转和平移矩阵。然后应用这个变换,再重复步骤1,直到收敛。

听起来很完美,对吧?但它的“死穴”就在于第一步——最近点搜索。如果两个点云的初始位置相差太远,那么“最近点”的假设就完全错了。算法会基于一堆错误的点对去计算变换,结果就是越迭代越错,直接掉进局部最优的陷阱里出不来。这就是为什么几乎所有ICP应用都强调需要一个良好的初始变换估计

在我们这个流程里,LOAM地图已经极大地改善了问题的“条件”。因为地图特征丰富,即使初始值有些偏差,正确特征点对之间强大的几何约束,也能把ICP“拉回”正确的方向。但提供一个尽可能好的初始值,依然能大幅减少迭代次数,提高成功率。

3.2 实战ICP:代码与参数详解

这里我以PCL库中的ICP算法为例,展示核心代码段和参数设置。假设我们已经从文件读取了 map1map2

#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/registration/icp.h>
#include <pcl/visualization/pcl_visualizer.h>

typedef pcl::PointXYZI PointT;

int main() {
    // 1. 加载点云地图
    pcl::PointCloud<PointT>::Ptr cloud_map1(new pcl::PointCloud<PointT>);
    pcl::PointCloud<PointT>::Ptr cloud_map2(new pcl::PointCloud<PointT>);
    pcl::io::loadPCDFile("map1.pcd", *cloud_map1);
    pcl::io::loadPCDFile("map2.pcd", *cloud_map2);

    // 2. 设置初始变换矩阵(这是关键!)
    // 假设通过粗略测量,得到雷达2相对于雷达1的初始位姿
    // 例如:x=0.5米, y=0.0, z=0.2, roll=0°, pitch=0°, yaw=45°
    Eigen::Matrix4f init_guess = Eigen::Matrix4f::Identity();
    float yaw = 45.0f * M_PI / 180.0f;
    init_guess(0, 0) = cos(yaw); init_guess(0, 1) = -sin(yaw);
    init_guess(1, 0) = sin(yaw); init_guess(1, 1) = cos(yaw);
    init_guess(0, 3) = 0.5; // x 平移
    init_guess(1, 3) = 0.0; // y 平移
    init_guess(2, 3) = 0.2; // z 平移

    // 3. 配置ICP
    pcl::IterativeClosestPoint<PointT, PointT> icp;
    icp.setInputSource(cloud_map2); // 源点云,待变换的
    icp.setInputTarget(cloud_map1); // 目标点云
    icp.setMaximumIterations(50);   // 最大迭代次数,通常50-100足够
    icp.setTransformationEpsilon(1e-8); // 变换矩阵变化阈值,小于此值则停止
    icp.setEuclideanFitnessEpsilon(1e-6); // 均方误差变化阈值
    icp.setMaxCorrespondenceDistance(0.5); // 最大对应点距离,超过此距离的点对不计入计算。这个参数非常重要!开始时可以设大点(如1.0),后期可减小以提高精度。
    icp.setRANSACIterations(0); // 可选项,如果点云噪声大,可设置RANSAC迭代来剔除错误匹配
    icp.align(*cloud_map2, init_guess); // 执行配准,并传入初始猜测

    // 4. 输出结果
    if (icp.hasConverged()) {
        std::cout << "ICP converged with score: " << icp.getFitnessScore() << std::endl;
        Eigen::Matrix4f transformation = icp.getFinalTransformation();
        std::cout << "Final transformation matrix:\n" << transformation << std::endl;
        // 这个 transformation 就是雷达2到雷达1的外参矩阵 T_2_to_1
    } else {
        std::cout << "ICP did not converge." << std::endl;
    }

    // 5. 可视化(可选,但强烈推荐)
    pcl::visualization::PCLVisualizer viewer("ICP Result");
    viewer.setBackgroundColor(0, 0, 0);
    // 将map1显示为白色
    pcl::visualization::PointCloudColorHandlerCustom<PointT> white(cloud_map1, 255, 255, 255);
    viewer.addPointCloud<PointT>(cloud_map1, white, "map1");
    // 将配准后的map2显示为绿色
    pcl::PointCloud<PointT>::Ptr aligned_cloud(new pcl::PointCloud<PointT>);
    pcl::transformPointCloud(*cloud_map2, *aligned_cloud, icp.getFinalTransformation());
    pcl::visualization::PointCloudColorHandlerCustom<PointT> green(aligned_cloud, 0, 255, 0);
    viewer.addPointCloud<PointT>(aligned_cloud, green, "map2_aligned");
    viewer.spin();
    return 0;
}

几个参数的经验之谈

  • setMaxCorrespondenceDistance:这是最重要的参数之一。它像一个“匹配搜索半径”。开始可以设置得宽松一些(比如0.5-1.0米),确保有足够的点对参与计算。当ICP初步收敛后,可以逐步减小这个距离(比如0.2-0.3米),进行更精细的配准,这能有效剔除一些边缘的错误匹配。
  • setMaximumIterationssetTransformationEpsilon:前者是保险,防止无限循环;后者是精度控制。通常1e-8是一个很严格的标准,意味着变换矩阵的变化已经微乎其微。
  • 初始变换 init_guess:哪怕你只知道两个雷达大致是平行安装的,只给一个粗略的平移和很小的角度,也比不给(单位矩阵)强十倍。用卷尺量一下两个雷达安装支架的大致相对位置,填进去,成功率会飙升。

4. 从理论到现实:在VLP-16上的实测与避坑指南

方法听起来很美,但在真实的Velodyne VLP-16上表现如何呢?我拿我们实验室的机器人平台做过大量测试,这里分享一些最直接的观察和必须避开的“坑”。

4.1 效果对比:单帧ICP vs. LOAM地图ICP

我专门做了一个对比实验。在同一个房间,固定两个VLP-16雷达,间距约0.6米,角度差约15度。

  • 方案A(传统):同步采集一帧点云,直接进行ICP配准(同样提供粗略初始值)。
  • 方案B(本文):采集15秒数据,分别用A-LOAM建图,然后用地图进行ICP配准。

结果肉眼可见

  • 方案A:成功率和稳定性极低。十次里可能只有两三次能收敛到一个看似合理的位置,而且每次收敛的结果之间相差能达到十几厘米甚至几十度。在特征较少的走廊区域,几乎百分之百失败。
  • 方案B:十次实验十次成功,收敛后的外参矩阵非常稳定。变换后的点云重合度很高,特别是墙面、桌角等线性特征,几乎严丝合缝。

为什么?因为VLP-16单帧点云太稀疏了。一帧数据大概只有3万个点,分布在16条扫描线上。在稍远距离(比如5米外),物体上的点可能就寥寥数个。而LOAM构建的地图,融合了数百帧数据,将稀疏的点“编织”成了连续的线和面,特征密度和可靠性有了数量级的提升。ICP算法在这样“信息充沛”的数据上工作,自然游刃有余。

4.2 你必须知道的局限性

当然,没有“银弹”,这个方法也有它的适用边界:

  1. 需要短时静态数据:建图阶段,机器人或雷达组需要保持十几秒的相对静止。这对于安装阶段的标定不是问题,但无法实现“在线实时标定”。
  2. 对初始值仍有依赖:虽然依赖度大大降低,但一个完全离谱的初始值(比如把180度猜成0度)仍然可能导致失败。良好的工程实践是,安装时就用物理工具(水平尺、量角器)测量一个大概值。
  3. 环境要求:LOAM建图需要环境有一定的几何结构特征。在一个完全空旷、毫无特征的广场上,或者一条无限长的、特征完全一致的隧道里,LOAM本身建图都会困难,更不用说后续标定。理想的标定场地是有墙角、柱状物、不同高度平面的室内或半室外环境。
  4. 雷达型号一致性:这个方法在相同型号的雷达间(如双VLP-16)效果最好。如果是一个32线雷达和一个16线雷达,由于点云密度和噪声水平差异巨大,直接匹配可能有问题。可能需要先对稠密点云进行降采样滤波,使两者密度接近。

4.3 精度验证与评估

标定完了,怎么知道准不准?不能光靠肉眼看。我常用的定量评估方法有:

  • 重投影误差:在另一个新的、未用于标定的数据集上,将雷达2的点云用标定得到的外参变换到雷达1坐标系,观察同一物体的点云是否重合。可以手动选取一些特征点(如尖锐的墙角),计算它们在不同点云中的距离均值。
  • 闭环检测:如果系统是移动的,可以让机器人走一个闭环。用标定后的多雷达数据一起做一次轻量级的SLAM,看看轨迹的闭合误差。闭合误差小,说明标定参数准确。
  • 与高精度工具对比:如果有全站仪等高精度测量设备,可以测量几个靶球在双雷达坐标系下的实际坐标,与标定变换后的坐标进行对比。这是最权威的方法,但成本也高。

在我最近的一个仓储机器人项目里,采用这个方法对车体前后两个VLP-16进行标定,最终通过闭环轨迹评估,平移误差稳定在2厘米以内,旋转误差小于0.5度。这对于后续的融合感知、障碍物检测已经足够了。

5. 进阶思考:如何让标定流程更自动化、更鲁棒?

走到这一步,我们已经有了一个稳定可用的标定方案。但如果你需要频繁标定(比如开发不同型号的机器人),或者想把它集成到产品化流程中,还可以做一些优化。

思路一:自动化初始值估计。我们可以不依赖手动测量。比如,在静止建图阶段,两个雷达的LOAM里程计虽然可能漂移,但它们各自估计的起始位姿可以提供一个非常粗略的相对关系(因为都从原点开始)。又或者,可以用一些基于特征匹配的全局注册算法(如FPFH + SAC-IA)先跑一遍,为ICP提供一个自动计算的初始值,进一步减少人工干预。

思路二:多帧联合优化。我们目前只用了一小段数据建图后做一次ICP。更鲁棒的做法是采集多段不同位置、不同角度的数据,每一段都产生一个外参估计,然后对这些估计值进行平均或采用图优化的方式,求出一个最优解。这能有效对抗某次数据采集不佳带来的偶然误差。

思路三:融合其他廉价传感器。如果设备上恰好有IMU(惯性测量单元),哪怕是很便宜的型号,也可以利用它。在静止建图时,IMU可以提供重力方向,这直接约束了雷达的滚转角和俯仰角。我们可以把这个强约束作为先验信息加入到ICP的优化过程中,或者直接用于修正初始猜测的pitch和roll,这样只剩下x, y, z和yaw四个自由度需要优化,问题又简化了不少。

这些进阶玩法,都是在解决了“从无到有”的稳定性问题后,自然延伸出来的优化方向。核心思想不变:用更丰富、更高质量的数据(地图替代单帧),来攻克复杂环境下的标定难题。这套LOAM+ICP的融合思路,我不仅在激光雷达互标上用,在雷达与相机、不同型号相机的标定上,也做过类似的变种尝试,核心逻辑是相通的——好的输入是成功的一半

最后,所有的代码、配置文件以及测试用的数据包,我都整理放在了GitHub上。你可以在仓库里找到完整的可运行demo,以及我踩坑过程中记录的各种参数设置笔记。实践出真知,遇到问题欢迎一起探讨。标定这条路没有终点,但选对了方法,至少能让我们的每一步都走得更踏实。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐