4、点云预处理:体素滤波、统计滤波、半径滤波、点云降采样与地面去除
点云预处理,说白了就是给原始点云数据「洗个澡」。
我刚开始做自动驾驶感知那会儿,拿到一帧64线激光雷达的数据,直接往上跑算法。结果呢?内存爆了,帧率掉到2帧,检测结果一塌糊涂。后来才明白——原始点云太「脏」了,不处理根本没法用。
这一章,咱们就把点云预处理的几个核心操作捋清楚。你想想看,一辆车在路上跑,每秒产生几十万个点,里面还有噪声、离群点、地面点……不处理干净,后面的检测、分割、跟踪全是白搭。
4.1 体素滤波:给点云「瘦身」
体素滤波是最常用的降采样方法。它的思路很简单:把三维空间切成一个个小立方体(体素),每个体素里只保留一个点。
我个人习惯用这个参数:体素边长0.1米。为什么?因为0.1米对于大多数自动驾驶场景来说,既能保留足够细节,又能把点云数量降到原来的十分之一甚至更少。
核心思想:用体素中心点(或重心)代替体素内所有点。
# PCL 中的体素滤波实现
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长
voxel_filter.filter(*cloud_filtered);
我在项目中遇到过一个问题:体素边长设得太小(比如0.05米),降采样效果不明显,点云还是很大。设得太大(比如0.5米),远处的行人直接变成一个小方块,检测器根本认不出来。
嗯,这里要注意:体素滤波会丢失精细结构。如果你做的是近距离的精细检测,体素边长别超过0.2米。
4.2 统计滤波:干掉「野点」
统计滤波用来去除离群点。什么叫离群点?就是那些孤零零飘在空中的点,可能是多路径反射造成的,也可能是传感器噪声。
它的原理其实很直观:计算每个点周围k个邻居的平均距离。如果这个距离太大,说明它周围没什么邻居,那它大概率是个「野点」。
我的经验:k值一般取50,标准差倍数取1.0。太严格(倍数太小)会把正常点也干掉,太宽松(倍数太大)又滤不干净。
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50); // 邻居数量
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.filter(*cloud_filtered);
我曾经在高速场景下吃过亏。当时把标准差倍数设成了0.5,结果远处的车辆点云被当成噪声全滤掉了。后来才发现——远处的点本来就稀疏,统计滤波对它们不太友好。
为什么会这样?因为远处的激光点间距大,每个点的邻居距离自然就大,容易被误判为离群点。所以,如果你处理的是远距离场景,建议把标准差倍数放宽到1.5甚至2.0。
4.3 半径滤波:另一种「清场」方式
半径滤波和统计滤波有点像,但判定标准不同。它看的是:在某个半径范围内,点的数量够不够。
说白了,如果某个点周围半径r内,邻居数量少于某个阈值,那就把它干掉。
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.5); // 搜索半径 0.5米
ror.setMinNeighborsInRadius(10); // 最少需要10个邻居
ror.filter(*cloud_filtered);
我个人习惯用半径滤波来处理一些「孤岛点」。比如在隧道里,墙壁反射回来的点很密集,但偶尔会有几个点飘到路中间——半径滤波一刷,干干净净。
注意:半径滤波的计算量比统计滤波大。因为每个点都要做一次半径搜索。点云量大的时候,建议先用体素滤波降采样,再做半径滤波。
4.4 点云降采样:不止体素一种方法
降采样,就是把点云变稀疏。体素滤波是最常用的,但不是唯一的。
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 体素滤波 | 空间网格化,每个格子保留一个点 | 通用场景,最推荐 |
| 均匀采样 | 按索引均匀抽取 | 快速降采样,但可能丢失结构 |
| 随机采样 | 随机保留部分点 | 简单粗暴,不推荐用于感知 |
| 曲率采样 | 保留曲率大的点(边缘、角点) | 特征提取场景 |
你想想看,如果只是做可视化,随机采样就够了。但如果是做检测、分割,我强烈建议用体素滤波——它能保持点云的几何结构,不会把关键特征丢掉。
4.5 地面去除:把路「掀掉」
地面去除是自动驾驶感知里绕不开的一步。为什么?因为地面点占了点云的一半以上,而且对障碍物检测来说,地面就是「背景噪音」。
最经典的方法是RANSAC平面拟合。它的思路是:从点云里随机选三个点,拟合一个平面,然后看有多少点落在这个平面上。重复这个过程,找到点数最多的那个平面——那就是地面。
pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
seg.setInputCloud(cloud);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.2); // 距离阈值 0.2米
seg.segment(*inliers, *coefficients);
我在项目中遇到过一个问题:坡道。平路用RANSAC没问题,但遇到上坡下坡,一个平面根本拟合不了地面。后来我改用分段拟合——把点云按距离分成几段,每段单独拟合地面。
避坑指南:我曾经把距离阈值设成0.5米,结果路沿、低矮障碍物全被当成地面去掉了。后来改成0.2米,效果好了很多。记住:阈值越小,地面越「干净」,但也可能漏掉一些地面点。
还有一种方法是基于高度阈值。简单粗暴:把Z值低于某个阈值的点全当成地面。这个方法在平坦道路上效果不错,但遇到颠簸路面就抓瞎了。
我个人习惯的做法是:先用RANSAC粗提取地面,再用高度阈值做精细筛选。两者结合,效果最好。
小结
点云预处理,说白了就是四个字:去噪、降维。体素滤波负责降采样,统计滤波和半径滤波负责去噪,地面去除负责「清场」。
嗯,这一章的内容就到这里。下一章咱们聊聊点云配准——怎么把多帧点云拼到一起。那又是另一个有意思的话题了。