3. 点云数据预处理:点云滤波与降采样策略
点云预处理,说白了就是给原始数据「洗个澡」。激光雷达扫出来的点云,里面什么妖魔鬼怪都有——飞点、噪点、冗余点。如果不处理,后面的配准、建图、定位全都会崩。我刚开始做机器人时,就吃过这个亏。
有一次在室外测试,点云里混了一堆树叶反射的杂点,结果建图直接跑飞了。从那以后,我养成了一个习惯:预处理做不好,绝不往下走。
核心观点:点云预处理不是可选项,而是必选项。它直接影响后续算法的精度和实时性。你花在滤波上的每一毫秒,都会在后面的环节里加倍还回来。
3.1 直通滤波:最暴力的「切蛋糕」
直通滤波的原理非常简单——你设定一个范围,范围内的点留下,范围外的全扔掉。比如你的机器人只在 10 米范围内活动,那 50 米外的点留着干嘛?
我个人习惯在预处理的第一步就做直通滤波。为什么?因为能大幅减少后续计算量。你想想看,一个 64 线激光雷达每秒产生 130 万个点,先切掉 80% 的无效区域,后面处理起来轻松多了。
// C++ 示例:PCL 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z"); // 沿 Z 轴滤波
pass.setFilterLimits(0.0, 3.0); // 保留 0~3 米范围
pass.setFilterLimitsNegative(false); // false=保留范围内
pass.filter(*cloud_filtered);
我的经验:直通滤波的参数不要拍脑袋定。我一般会先跑一次数据统计,看看点云在 x/y/z 方向上的分布直方图,再决定阈值。比如扫地机器人,Z 轴范围通常设成 -0.1~2.0 米就够了。
3.2 体素滤波:降采样的「压缩器」
体素滤波,说白了就是把空间切成一个个小立方体(体素),每个立方体里只保留一个代表点。这个代表点通常是体素内所有点的重心。
体素滤波有两个核心参数:体素大小和下采样比例。体素越大,点云越稀疏,计算越快,但细节丢失也越多。
| 体素大小 (m) | 点云数量 (原始 100 万点) | 耗时 (ms) | 适用场景 |
|---|---|---|---|
| 0.01 | ~80 万 | 15~25 | 高精度建图 |
| 0.05 | ~10 万 | 5~10 | 实时定位 |
| 0.10 | ~3 万 | 2~5 | 快速导航 |
| 0.20 | ~1 万 | <2 | 粗略障碍物检测 |
// C++ 示例:PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.05f, 0.05f, 0.05f); // 5cm 体素
voxel.filter(*cloud_filtered);
注意:体素大小不是越小越好。我曾经在一个项目中把体素设成 0.005m,结果点云数量几乎没减少,还多花了 30ms 的计算时间。体素滤波的初衷是降采样,不是做精细滤波。
3.3 半径滤波:清理「孤魂野鬼」
半径滤波,也叫统计滤波。它的逻辑是:如果一个点周围(指定半径内)的邻居太少,那它大概率是个噪点,直接扔掉。
我记得有一次在室内走廊测试,点云里总有一些飘在半空中的点。后来发现是灰尘反射造成的。半径滤波一上,这些「孤魂野鬼」全没了。
// C++ 示例:PCL 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud);
rad.setRadiusSearch(0.1); // 搜索半径 0.1m
rad.setMinNeighborsInRadius(6); // 至少 6 个邻居
rad.filter(*cloud_filtered);
参数怎么调?我一般遵循这个原则:半径 = 体素大小的 2~3 倍。比如体素是 5cm,半径就设 10~15cm。邻居数量设 3~6 个,视点云密度而定。
3.4 离群点移除:最后的「清道夫」
离群点移除和半径滤波有点像,但更激进。它基于统计分布——计算每个点到其 k 个最近邻的平均距离,如果这个距离超过全局均值 + n 倍标准差,就判定为离群点。
我习惯把离群点移除放在预处理流程的最后一步。因为前面的滤波已经去掉了大部分噪点,剩下的离群点往往是「顽固分子」,需要用统计方法才能揪出来。
// C++ 示例:PCL 统计离群点移除
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50); // 考虑 50 个最近邻
sor.setStddevMulThresh(1.0); // 标准差倍数阈值
sor.filter(*cloud_filtered);
避坑指南:我曾经把 stddevMulThresh 设成 0.5,结果把正常点也删了一大片。后来发现,对于室内环境,1.0~1.5 比较合适;室外环境可以放宽到 2.0~3.0。这个参数一定要根据实际数据调试,别照搬网上的值。
3.5 降采样策略与性能权衡
降采样不是越狠越好。你想想看,点云太稀疏了,特征提取就做不好,配准精度也会下降。这里有一个性能-精度平衡的问题。
我总结了一套策略,按场景分类:
- 实时导航(10Hz+):体素 10cm + 直通滤波 + 半径滤波。点云量控制在 1~3 万点,单帧处理时间 < 5ms。
- 高精度建图(离线):体素 2cm + 直通滤波 + 统计离群点移除。点云量 10~30 万点,处理时间可以放宽到 50ms。
- 障碍物检测(5~10Hz):体素 5cm + 直通滤波。点云量 5~10 万点,处理时间 10~15ms。
| 场景 | 体素大小 | 滤波组合 | 目标点云量 | 预算耗时 |
|---|---|---|---|---|
| 实时导航 | 10cm | 直通 + 半径 | 1~3 万 | <5ms |
| 高精度建图 | 2cm | 直通 + 统计离群点 | 10~30 万 | <50ms |
| 障碍物检测 | 5cm | 直通 | 5~10 万 | 10~15ms |
| 快速避障 | 20cm | 直通 | <1 万 | <2ms |
我的建议:别一上来就追求极致性能。先跑通流程,用体素 5cm 做 baseline,然后逐步增大体素,观察精度下降的拐点。找到那个「再大一点精度就崩了」的临界值,那就是你的最优参数。
3.6 滤波顺序:先做什么后做什么?
这个问题我经常被问到。我的答案是:先粗后细,先快后慢。
- 第一步:直通滤波。最快,能砍掉 50%~80% 的点,为后续减负。
- 第二步:体素滤波。降采样,进一步压缩数据量。
- 第三步:半径滤波或统计离群点移除。清理残余噪点。
为什么体素滤波要放在半径滤波前面?因为体素滤波会改变点的空间分布,如果先做半径滤波,体素化之后邻居关系就变了,之前的滤波效果会打折扣。嗯,这个顺序我踩过坑才总结出来的。
警告:不要在体素滤波之前做统计离群点移除。体素滤波会改变点的密度分布,导致统计结果失真。先降采样,再清理离群点,这个顺序不能乱。