4、点云预处理:体素滤波降采样、直通滤波、统计滤波去噪、点云分割(地面与非地面)

各位同学,咱们今天聊聊点云预处理。说实话,这是整个感知管线里最“脏”最“累”的活,但也是决定后续算法成败的关键一步。我见过太多团队,算法模型选得再好,点云预处理没做好,结果就是各种误检、漏检。嗯,咱们今天就把这块硬骨头啃下来。

4.1 为什么要做点云预处理?

激光雷达一帧扫下来,少则几万,多则几十万个点。你想想看,这些点里,有远处的噪声,有近处的冗余,还有地面点、障碍物点混在一起。如果直接把这些原始数据丢给后面的检测算法,别说实时性了,内存都能给你撑爆。

我个人习惯把点云预处理比作“洗菜”。菜不洗干净,炒出来的菜没法吃。点云预处理的核心目标就三个:降采样、去噪、分割。说白了,就是把原始点云变成干净、轻量、有结构的数据。

核心目标: 在保留关键几何信息的前提下,大幅减少数据量,剔除噪声,分离地面与非地面点云。

4.2 体素滤波降采样

体素滤波,英文叫 Voxel Grid Filter。这是我最常用的降采样方法,没有之一。

它的原理其实很简单:把三维空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点或中心点)。体素尺寸越大,降采样越狠,点云越稀疏。

我在项目中遇到过一个问题:体素尺寸设得太小,降采样效果不明显;设得太大,一些细小的障碍物(比如路沿、锥桶)直接被“滤”没了。所以,这个尺寸需要根据实际场景来调。

我的经验: 对于高速公路场景,体素边长 0.2m ~ 0.5m 比较合适;对于城区复杂场景,建议 0.1m ~ 0.2m。你可以先设 0.2m 试试,再根据效果微调。

下面是一个简单的 PCL 实现示例:

// C++ PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud_input);
voxel_filter.setLeafSize(0.2f, 0.2f, 0.2f); // 体素边长 0.2m
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>());
voxel_filter.filter(*cloud_filtered);

4.3 直通滤波

直通滤波,PassThrough Filter,名字很直白——就是沿着某个坐标轴,只保留指定范围内的点。

为什么要做这个?你想想看,激光雷达能扫到几百米外的点,但那些点对我们自动驾驶来说有用吗?大部分情况下没用。我们只关心车辆周围几十米范围内的障碍物。远处的点不仅没用,还会引入噪声。

我个人习惯在 X、Y、Z 三个方向上都做直通滤波。比如:

  • X 方向(车辆前方): 保留 -50m ~ 80m(根据车速调整)
  • Y 方向(车辆左右): 保留 -30m ~ 30m
  • Z 方向(高度): 保留 -2m ~ 5m(去掉地面以下和过高的点)
注意: 直通滤波的参数一定要和你的传感器安装位置、车辆尺寸匹配。我曾经见过一个团队,Z 方向下限设成了 0,结果把地面点全滤掉了,地面分割直接没法做。
// C++ PCL 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("z");
pass.setFilterLimits(-2.0, 5.0); // 保留高度 -2m 到 5m 的点
pass.filter(*cloud_filtered);

4.4 统计滤波去噪

统计滤波,Statistical Outlier Removal,SOR。这个滤波器专门用来干掉那些“离群点”——也就是周围邻居很少的孤立点。

为什么会存在这些离群点?可能是激光打到玻璃上产生的反射噪声,可能是空气中的灰尘,也可能是传感器本身的测量误差。这些点数量不多,但如果不处理,后续的聚类算法很容易把它们当成小障碍物,产生误检。

统计滤波的原理:对每个点,计算它到 k 个最近邻的平均距离。如果这个平均距离大于某个阈值(通常是全局均值 + 若干倍标准差),就判定为离群点,直接剔除。

我曾经在雨雾天气测试时发现,统计滤波的参数需要适当放宽。因为雨滴和雾滴本身就会产生大量稀疏点,如果阈值设得太严,会把有用的点也滤掉。

参数建议: 邻居数 k 通常设为 50 ~ 100,标准差倍数设为 1.0 ~ 2.0。干燥天气用 1.5,雨雾天气可以放宽到 2.5。
// C++ PCL 统计滤波示例
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setMeanK(50);          // 邻居数
sor.setStddevMulThresh(1.5); // 标准差倍数
sor.filter(*cloud_filtered);

4.5 点云分割:地面与非地面

这是预处理里最关键的一步。地面点如果不分离,后续的障碍物检测会非常痛苦——因为地面点会跟障碍物点连在一起,聚类算法根本分不清。

常用的地面分割方法有很多,比如:

  • RANSAC 平面拟合: 假设地面是一个平面,用 RANSAC 算法拟合出平面方程,平面上的点就是地面点。
  • 射线坡道法: 按角度划分射线,在每条射线上找坡度变化点,坡度突变处就是地面与障碍物的分界。
  • 基于深度学习的方法: 用 PointNet++ 等网络直接分割地面,精度高但计算量大。

我个人在工程中最常用的是 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.setInputCloud(cloud_filtered);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.2); // 距离平面 0.2m 以内的点视为地面
seg.segment(*inliers, *coefficients);
分割后的处理: 地面点可以用于可行驶区域分析,非地面点则送入后续的障碍物检测模块。两者都很重要,不要扔掉任何一个。

4.6 知识体系总览

下面这张图总结了点云预处理的完整流程,我把它画成了 SVG,方便你理解整个管线:

点云预处理流程 原始点云 体素滤波降采样 直通滤波(X/Y/Z范围裁剪) 统计滤波去噪 点云分割(地面 vs 非地面)

嗯,整个流程就是这样。每一步都有它的意义,缺一不可。你想想看,如果跳过体素滤波,几十万个点直接做地面分割,计算量会非常大;如果跳过统计滤波,那些离群点会让后续的聚类结果一团糟。

避坑指南: 我曾经在项目里跳过直通滤波,结果激光雷达扫到了车顶上的树枝,被误检成了障碍物,车辆直接急刹。从那以后,我再也不敢省这一步了。

好了,点云预处理的内容就讲到这里。记住,预处理做得好,后面的算法才能跑得稳。这是整个感知系统的基石,值得你花时间去调优每一个参数。

课后思考: 如果车辆行驶在隧道里,点云预处理流程需要做哪些调整?特别是直通滤波和地面分割的参数,你会怎么改?
可可精品资料库