3、点云预处理:点云滤波、降采样与地面分割

各位同学,欢迎来到第三章。

上一章我们聊了聊点云数据是怎么来的,以及它长什么样。这一章,咱们要动真格的了——处理点云。

你想想看,激光雷达一秒钟扫出几十万个点,这里面有噪声、有远距离的稀疏点、有地面、有车辆、有行人……如果一股脑全丢给后面的感知算法,那计算量直接爆炸,而且效果还差。所以,预处理这一步,说白了就是“去粗取精”。

我个人习惯把点云预处理分成三大块:滤波、降采样、分割。咱们一个一个来。

核心思路: 先滤掉噪声和无关区域,再降低数据量,最后把地面和非地面分开。顺序很重要,别搞反了。

点云预处理核心流程 原始点云 第一步:点云滤波 体素滤波 | 直通滤波 | 半径滤波 第二步:点云降采样 体素网格降采样(Voxel Grid) 第三步:点云分割(地面 vs 非地面) 输出:干净的非地面点云 —— 送给后续感知模块

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(随机采样一致性)的思路很直接:

  1. 随机选三个点,拟合一个平面。
  2. 计算所有点到这个平面的距离。
  3. 距离小于阈值的点,认为是“内点”(地面点)。
  4. 重复以上步骤,找到内点最多的那个平面。

说白了,就是“投票选出最像地面的那个平面”。

// 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最常用,但坡道要小心。

预处理做得好,后面的感知模块才能跑得稳。我见过太多同学一上来就调检测参数,结果发现是预处理没做好——嗯,白费功夫。

下一章,咱们聊聊点云聚类,把一个个障碍物从点云里“抠”出来。

可可精品资料库