4、点云滤波实战:直通滤波去除远点、体素滤波降采样、统计滤波去噪点、半径滤波去离群点
各位同学,欢迎来到点云滤波的实战环节。
说实话,滤波这件事,在点云处理里就像吃饭喝水一样平常。你拿到的原始点云,基本不可能直接用的。要么太密,要么太吵,要么远处飘着一堆没用的点。我刚开始做项目那会儿,就吃过这个亏——直接把原始点云丢进配准算法,结果跑了半天,发现全是噪声在打架。
所以今天咱们就把四种最常用的滤波方法,一个一个掰开揉碎了讲清楚。分别是:直通滤波、体素滤波、统计滤波、半径滤波。
4.1 直通滤波:先砍掉没用的远点
直通滤波,说白了就是切一刀。你设定一个范围,在这个范围之外的,全砍掉。
举个例子。你装了个激光雷达在车顶,能扫到前方200米。但你的任务只需要关注前方50米内的障碍物。那50米以外的点,留着干嘛?只会增加计算量。
我个人习惯,拿到点云第一步就是做直通滤波。先把明显没用的区域切掉,后面处理起来轻松很多。
代码实现也很简单,PCL里直接调用 pcl::PassThrough 就行:
// 创建直通滤波对象
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_in);
pass.setFilterFieldName("z"); // 沿Z轴方向滤波
pass.setFilterLimits(0.0, 2.0); // 保留Z在0到2米之间的点
pass.setFilterLimitsNegative(false); // false表示保留范围内的点
pass.filter(*cloud_out);
这里要注意一个细节:setFilterLimitsNegative 这个参数。默认是false,保留范围内的点。如果你设成true,那就变成保留范围外的点了。嗯,这个我踩过坑,有一次想保留地面以上的点,结果设反了,把地面全留下了,空中飘的点全丢了。
4.2 体素滤波:降采样,让点云变轻
体素滤波,是降采样的主力军。你想想看,一个64线激光雷达,一秒钟能产生几十万个点。这么多点,处理起来太慢了。体素滤波就是把这些点装进一个个小立方体(体素)里,每个立方体只保留一个代表点。
这个代表点怎么选?通常是取立方体内所有点的重心,或者直接取中心点。PCL默认用的是重心,效果更好一些。
// 创建体素滤波对象
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud_in);
voxel.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长0.1米
voxel.filter(*cloud_out);
这里有个关键参数:setLeafSize。体素边长设多大,直接决定了降采样后的点云密度。设小了,点云还是很密,降采样效果不明显;设大了,点云变得稀疏,可能会丢失细节。
我在项目中遇到过一个问题:体素边长设得太大,结果把一些细小的障碍物给滤没了。后来我调整到0.05米,才在速度和精度之间找到了平衡。
| 体素边长 | 降采样效果 | 适用场景 |
|---|---|---|
| 0.01m | 几乎无变化 | 高精度建模 |
| 0.05m | 适中 | 一般SLAM、目标检测 |
| 0.1m | 明显稀疏 | 快速预览、大场景 |
| 0.5m | 非常稀疏 | 粗略定位 |
4.3 统计滤波:干掉那些孤立的噪声点
统计滤波,是用来去噪点的。它的原理很简单:计算每个点周围邻居的平均距离。如果一个点离它的邻居们太远了,那它大概率是个噪声点。
为什么会这样?因为正常的点云,点与点之间是连续的。比如一面墙,上面的点密密麻麻挨在一起。但噪声点往往是随机出现的,周围没什么邻居。
// 创建统计滤波对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_in);
sor.setMeanK(50); // 每个点考虑50个最近邻
sor.setStddevMulThresh(1.0); // 标准差倍数阈值
sor.filter(*cloud_out);
两个参数要理解清楚:
- setMeanK:每个点找多少个邻居来计算平均距离。设得越大,计算越慢,但统计结果更稳定。
- setStddevMulThresh:标准差倍数。这个值越小,过滤掉的点越多。一般设1.0到2.0之间。
我曾经在室外场景用过统计滤波,效果非常好。激光雷达扫到树叶上,会产生一些飘忽不定的点,统计滤波能把这些点干净地去掉。但要注意,如果场景本身就很稀疏(比如远距离的点),统计滤波可能会把有用的点也误删掉。
4.4 半径滤波:另一种去离群点的思路
半径滤波和统计滤波有点像,都是去离群点。但思路不同。
半径滤波的做法是:以每个点为中心,画一个半径为r的球。如果这个球里的邻居点数量少于某个阈值,就把这个点删掉。
说白了,就是看这个点周围有没有足够多的兄弟。没有?那就滚蛋。
// 创建半径滤波对象
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud_in);
rad.setRadiusSearch(0.1); // 搜索半径0.1米
rad.setMinNeighborsInRadius(5); // 半径内至少要有5个点
rad.filter(*cloud_out);
这里有两个参数:
- setRadiusSearch:搜索半径。设得越大,每个点能覆盖的范围越大。
- setMinNeighborsInRadius:半径内最少邻居数。设得越大,过滤掉的点越多。
我个人觉得,半径滤波比统计滤波更直观。你直接告诉算法:半径0.1米内少于5个点的,都给我删了。简单粗暴,效果也不错。
- 统计滤波:基于全局统计特性,适合去除均匀分布的噪声。
- 半径滤波:基于局部密度,适合去除稀疏的离群点。
4.5 四种滤波的搭配使用
在实际项目中,很少只用一种滤波。我一般会这样搭配:
- 第一步:直通滤波。先切掉远处和无关区域,减少数据量。
- 第二步:体素滤波。降采样,让点云密度适中。
- 第三步:统计滤波或半径滤波。去噪点,清理离群点。
这个顺序是有讲究的。先做直通滤波,可以减少后续处理的数据量。再做体素滤波,让点云均匀分布,这样统计滤波和半径滤波的效果会更好。你想想看,如果点云密度不均匀,统计滤波的统计结果就不准了。
好了,四种滤波方法就讲到这里。下一章咱们开始聊点云配准,那才是真正考验算法功底的地方。