激光雷达定位与点云配准工程 - 第2节

2. 点云预处理技术

做激光SLAM这么多年,我踩过最大的坑是什么?

不是算法选型,不是参数调优,而是——点云预处理没做好。

你想想看,原始点云数据就像一块刚从矿里挖出来的石头,杂质多、密度不均、还有运动畸变。直接拿去做配准,结果可想而知。我早期有个项目,定位精度死活提不上去,折腾了两周,最后发现是运动畸变没处理干净。嗯,从那以后,我再也不敢轻视预处理这步了。

核心观点:点云预处理的质量,直接决定了后续配准和定位的上限。预处理做得好,算法事半功倍;预处理糊弄过去,后面全是坑。

点云预处理技术 点云滤波 点云降采样 离群点移除 运动畸变去除 体素滤波 直通滤波 统计滤波 体素降采样 均匀降采样 随机降采样 半径滤波 统计滤波 纯估计法 IMU辅助法 轮式里程计法

2.1 点云滤波:先给数据洗个澡

2.1.1 体素滤波(Voxel Filter)

体素滤波说白了就是用一个小立方体去框点云。每个立方体里只保留一个点,其他点都扔掉。这样点云就变得稀疏了,但整体形状还在。

我个人习惯把体素大小设成 0.1m 到 0.5m 之间。太小了降采样效果不明显,太大了形状会失真。我在做园区无人车项目时,试过把体素设成 1m,结果点云直接变成了一坨一坨的方块,配准根本没法做。

// PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.2f, 0.2f, 0.2f);  // 体素边长 0.2m
voxel_filter.filter(*cloud_filtered);

我的经验:体素大小不要固定死。室内场景用 0.1m,室外开阔场景用 0.3m 左右。如果你用的是 16 线激光雷达,建议从 0.2m 开始试。

2.1.2 直通滤波(PassThrough Filter)

直通滤波就是切掉不需要的部分。比如你只关心前方 30 米内的障碍物,那就把 30 米以外的点全扔掉。

我一般会在 x、y、z 三个方向上都做一次直通滤波。特别是 z 轴,地面点和天花板点经常是噪声来源。记得有一次,我在室内做定位,点云里全是天花板上的管道,配准时老是收敛到错误位置。后来加了 z 轴直通滤波,问题就解决了。

// PCL 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.0, 2.0);  // 保留 z 在 -1m 到 2m 之间的点
pass.filter(*cloud_filtered);

注意:直通滤波的阈值要根据传感器安装位置来定。激光雷达装在车顶和装在车头,z 轴范围完全不同。别照搬别人的参数。

2.1.3 统计滤波(Statistical Outlier Removal)

统计滤波是用来剔除稀疏的离群点的。它的原理很简单:计算每个点周围 k 个邻居的平均距离,如果这个距离太大,就认为它是离群点。

我常用的参数是 k=50,标准差倍数设为 1.0。太严格会把正常点也删掉,太宽松又起不到效果。嗯,这个需要根据点云密度来调。

// PCL 统计滤波示例
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);          // 邻居点数
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.filter(*cloud_filtered);

2.2 点云降采样:别让数据撑爆内存

16 线激光雷达一帧大概 3 万个点,64 线能到 10 万以上。直接处理这么多点,计算量太大了。降采样就是在保持形状的前提下减少点数。

常用的降采样方法有三种:

方法 原理 适用场景 我常用的参数
体素降采样 每个体素内取重心 通用场景 0.1m - 0.3m
均匀降采样 每隔 n 个点取一个 快速预览 n=2 或 n=3
随机降采样 随机抽取 实时性要求高 保留 30% - 50%

我个人最常用的是体素降采样。它既能控制点数,又能保持点云的几何结构。均匀降采样虽然快,但容易丢失细节。随机降采样嘛,我一般只在调试时用。

2.3 离群点移除:把"飞点"揪出来

激光雷达偶尔会打出一些莫名其妙的点,比如突然出现在空中的一个孤立点。这些点就是离群点,也叫"飞点"。它们对配准的影响很大,必须去掉。

离群点移除有两种主流方法:

  • 半径滤波:如果一个点周围指定半径内没有足够多的邻居,就删掉它。我一般设半径 0.5m,最少邻居数 3 个。
  • 统计滤波:前面讲过了,基于距离统计来判断。

我曾经在一个雨天的测试中,发现点云里全是雨滴造成的噪声点。统计滤波一上,干净多了。但要注意,雨滴噪声和真实障碍物有时很难区分,参数调不好会把远处的车也删掉。

避坑指南:离群点移除的参数不要设得太激进。我曾经为了追求"干净"的点云,把统计滤波的标准差倍数设成 0.5,结果把墙上的点都删掉了一半,配准时直接崩了。

2.4 运动畸变去除:让点云"停"下来

这是预处理里最容易被忽视、但影响最大的一步。

激光雷达扫描一帧需要时间。比如 16 线雷达,一帧大概 100ms。在这 100ms 里,如果机器人正在运动,那么点云里的每个点其实是在不同位置采集的。直接拿这帧点云去做配准,相当于把不同时刻的坐标强行对齐,结果可想而知。

运动畸变去除的核心思想:把每个点都补偿到同一时刻。通常补偿到这一帧的起始时刻或结束时刻。

常用的方法有三种:

  1. 纯估计法:用匀速运动模型估计每个点的位姿。简单但精度有限。
  2. IMU 辅助法:用 IMU 的角速度和线加速度来补偿。精度高,但需要 IMU 数据。
  3. 轮式里程计法:用轮式里程计的速度来补偿。适合轮式机器人。

我个人最推荐 IMU 辅助法。在 AGV 项目中,我用 IMU 辅助去畸变后,配准精度从 10cm 提升到了 3cm。效果非常明显。

// 运动畸变去除伪代码(IMU辅助法)
for each point in cloud:
    // 计算该点相对于帧起始的时间偏移
    time_offset = point.timestamp - frame_start_time
    
    // 用IMU数据插值得到该时刻的位姿
    pose_at_point = interpolate_pose(imu_data, time_offset)
    
    // 将点补偿到帧起始时刻的坐标系下
    point_compensated = pose_at_point.inverse() * point.original

重要提醒:运动畸变去除的前提是你知道每个点的时间戳。有些低端激光雷达不提供逐点时间戳,那就只能用帧间匀速模型来近似了。这种情况下,精度会打折扣。

小结

点云预处理不是花哨的技术,但它是整个 SLAM 系统的地基。地基没打好,上面盖的房子再漂亮也会塌。

我给你的建议是:

  • 先做直通滤波,去掉明显不需要的区域
  • 再做体素降采样,控制点数
  • 然后做统计滤波,去掉离群点
  • 最后做运动畸变去除,补偿运动带来的误差

这个流程我用了好几年,在多个项目上都验证过。你可以直接拿来用,再根据你的传感器和场景微调参数。