3. 点云预处理:体素滤波、直通滤波、统计滤波、半径滤波的原理与实战
点云数据,说白了就是一堆三维坐标点的集合。但现实世界中的激光雷达扫出来的点云,往往带着大量噪声、离群点,而且数据量巨大——一帧64线激光雷达的数据可能就有几十万个点。直接拿这些原始数据去做SLAM?我试过,结果就是地图建得一塌糊涂,定位也飘得厉害。
所以,预处理是第一步,也是至关重要的一步。今天我们就来聊聊四种最常用的点云滤波方法:体素滤波、直通滤波、统计滤波、半径滤波。我会结合我实际项目中的经验,把它们的原理和用法讲透。
3.1 体素滤波(Voxel Grid Filter)
体素滤波,我习惯叫它“降采样神器”。它的核心思想很简单:把三维空间划分成一个个小立方体(体素),每个立方体里只保留一个点(通常是重心点)。
为什么要用体素滤波?
- 数据量太大,计算扛不住。一帧64线激光雷达的点云可能有10万+点,体素滤波后能降到几千点,计算量直接少一个数量级。
- 点云分布不均匀。近处点密,远处点疏,体素滤波能均匀化密度。
- 去除冗余信息。很多点其实是重复的,保留一个就够了。
关键参数:体素大小(leaf size)
体素大小决定了降采样的程度。我一般这样选:
- 室内场景:0.05m ~ 0.1m
- 室外场景:0.2m ~ 0.5m
- 快速原型验证:0.5m(但精度会下降)
体素太小,降采样效果不明显;体素太大,会丢失细节特征。我踩过这个坑——有一次为了追求速度,把体素设成1m,结果墙角特征全没了,回环检测直接失效。
代码实现(PCL):
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 体素大小
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*filtered_cloud);
我的经验:体素滤波后,记得检查一下点云密度。如果降采样后点太稀疏,后续的特征提取会出问题。我一般会保留原始点云的10%~30%作为经验值。
3.2 直通滤波(PassThrough Filter)
直通滤波,说白了就是“切一刀”。你指定一个轴(比如Z轴)和一个范围,只保留这个范围内的点。其他点全部扔掉。
什么时候用直通滤波?
- 去除地面点。激光雷达扫描时,地面点占了很大比例,但SLAM中我们通常只关心障碍物和结构特征。
- 去除远距离噪声。比如50米外的点,信噪比很低,留着反而干扰。
- 限定感兴趣区域(ROI)。比如只处理车辆前方20米内的点云。
我记得有一次做园区自动驾驶,激光雷达装在车顶,扫描范围是360度。但车体本身会遮挡一部分,而且地面点太多。我用直通滤波先切掉Z轴负半轴(地面以下)和Z轴太高(树冠以上)的点,再切掉X轴太远的点,数据量直接减少了一半以上。
代码实现:
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z"); // 沿Z轴滤波
pass.setFilterLimits(0.0, 2.0); // 保留0~2米高度的点
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*filtered_cloud);
注意:直通滤波会改变点云的索引顺序。如果你后续需要用到点云的索引信息(比如做特征匹配),记得保存原始索引映射。我曾经因为这个原因,找了一整天的bug。
3.3 统计滤波(Statistical Outlier Removal, SOR)
统计滤波,我管它叫“离群点杀手”。它的原理是:计算每个点与其k个最近邻的平均距离,如果这个距离超过全局平均距离的某个倍数(标准差倍数),就判定为离群点并剔除。
为什么需要统计滤波?
- 激光雷达本身会产生噪声点,尤其是多路径反射造成的“飞点”。
- 动态物体(如行人、车辆)的边缘点往往不稳定。
- 环境中的灰尘、雨滴也会产生孤立点。
参数设置:
| 参数 | 含义 | 推荐值 |
|---|---|---|
| k(近邻点数) | 计算平均距离时考虑的邻居数量 | 10~50 |
| std_dev(标准差倍数) | 判定阈值,越大越宽松 | 1.0~3.0 |
代码实现:
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(20); // 近邻点数
sor.setStddevMulThresh(2.0); // 标准差倍数
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*filtered_cloud);
避坑指南:我曾经在雨天的数据集上用过统计滤波,结果发现雨滴点被当成离群点剔除了,但同时也误删了一些真实的边缘点。后来我把标准差倍数从2.0调到了3.0,效果才好一些。记住:参数要根据实际场景调,没有万能值。
3.4 半径滤波(Radius Outlier Removal, ROR)
半径滤波和统计滤波有点像,但判定逻辑更直接:对于每个点,检查它周围指定半径内是否有足够多的邻居点。如果邻居点数量少于阈值,就判定为离群点。
半径滤波 vs 统计滤波:
- 统计滤波:基于全局统计特性,适合去除稀疏的孤立点。
- 半径滤波:基于局部密度,适合去除小簇的噪声点。
我个人的习惯是:如果点云整体密度均匀,用统计滤波;如果密度变化很大(比如近处密、远处疏),用半径滤波更靠谱。
代码实现:
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.1); // 搜索半径
ror.setMinNeighborsInRadius(5); // 最少邻居点数
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
ror.filter(*filtered_cloud);
小技巧:半径滤波的搜索半径最好和体素滤波的体素大小保持一致。比如体素滤波用了0.1m,半径滤波的搜索半径也设为0.1m,这样逻辑上更统一。
3.5 四种滤波的对比与选择
说了这么多,到底该用哪个?我整理了一个对比表:
| 滤波方法 | 主要用途 | 计算开销 | 适用场景 |
|---|---|---|---|
| 体素滤波 | 降采样、均匀化密度 | 中等 | 所有场景(必用) |
| 直通滤波 | 去除地面、限定ROI | 低 | 有明确空间范围限制的场景 |
| 统计滤波 | 去除稀疏离群点 | 高 | 点云密度均匀的场景 |
| 半径滤波 | 去除小簇噪声 | 高 | 点云密度变化大的场景 |
在实际项目中,我通常的预处理流程是:
- 先用直通滤波切掉地面和远距离噪声
- 再用体素滤波降采样
- 最后用统计滤波或半径滤波去除离群点
这个顺序不是固定的。你想想看,如果先做统计滤波,数据量太大,计算会很慢。所以先降采样再滤波,效率更高。
3.6 实战经验总结
最后,分享几个我踩过的坑:
- 滤波顺序很重要。先降采样再滤波,能省不少计算时间。
- 参数不是固定的。不同传感器、不同场景,参数都要调。我一般会在数据集上跑一遍,可视化看看效果。
- 不要过度滤波。滤波的目的是去除噪声,不是把点云变成“光杆司令”。保留足够的点,后续的特征提取和匹配才能做好。
- 保存原始数据。我习惯在预处理前备份一份原始点云,万一滤波参数设错了,还能恢复。
嗯,点云预处理就讲到这里。这些方法看起来简单,但用好了,能让你后续的SLAM工作事半功倍。记住:好的预处理,是成功SLAM的一半。
公众号:蓝海资料掘金营,微信deep3321