4. 点云预处理
各位同学,咱们今天聊聊点云预处理。说实话,这是整个感知管线里最“脏”最“累”的活,但也是决定后续算法成败的关键一步。我见过太多团队,算法模型选得再好,点云预处理没做好,结果就是各种误检、漏检。嗯,咱们今天就把这块硬骨头啃下来。
4.1 为什么要做点云预处理?
激光雷达一帧扫下来,少则几万,多则几十万个点。你想想看,这些点里,有远处的噪声,有近处的冗余,还有地面点、障碍物点混在一起。如果直接把这些原始数据丢给后面的检测算法,别说实时性了,内存都能给你撑爆。
我个人习惯把点云预处理比作“洗菜”。菜不洗干净,炒出来的菜没法吃。点云预处理的核心目标就三个:降采样、去噪、分割。说白了,就是把原始点云变成干净、轻量、有结构的数据。
4.2 体素滤波降采样
体素滤波,英文叫 Voxel Grid Filter。这是我最常用的降采样方法,没有之一。
它的原理其实很简单:把三维空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点或中心点)。体素尺寸越大,降采样越狠,点云越稀疏。
我在项目中遇到过一个问题:体素尺寸设得太小,降采样效果不明显;设得太大,一些细小的障碍物(比如路沿、锥桶)直接被“滤”没了。所以,这个尺寸需要根据实际场景来调。
下面是一个简单的 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(去掉地面以下和过高的点)
// 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 个最近邻的平均距离。如果这个平均距离大于某个阈值(通常是全局均值 + 若干倍标准差),就判定为离群点,直接剔除。
我曾经在雨雾天气测试时发现,统计滤波的参数需要适当放宽。因为雨滴和雾滴本身就会产生大量稀疏点,如果阈值设得太严,会把有用的点也滤掉。
// 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,方便你理解整个管线:
嗯,整个流程就是这样。每一步都有它的意义,缺一不可。你想想看,如果跳过体素滤波,几十万个点直接做地面分割,计算量会非常大;如果跳过统计滤波,那些离群点会让后续的聚类结果一团糟。
好了,点云预处理的内容就讲到这里。记住,预处理做得好,后面的算法才能跑得稳。这是整个感知系统的基石,值得你花时间去调优每一个参数。