3、点云预处理:点云滤波、降采样与地面分割
各位同学,欢迎来到第三章。
上一章我们聊了聊点云数据是怎么来的,以及它长什么样。这一章,咱们要动真格的了——处理点云。
你想想看,激光雷达一秒钟扫出几十万个点,这里面有噪声、有远距离的稀疏点、有地面、有车辆、有行人……如果一股脑全丢给后面的感知算法,那计算量直接爆炸,而且效果还差。所以,预处理这一步,说白了就是“去粗取精”。
我个人习惯把点云预处理分成三大块:滤波、降采样、分割。咱们一个一个来。
核心思路: 先滤掉噪声和无关区域,再降低数据量,最后把地面和非地面分开。顺序很重要,别搞反了。
3.1 点云滤波:把脏东西筛出去
滤波,就是过滤掉我们不想要的数据。在自动驾驶场景里,常见的“脏东西”有:
- 离群点:莫名其妙飘在空中的孤立点,通常是传感器噪声。
- 远距离点:超过100米甚至200米的点,反射率低、精度差,基本没用。
- 自车车身点:激光雷达扫到自己车身上的点,必须去掉。
常用的滤波方法有三种,我按使用频率排个序:
3.1.1 直通滤波(PassThrough Filter)
这个最简单,也最常用。说白了就是“切一刀”。
比如,我只关心车前50米范围内的障碍物,那就把x方向(车辆前进方向)上大于50米的点全部扔掉。同样,z方向(高度)上,高于车顶或低于地面的点也可以切掉。
我在项目中遇到过一个坑:直通滤波的坐标系一定要搞清楚。有一次我把x和y搞反了,结果把左右两侧的车全滤掉了,正前方的车反而留着……嗯,调试了半小时才发现。
// C++ 示例:PCL 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z"); // 沿Z轴(高度)滤波
pass.setFilterLimits(-1.5, 3.0); // 保留 -1.5m 到 3.0m 之间的点
pass.filter(*cloud_filtered);
小技巧: 直通滤波的阈值不要设得太死。比如地面高度,不同路况下可能有起伏,留点余量。
3.1.2 体素滤波(Voxel Filter)
体素滤波,其实更常用于降采样。但它的另一个功能是“去噪”。
原理很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点)。这样,同一个体素内的多个点就被合并了。
这能起到滤波作用吗?能。因为噪声点往往是孤立的,如果某个体素内只有一个点,那这个点大概率是噪声,可以直接扔掉。
// C++ 示例:PCL 体素滤波(用于去噪)
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.1f, 0.1f, 0.1f); // 体素大小 10cm
voxel.filter(*cloud_filtered);
3.1.3 半径滤波(Radius Outlier Removal)
这个滤波器,专门对付“孤魂野鬼”。
它的逻辑是:对于每个点,检查它周围指定半径内有多少个邻居。如果邻居数量少于某个阈值,就认为它是离群点,删掉。
我曾经在高速场景下用过半径滤波。当时发现,如果半径设得太小(比如0.3米),远处的稀疏点会被误删;如果设得太大(比如2米),计算量又上去了。后来我取了个折中:0.5米半径,至少需要3个邻居。
// C++ 示例:PCL 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud);
rad.setRadiusSearch(0.5); // 搜索半径 0.5米
rad.setMinNeighborsInRadius(3); // 至少3个邻居
rad.filter(*cloud_filtered);
注意: 半径滤波对近处密集点效果好,但对远处稀疏点要小心。远处的一辆车可能只有几个点,半径滤波可能会把它当成噪声删掉。
3.2 点云降采样:给数据“瘦身”
滤波之后,点云还是太密。尤其是近处的点,密密麻麻的,计算量太大。
降采样的目的,就是在不损失太多信息的前提下,减少点的数量。最常用的方法就是——体素网格降采样。
你没看错,和刚才的体素滤波用的是同一个算法。但目的不同:刚才是为了去噪,现在是为了降采样。
// C++ 示例:体素网格降采样
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setLeafSize(0.2f, 0.2f, 0.2f); // 20cm 体素,降采样力度较大
sor.filter(*cloud_downsampled);
体素大小怎么选?我个人的经验是:
| 场景 | 推荐体素大小 | 说明 |
|---|---|---|
| 高速公路 | 0.2m - 0.3m | 远处目标小,体素太大容易丢失细节 |
| 城市道路 | 0.1m - 0.2m | 行人、自行车需要较高分辨率 |
| 停车场/园区 | 0.05m - 0.1m | 近距离、高精度需求 |
我的习惯: 先设一个较大的体素(比如0.3m),看看效果。如果目标检测漏检了,再逐步调小。别一上来就设0.05m,那跟没降采样差不多。
3.3 点云分割:地面 vs 非地面
这是预处理里最关键的一步。
为什么要把地面分割出来?因为地面上的点太多了,占了点云总量的30%-50%。如果不分割,后面的障碍物检测、聚类算法会被地面点严重干扰。
常用的地面分割方法有很多,比如RANSAC平面拟合、基于射线的方法、基于高度图的方法。在Autoware里,最常用的是基于RANSAC的平面拟合。
3.3.1 RANSAC地面分割原理
RANSAC(随机采样一致性)的思路很直接:
- 随机选三个点,拟合一个平面。
- 计算所有点到这个平面的距离。
- 距离小于阈值的点,认为是“内点”(地面点)。
- 重复以上步骤,找到内点最多的那个平面。
说白了,就是“投票选出最像地面的那个平面”。
// C++ 示例:PCL RANSAC 地面分割
pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE); // 平面模型
seg.setMethodType(pcl::SAC_RANSAC); // RANSAC方法
seg.setMaxIterations(100); // 最大迭代次数
seg.setDistanceThreshold(0.2); // 距离阈值 0.2米
seg.setInputCloud(cloud_downsampled);
seg.segment(*inliers, *coefficients);
避坑指南: 我曾经在坡道上吃过亏。RANSAC默认假设地面是平面,但坡道是斜面。如果坡度太大,RANSAC会把坡道上的车也当成地面点。后来我改用基于射线的地面分割,效果才好起来。
3.3.2 提取非地面点
分割出地面点之后,剩下的就是非地面点——也就是我们真正关心的障碍物点。
// 提取非地面点(障碍物)
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud_downsampled);
extract.setIndices(inliers);
extract.setNegative(true); // 提取非地面点
extract.filter(*cloud_obstacle);
3.4 本章小结
好了,这一章的内容就这些。总结一下:
- 滤波:直通滤波切范围,半径滤波去噪声,体素滤波可兼用。
- 降采样:体素网格是王道,体素大小看场景。
- 地面分割:RANSAC最常用,但坡道要小心。
预处理做得好,后面的感知模块才能跑得稳。我见过太多同学一上来就调检测参数,结果发现是预处理没做好——嗯,白费功夫。
下一章,咱们聊聊点云聚类,把一个个障碍物从点云里“抠”出来。