3. 点云预处理实战:体素滤波降采样、直通滤波(ROI提取)、统计滤波去噪、半径滤波去噪
各位同学,欢迎来到第三讲。
上一章我们聊了激光雷达的选型和数据格式。说实话,拿到原始点云的那一刻,你可能会有点懵——几万甚至几十万个点,密密麻麻,还带着各种噪声。直接拿去做车道线检测?那基本是给自己找麻烦。
所以这一章,我们来点实在的:点云预处理。
我个人的习惯是,拿到点云数据后,先做四件事:降采样、提ROI、去噪、再精去噪。这四步走完,数据才算“干净”,才能喂给后面的算法。
下面我们一个一个来。
3.1 体素滤波降采样:让数据“瘦身”
为什么需要降采样?
你想想看,一个64线激光雷达,每秒能产生130万个点。这么多点,大部分是冗余的。比如路面上的点,相邻两个点之间的距离可能只有几厘米,但它们的几何信息几乎一样。处理这些冗余点,只会拖慢算法速度。
体素滤波,说白了就是用一个个小立方体(体素)把空间划分开,每个体素内只保留一个点(通常是重心点)。
我在项目中遇到过一个问题:体素大小设得太小,降采样效果不明显;设得太大,车道线的细节就丢了。后来我总结了一个经验——对于车道线检测,体素边长设为0.1米到0.2米比较合适。
核心思想:用空间换时间,用精度换速度。
代码实现其实很简单,PCL(Point Cloud Library)里直接有现成的滤波器:
// C++ PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长0.1米
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*cloud_filtered);
嗯,这里要注意:setLeafSize的三个参数分别是X、Y、Z方向的体素边长。如果你的激光雷达是水平安装的,通常三个方向设成一样就行。
3.2 直通滤波(ROI提取):只关心“有用”的区域
降采样之后,下一步就是划定感兴趣区域(ROI)。
为什么?因为一辆车前方50米的路面,和左右两侧的建筑、树木,对车道线检测来说完全是噪声。我们只关心车前方一定范围内的路面。
直通滤波,就是沿着某个坐标轴方向,设定一个范围,只保留这个范围内的点。
我一般会这样设:
- X方向(车辆前方):0米到50米。太近的点(比如车头正下方)没有意义,太远的点稀疏且噪声大。
- Y方向(左右):-10米到10米。一般车道宽度3.5米,左右各10米足够覆盖相邻车道。
- Z方向(高度):-1.5米到0.5米。路面高度设为0,往上0.5米(路沿、低矮障碍物),往下1.5米(路面以下的地面反射)。
小技巧:如果你用的是16线激光雷达,点云比较稀疏,可以把X方向范围缩小到30米以内,否则远处点太少,车道线根本看不清。
代码示例:
// 直通滤波 - 提取ROI
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 50.0); // 保留0到50米
pass.filter(*cloud_roi);
pass.setInputCloud(cloud_roi);
pass.setFilterFieldName("y");
pass.setFilterLimits(-10.0, 10.0);
pass.filter(*cloud_roi);
pass.setInputCloud(cloud_roi);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.5, 0.5);
pass.filter(*cloud_roi);
我曾经犯过一个错误:先做直通滤波再做体素滤波。结果发现ROI外的点虽然被滤掉了,但体素滤波的计算量并没有减少多少。后来我改成先降采样、再提ROI,速度提升了一倍。你想想看,先让数据瘦身,再切掉不要的部分,是不是更合理?
3.3 统计滤波去噪:干掉“离群点”
ROI提取之后,点云里可能还有一些“孤魂野鬼”——比如飞鸟、扬尘、或者激光雷达自身的反射噪声。这些点数量少,但位置随机,对车道线拟合影响很大。
统计滤波的原理很简单:对每个点,计算它到周围K个邻居的平均距离。如果这个平均距离大于某个阈值,就认为它是离群点,删掉。
说白了,就是“看你和邻居们的关系好不好”。如果周围都没人,那你就是噪声。
我习惯的参数设置:
- K(邻居数量):50。这个值不能太小,否则会把一些边缘点误判为噪声;也不能太大,否则计算量爆炸。
- 标准差倍数(stddev_mul):1.0。这个值越小,去噪越狠。对于车道线场景,1.0倍标准差比较合适。
// 统计滤波去噪
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_roi);
sor.setMeanK(50); // 邻居数量
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.filter(*cloud_denoised);
注意:统计滤波对稀疏区域不太友好。比如50米外的路面,点本来就少,统计滤波可能会把一些有用的点也当成噪声删掉。所以如果你发现远处车道线被删没了,可以适当增大K值或减小标准差倍数。
3.4 半径滤波去噪:另一种“社交”策略
统计滤波看的是“平均距离”,而半径滤波看的是“一定半径内有多少个邻居”。
具体来说:对每个点,设定一个半径r,统计这个半径内有多少个点。如果数量少于某个阈值,就认为它是离群点。
这两种滤波有什么区别?
| 滤波方法 | 判断依据 | 适用场景 |
|---|---|---|
| 统计滤波 | 邻居平均距离 vs 全局标准差 | 均匀分布的噪声(如高斯噪声) |
| 半径滤波 | 半径内邻居数量 vs 阈值 | 孤立点、小簇噪声(如飞鸟、扬尘) |
我个人习惯是先做统计滤波,再做半径滤波。统计滤波干掉大片的稀疏噪声,半径滤波再清理那些漏网的小点。
半径滤波的参数设置:
- 半径r:0.2米。这个值要和体素大小匹配。体素是0.1米,半径0.2米意味着每个点周围至少有2-3个体素内的点。
- 最小邻居数:3。也就是说,半径0.2米内如果少于3个点,就删掉。
// 半径滤波去噪
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud_denoised);
rad.setRadiusSearch(0.2); // 搜索半径0.2米
rad.setMinNeighborsInRadius(3); // 最少3个邻居
rad.filter(*cloud_final);
嗯,这里有个坑:如果你把半径设得太大,比如0.5米,那车道线上的点也会被误删,因为车道线本身就很细,半径内可能只有一两个点。所以半径不要超过车道线宽度的一半。
3.5 本章知识体系
为了让你更直观地理解这四步的关系,我画了一张流程图:
这张图很清楚地展示了整个流程:原始点云 → 体素滤波降采样 → 直通滤波提ROI → 统计滤波去噪 → 半径滤波去噪 → 干净点云。每一步的输出,都是下一步的输入。
最后总结一下我的个人经验:
- 体素滤波:0.1米体素,先做,让数据瘦身。
- 直通滤波:X:0-50米,Y:-10到10米,Z:-1.5到0.5米,切出ROI。
- 统计滤波:K=50,stddev=1.0,干掉大片噪声。
- 半径滤波:半径0.2米,最少3个邻居,清理漏网之鱼。
这四步走完,你的点云数据就基本“干净”了。下一章,我们会用这些干净的数据来做地面分割和车道线提取。
嗯,今天就到这里。记住,预处理做得好,后面事半功倍。我在项目里吃过不少预处理的亏,希望你少走弯路。