2. 点云预处理技术:体素滤波、直通滤波、半径滤波与统计滤波的原理与实现
大家好,欢迎来到点云预处理这一讲。
说实话,在自动驾驶感知这条路上摸爬滚打这么多年,我最大的体会就是:原始点云数据,真的没法直接用。你想想看,一帧64线激光雷达的数据,动辄十几万个点,里面还夹杂着各种噪声、离群点、地面反射杂波。你要是直接把这些数据扔给后面的检测算法,那结果基本就是灾难。
所以,预处理这一步,说白了就是给数据“洗个澡”,把脏东西去掉,把有用的信息留下来。今天我们就来聊聊四种最常用的滤波方法:体素滤波、直通滤波、半径滤波和统计滤波。我会结合我自己的项目经验,把它们的原理、适用场景和代码实现都讲透。
核心观点:预处理不是简单的“过滤”,而是为了在保留关键几何特征的前提下,大幅降低数据量,同时剔除噪声。这一步做得好,后面的跟踪算法能省一半的力气。
2.1 体素滤波(Voxel Grid Filter)
体素滤波,我个人认为是预处理里最常用、也最基础的一招。它的思路其实很简单:把三维空间划分成一个个小立方体(体素),然后每个体素内只保留一个点,通常是体素内所有点的重心,或者直接取中心点。
为什么要这么做?
你想想看,激光雷达扫描的时候,近处的物体点云非常密集,一个行人身上可能就有上千个点。但说实话,对于目标跟踪来说,我们不需要这么高的密度。体素滤波能均匀地降低点云密度,同时保持物体的几何形状。
我的经验:在项目中,我习惯把体素大小设为0.1m到0.2m之间。太小了降采样效果不明显,太大了会把小物体(比如行人)给“抹平”了。有一次我在高速场景测试,把体素设成了0.5m,结果一辆摩托车直接被滤成了几个点,跟踪直接跟丢了。嗯,这个坑我踩过。
代码实现(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);
这里有个细节要注意:setLeafSize的三个参数分别对应x、y、z方向的体素尺寸。如果你的场景是水平方向为主(比如道路),可以适当把z方向的尺寸设大一点,这样能保留更多的垂直结构信息。
2.2 直通滤波(PassThrough Filter)
直通滤波,说白了就是“切一刀”。你指定一个坐标轴的范围,在这个范围之外的点全部砍掉。这招在提取ROI(感兴趣区域)时特别好用。
我记得有一次做城市道路场景,激光雷达装在车顶,扫描范围是360度的。但真正有用的区域其实只有前方100米、左右各30米。用直通滤波把x轴限制在0~100米,y轴限制在-30~30米,数据量直接减少了70%。
适用场景:
- 去除车体自身点云(比如车顶、引擎盖上的反射)
- 提取特定高度范围内的点云(比如只保留地面以上0.5m到2.5m的行人区域)
- 限制检测距离(比如只关注前方80米内的目标)
注意:直通滤波是“硬裁剪”,边界处可能会把目标的一部分切掉。比如一个行人刚好站在80米边界上,你设了80米的距离限制,那这个行人可能只保留了一半的点云。我建议在边界处留一点余量,比如设成85米。
代码实现(PCL):
// 直通滤波 - 沿x轴方向裁剪
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 80.0); // 保留0到80米
pass.filter(*cloud_filtered);
2.3 半径滤波(Radius Outlier Removal)
半径滤波,它的逻辑很直观:对于每个点,检查它周围指定半径内有多少个邻居点。如果邻居点数量少于某个阈值,就认为这个点是孤立噪声,直接干掉。
为什么会需要这个?激光雷达在远距离或者边缘处,经常会产生一些“飘”出来的孤立点。这些点既不是真实物体,也不是地面反射,纯粹是噪声。半径滤波就是专门对付它们的。
参数设置建议:
| 场景 | 搜索半径 | 最小邻居数 |
|---|---|---|
| 近距离(<30m) | 0.3m | 3 |
| 中距离(30-60m) | 0.5m | 2 |
| 远距离(>60m) | 1.0m | 1 |
避坑指南:我曾经在雨雾天气测试时,发现半径滤波把远处的车辆点云也给滤掉了。后来一查,是因为雨雾导致点云稀疏,远处的车辆本来就没几个点,半径滤波一过,全没了。所以天气不好的时候,我会把最小邻居数调低到1或者2。
代码实现(PCL):
// 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad_filter;
rad_filter.setInputCloud(cloud);
rad_filter.setRadiusSearch(0.5); // 搜索半径0.5米
rad_filter.setMinNeighborsInRadius(3); // 至少3个邻居
rad_filter.filter(*cloud_filtered);
2.4 统计滤波(Statistical Outlier Removal)
统计滤波,比半径滤波更“聪明”一点。它不是用固定的半径和阈值,而是基于整个点云的统计特性来做判断。
具体做法是:计算每个点到它最近k个邻居的平均距离。然后,计算所有点的平均距离的均值和标准差。如果一个点的平均距离超出了“均值 + 标准差 × 倍数”的范围,就认为它是离群点。
说白了,它是在问:“这个点跟周围的点比起来,是不是太‘孤单’了?”
参数说明:
- k(邻居数): 一般取20-50。太小了统计不稳定,太大了计算量暴增。
- stddev_mul(标准差倍数): 一般取1.0到2.0。倍数越小,过滤越严格。
我的习惯:在城区场景,我通常设k=30,stddev_mul=1.5。这个组合能很好地去除稀疏的离群点,同时保留密集的物体点云。如果是高速场景,点云更稀疏,我会把stddev_mul调到2.0,避免把远处的车辆误删。
代码实现(PCL):
// 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> stat_filter;
stat_filter.setInputCloud(cloud);
stat_filter.setMeanK(30); // 邻居数
stat_filter.setStddevMulThresh(1.5); // 标准差倍数
stat_filter.filter(*cloud_filtered);
2.5 四种滤波的对比与选择
好了,四种方法都讲完了。你可能会问:到底该用哪个?
我的建议是:组合使用,按顺序来。
- 第一步:直通滤波 — 先切出ROI,把无关区域去掉。
- 第二步:体素滤波 — 降采样,减少数据量。
- 第三步:统计滤波或半径滤波 — 去噪,二选一即可。
为什么是这个顺序?因为先做直通滤波能大幅减少点数,后面的体素滤波和统计滤波计算量就小很多。我见过有人先做体素滤波再做直通滤波,结果体素滤波在无用区域浪费了大量计算资源。嗯,顺序很重要。
性能提醒:统计滤波的计算量比半径滤波大,因为它需要计算所有点的平均距离分布。如果你的系统对实时性要求很高(比如10Hz以上),建议用半径滤波代替统计滤波。我在嵌入式平台上做过测试,统计滤波比半径滤波慢了大约30%。
最后,我想说一句:预处理没有银弹。不同的场景、不同的传感器、不同的算法,需要的预处理策略都不一样。我的建议是,多试几组参数,用可视化工具看看效果,找到最适合你场景的那一套。
好了,这一讲就到这里。下一讲我们会聊聊点云的分割技术,包括地面分割和聚类,敬请期待。