4. 点云数据预处理:体素滤波降采样、直通滤波、半径滤波。PCL库基础使用。
点云数据,说白了就是一堆三维坐标点的集合。但激光雷达扫出来的原始点云,往往又密又乱,还带着各种噪声。我刚开始做SLAM时,拿着原始点云直接跑匹配,结果定位精度惨不忍睹。后来才明白——预处理做不好,后面全是白忙活。
这一章,我们就来聊聊点云预处理的三个核心操作:体素滤波、直通滤波和半径滤波。同时,我会带你上手PCL库,这是点云处理领域最常用的工具库,没有之一。
4.1 为什么需要点云预处理?
你想想看,一台16线激光雷达,每秒能产生30万个点。如果直接拿这些点去算匹配,计算量巨大,而且很多点其实是没用的。比如,远处的点稀疏且噪声大,地面上的点对定位帮助有限,还有一些孤立的噪点会严重干扰特征提取。
预处理的目的就三个:减量、去噪、提纯。说白了,就是把原始点云里那些「没用」或「有害」的点去掉,留下真正有价值的信息。
核心原则:预处理不是越狠越好。过度滤波会丢失细节,影响后续匹配精度。我一般会先保守地设参数,然后根据效果逐步调整。
4.2 PCL库快速上手
PCL(Point Cloud Library)是点云处理的事实标准。它封装了从滤波、特征提取到配准、分割的全套算法。我个人习惯用PCL 1.8以上版本,配合ROS Melodic或Noetic都很稳定。
先看一个最基础的PCL程序框架:
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/passthrough.h>
#include <pcl/filters/radius_outlier_removal.h>
// 定义点云类型
typedef pcl::PointXYZ PointT;
typedef pcl::PointCloud<PointT> PointCloud;
int main(int argc, char** argv) {
// 创建点云对象
PointCloud::Ptr cloud(new PointCloud);
PointCloud::Ptr cloud_filtered(new PointCloud);
// 加载点云(假设已有数据)
// cloud = ...
// 这里放滤波代码
return 0;
}
嗯,这里要注意:PCL里大量使用智能指针(::Ptr),这是为了避免内存泄漏。刚开始用的时候可能会不习惯,但用多了你会发现这设计确实好。
4.3 体素滤波:给点云「瘦身」
体素滤波的原理很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个重心点。这样,原本密集的点云就变得稀疏了,但整体形状保持不变。
我在项目中遇到过这样的情况:用32线雷达扫描一个房间,原始点云有80万个点。体素滤波后,降到5万个点,但墙角和桌面的轮廓依然清晰。匹配速度提升了10倍以上。
代码实现:
// 体素滤波
pcl::VoxelGrid<PointT> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.05f, 0.05f, 0.05f); // 体素边长5cm
voxel.filter(*cloud_filtered);
std::cout << "滤波前点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;
| 体素大小 | 适用场景 | 效果 |
|---|---|---|
| 0.01m (1cm) | 高精度室内建图 | 几乎不降采样,保留细节 |
| 0.05m (5cm) | 一般SLAM场景 | 推荐值,平衡精度与速度 |
| 0.10m (10cm) | 大场景快速建图 | 点云稀疏,可能丢失小物体 |
| 0.20m (20cm) | 室外远距离 | 仅保留主要结构 |
我的经验:体素大小不要超过你关注的最小物体尺寸的一半。比如你要识别路沿,路沿宽度约15cm,那体素最好设在7cm以内。
4.4 直通滤波:裁剪空间范围
直通滤波,说白了就是「切一刀」。你指定一个坐标轴的范围,落在范围外的点全部砍掉。这在去除远处噪点、裁剪感兴趣区域时非常有用。
比如,激光雷达装在车顶,我们只关心前方20米内的障碍物,那就可以把Z轴(高度)和X轴(前方距离)限制一下:
// 直通滤波 - 限制Z轴范围(去除地面和过高点)
pcl::PassThrough<PointT> pass;
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("z");
pass.setFilterLimits(-0.5, 2.0); // 保留高度-0.5m到2.0m之间的点
pass.filter(*cloud_filtered);
// 直通滤波 - 限制X轴范围(只看前方)
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 20.0); // 保留前方0到20米
pass.filter(*cloud_filtered);
我曾经犯过一个错误:在直通滤波时,忘了设置setFilterLimitsNegative(false),结果把想要的点全滤掉了,保留了外面的点。调试了半天才发现。嗯,这个坑你们别踩。
注意:直通滤波默认保留范围内的点。如果想保留范围外的点,需要调用 pass.setFilterLimitsNegative(true)。另外,多次直通滤波可以组合使用,实现任意形状的裁剪。
4.5 半径滤波:清除离群噪点
激光雷达扫到玻璃、水面或者远距离物体时,会产生一些孤立的噪点。这些点周围没有其他点支撑,一看就是「孤儿」。半径滤波就是专门收拾这些孤儿的。
它的逻辑是:对每个点,检查它周围指定半径内有多少个邻居。如果邻居数量少于阈值,就判定为离群点,直接删除。
// 半径滤波
pcl::RadiusOutlierRemoval<PointT> rad;
rad.setInputCloud(cloud_filtered);
rad.setRadiusSearch(0.1); // 搜索半径10cm
rad.setMinNeighborsInRadius(3); // 至少要有3个邻居
rad.filter(*cloud_filtered);
参数怎么调?我一般这样试:
- 半径:设为体素大小的2-3倍。体素5cm,半径就设10-15cm。
- 最小邻居数:室内场景设2-3,室外稀疏场景设1-2。
你想想看,如果半径设太大、邻居数设太高,那正常的点也可能被误删。如果设太小,噪点又清不干净。这需要根据实际点云密度来微调。
4.6 三种滤波的组合策略
在实际项目中,我通常按这个顺序处理:
- 先直通滤波:快速裁剪掉不关心的区域,减少后续计算量。
- 再体素滤波:降采样,控制点云密度。
- 最后半径滤波:清除降采样后可能出现的离群点。
为什么是这个顺序?因为体素滤波会改变点的分布,如果先做半径滤波,体素化后可能又产生新的离群点。先降采样再清噪,效果更干净。
完整流程代码:
// 1. 直通滤波 - 裁剪空间
pcl::PassThrough<PointT> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.0, 3.0);
pass.filter(*cloud_tmp);
// 2. 体素滤波 - 降采样
pcl::VoxelGrid<PointT> voxel;
voxel.setInputCloud(cloud_tmp);
voxel.setLeafSize(0.05f, 0.05f, 0.05f);
voxel.filter(*cloud_tmp);
// 3. 半径滤波 - 去噪
pcl::RadiusOutlierRemoval<PointT> rad;
rad.setInputCloud(cloud_tmp);
rad.setRadiusSearch(0.15);
rad.setMinNeighborsInRadius(3);
rad.filter(*cloud_filtered);
4.7 避坑指南
做点云预处理这几年,我踩过不少坑。挑几个典型的说说:
- 体素滤波后点云变「方块感」:体素设太大就会这样。如果发现建图结果出现锯齿状边缘,把体素缩小一半试试。
- 直通滤波把目标切没了:有一次我想滤掉地面,Z轴下限设了0.0,结果车停在坡上,半个车身被切掉了。后来我改成动态阈值,根据IMU的俯仰角调整。
- 半径滤波把墙上的点删光了:墙面点本来就很稀疏,半径设太小、邻居数设太高,墙就没了。对于墙面这种平面结构,建议用统计滤波替代半径滤波。
最后说一句:PCL的滤波函数都是线程安全的,但如果你在ROS的回调函数里频繁创建和销毁滤波器对象,会有性能开销。我习惯把滤波器对象声明为类成员,初始化一次,反复使用。
好了,点云预处理就聊到这里。这三种滤波是SLAM预处理阶段最基础也最实用的操作。掌握了它们,你就有了处理原始点云的第一把利器。