3. 体素滤波实战:PCL库体素滤波实现、参数调优技巧、在激光SLAM中的典型应用
3.1 体素滤波到底在干什么?
体素滤波,说白了就是给点云“降采样”。
你想想看,激光雷达一秒钟扫几十万个点出来。这么多点,直接拿去跑SLAM,CPU直接冒烟。我刚开始做定位的时候,就吃过这个亏——点云没处理就往里灌,结果前端匹配卡成PPT。
体素滤波的思路其实很简单:
- 把三维空间切成一个个小立方体(体素)
- 每个小立方体里,只保留一个代表点
- 其他点全部扔掉
嗯,就是这么粗暴。但效果出奇的好。
核心思想:用空间换时间。牺牲一点点精度,换来几十倍的效率提升。
3.2 PCL库体素滤波实现
PCL里实现体素滤波,主要用 pcl::VoxelGrid 这个类。我直接上代码,你们感受一下:
#include <pcl/filters/voxel_grid.h>
// 输入点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 输出点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
// 创建体素滤波器对象
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
// 设置体素大小(单位:米)
sor.setLeafSize(0.1f, 0.1f, 0.1f);
// 执行滤波
sor.filter(*cloud_filtered);
std::cout << "原始点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;
代码就这么几行。但真正关键的,是那个 setLeafSize 的参数怎么设。
我的习惯:先设0.1米跑一遍,看看点数降了多少。如果降得太狠(比如只剩1%),就改小一点。如果降得不够,就改大一点。
3.3 参数调优技巧
体素大小这个参数,调起来其实有门道。我踩过不少坑,给你们总结一下:
| 场景 | 推荐体素大小 | 说明 |
|---|---|---|
| 室内SLAM | 0.05m - 0.1m | 室内环境结构细节多,体素不能太大 |
| 室外SLAM | 0.1m - 0.3m | 室外空旷,点云稀疏,体素可以大一些 |
| 快速建图 | 0.2m - 0.5m | 追求速度,牺牲精度 |
| 高精度定位 | 0.02m - 0.05m | 需要保留更多细节 |
为什么会这样?
室内环境,墙壁、柱子、家具这些结构,体素太大就把细节全抹掉了。我做过一个项目,在走廊里跑SLAM,体素设了0.3米,结果走廊墙壁直接变成了一坨点云,匹配根本对不上。
室外环境就不一样了。空旷的地方,点本来就少,体素设小一点反而没什么影响。
我曾经踩过的坑:体素大小设得太大,导致两个相邻的墙面被合并成一个平面。前端匹配时,明明在转弯,算法以为还在直行。定位直接飘了。
3.4 在激光SLAM中的典型应用
体素滤波在SLAM里,主要有三个用途:
3.4.1 前端匹配前的预处理
这是最常见的用法。不管是ICP还是NDT,点云数量越少,匹配越快。我一般会在匹配前做一次体素滤波,把点数控制在几千到一万左右。
举个例子:
// 前端匹配前的预处理
pcl::VoxelGrid<pcl::PointXYZ> pre_filter;
pre_filter.setLeafSize(0.1f, 0.1f, 0.1f);
// 对当前帧滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr current_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pre_filter.setInputCloud(current_scan);
pre_filter.filter(*current_filtered);
// 对地图滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr map_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pre_filter.setInputCloud(map_cloud);
pre_filter.filter(*map_filtered);
// 然后做ICP匹配
pcl::IterativeClosestPoint<pcl::PointXYZ, pcl::PointXYZ> icp;
icp.setInputSource(current_filtered);
icp.setInputTarget(map_filtered);
// ...
3.4.2 地图构建时的降采样
建图的时候,如果不做降采样,地图会越来越大。跑个半小时,地图就几个G了。
我习惯在建图过程中,每插入一帧点云,就对全局地图做一次体素滤波。这样地图大小能控制在合理范围内。
注意:建图时的体素大小,一般要比前端匹配时大一些。因为地图需要长期保存,太大了内存扛不住。
3.4.3 回环检测前的点云压缩
回环检测通常需要把当前帧和历史帧做匹配。如果不压缩,每帧几万个点,匹配一次要好几秒。
我一般会在回环检测前,把体素大小设到0.2米左右。这样点数能降到几百个,匹配速度飞快。
// 回环检测前的压缩
pcl::VoxelGrid<pcl::PointXYZ> loop_filter;
loop_filter.setLeafSize(0.2f, 0.2f, 0.2f);
pcl::PointCloud<pcl::PointXYZ>::Ptr loop_cloud(new pcl::PointCloud<pcl::PointXYZ>);
loop_filter.setInputCloud(current_scan);
loop_filter.filter(*loop_cloud);
// 用压缩后的点云做回环检测
// ...
3.5 知识体系总览
下面这张图,是我自己总结的体素滤波在SLAM中的完整流程:
3.6 避坑指南
最后,分享几个我实战中踩过的坑:
- 体素大小不要用固定值:不同场景、不同距离,体素大小应该动态调整。我一般会根据点云密度自动计算。
- 注意体素中心点的选择:PCL默认用体素内所有点的重心作为代表点。但如果你需要保留原始点的精确位置,可以用
setFilterFieldName指定保留哪个点。 - 体素滤波不是万能的:如果点云本身就很稀疏,体素滤波反而会丢失信息。这时候可以考虑用随机采样。
我的建议:体素滤波是SLAM预处理的第一步,但不是唯一的一步。后面还要结合离群点滤波、地面滤波等,才能得到干净的点云。
好了,体素滤波的实战就讲到这里。记住一句话:参数调得好,SLAM跑得稳。参数调不好,定位到处飘。