4. 点云数据预处理:体素滤波、直通滤波、离群点移除
做SLAM这么多年,我越来越觉得一个道理:预处理做得好,后端优化没烦恼。你想想看,原始激光雷达一帧数据动不动就十几万个点,里面还夹杂着各种噪声、飞点、甚至树叶晃动的干扰。你要是直接拿这些数据去做特征提取,那结果基本就是灾难。
所以今天咱们聊聊点云预处理的三板斧:体素滤波、直通滤波、离群点移除。这三招用好了,你的特征匹配质量能提升一个档次。
核心观点:预处理不是简单的“降采样+去噪”,而是根据传感器特性和场景需求,有策略地保留有效信息。我见过太多人上来就一顿暴力降采样,结果把墙角的特征点全滤没了——这属于典型的“把洗澡水连同孩子一起倒掉”。
4.1 直通滤波:先切出你关心的区域
直通滤波,说白了就是按坐标轴切一刀。比如你装了个16线激光雷达在车顶,车前方50米以外的东西你根本不需要——那些点只会增加计算量,还容易引入噪声。
我在项目中遇到过一件事:有次在隧道里跑SLAM,隧道壁的回波特别强,导致远处出现了大量“鬼影点”。后来我加了个直通滤波,把距离限制在30米以内,问题直接解决。
// PCL 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_in);
pass.setFilterFieldName("z"); // 沿Z轴裁剪
pass.setFilterLimits(-1.0, 3.0); // 保留高度 -1m 到 3m 的点
// pass.setFilterLimitsNegative(true); // 如果设为true,则保留范围外的点
pass.filter(*cloud_filtered);
我的习惯:直通滤波通常放在预处理的第一步。先砍掉明显没用的区域,后续的体素滤波和离群点移除计算量能减少30%~50%。
常用的裁剪维度:
- 距离(Range):比如只保留 0.5m ~ 50m 的点。太近的点有传感器盲区噪声,太远的点稀疏且不可靠。
- 高度(Z轴):地面机器人通常保留 -1m ~ 3m,无人机则根据飞行高度灵活调整。
- 角度(水平/垂直FOV):有些雷达背后有安装支架的回波,直接按角度切掉。
4.2 体素滤波:降采样但不丢结构
体素滤波是SLAM预处理里最常用、也最容易被用错的一个工具。它的原理很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心或中心点)。
你想想看,一帧32线雷达的点云可能有12万个点,但其中大量点都落在同一个平面上。体素滤波能把这些冗余点压缩掉,同时保持几何结构基本不变。
我曾经踩过的坑:体素大小设得太大(比如0.5m),结果把墙角的拐角特征直接抹平了。后来做特征匹配时发现角点数量锐减,定位精度从5cm掉到了20cm。所以体素大小要根据场景来调——室内0.05m~0.1m,室外0.1m~0.2m比较合理。
// PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud_filtered); // 接在直通滤波之后
voxel.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长0.1米
voxel.filter(*cloud_voxel);
体素滤波的几个关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Leaf Size(体素边长) | 0.05 ~ 0.2 m | 越小保留细节越多,但计算量也越大 |
| Downsample All Data | True | 是否同时对点云的法向量、颜色等信息降采样 |
| Minimum Points per Voxel | 1(默认) | 体素内至少多少个点才保留,设>1可进一步去噪 |
一个实用技巧:如果你做的是实时SLAM,体素滤波的耗时跟点云数量成正比。我一般把体素大小设成动态的——当帧率掉到10Hz以下时,自动增大体素尺寸来保证实时性。
4.3 离群点移除:把“飞点”和“孤点”干掉
激光雷达的离群点,说白了就是那些莫名其妙出现在空中的点。产生原因很多:雨滴反射、灰尘、传感器噪声、甚至是飞鸟。这些点如果不处理,特征匹配时会产生大量错误对应。
最常用的方法是统计滤波(Statistical Outlier Removal)。它的逻辑很直观:对每个点,计算它到k个最近邻的平均距离。如果这个平均距离超过全局均值一定标准差,就判定为离群点。
// PCL 统计滤波示例
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_voxel);
sor.setMeanK(50); // 考虑50个近邻点
sor.setStddevMulThresh(1.0); // 标准差倍数阈值
sor.filter(*cloud_clean);
参数怎么调?我个人的经验:
- MeanK(近邻数量):一般设20~50。设太小了容易把正常点误判为离群点,设太大了计算量飙升。
- StddevMulThresh(标准差倍数):1.0~2.0之间。值越小,剔除得越狠。室内场景我常用1.0,室外开阔场景可以放宽到1.5。
嗯,这里要注意:统计滤波对“孤立小簇”效果很好,但对“大片噪声区域”基本无效。比如整片树叶被风吹动产生的密集噪点,统计滤波会认为它们是“正常点”。这种情况我一般会结合半径滤波(Radius Outlier Removal)来用。
半径滤波是另一种思路:如果某个点周围指定半径内点数少于阈值,就干掉它。适合处理稀疏的飞点。
// 半径滤波示例
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud_voxel);
rad.setRadiusSearch(0.2); // 搜索半径0.2米
rad.setMinNeighborsInRadius(5); // 至少要有5个邻居
rad.filter(*cloud_clean);
4.4 预处理流程的推荐顺序
这三步的顺序其实有讲究。我建议的流程是:
- 先直通滤波:快速砍掉无用区域,减少后续计算量。
- 再体素滤波:降采样,让点云分布均匀。
- 最后离群点移除:在降采样后的点云上做统计滤波,效果更好、速度更快。
为什么最后做离群点移除?因为体素滤波本身就有一定的去噪效果——孤立的离群点所在的体素里往往只有它自己,降采样后自然就被干掉了。所以先降采样,再对剩下的点做精细去噪,效率最高。
一句话总结:预处理不是流水账,每一步都有它的目的。直通滤波管“范围”,体素滤波管“密度”,离群点移除管“质量”。三者配合好了,你的特征提取才能站在一个干净的数据基础上。
公众号:蓝海资料掘金营,微信deep3321