激光雷达点云滤波算法与处理管线 - 第3节
一、体素滤波:点云世界的“像素化”降维
体素滤波,英文叫 VoxelGrid。说白了,就是把三维空间切成一个个小立方体。
每个小立方体,就是一个体素(Voxel)。
然后,在每个体素里,只保留一个点。其他点,全部丢掉。
嗯,就这么简单粗暴。
我刚开始接触点云时,觉得这操作太“暴力”了。后来发现,这恰恰是点云预处理中最优雅的一步。为什么?你想想看,激光雷达一帧数据,动辄十几万甚至上百万个点。这么多点,很多都是冗余的。比如一面墙,你扫出10万个点,和扫出1000个点,对后续的配准、建图来说,差别不大。
体素滤波的核心思想,就是用空间分辨率换计算效率。它不关心点的密度,只关心点云在空间中的“骨架”。
1.1 体素滤波到底在做什么?
我习惯用一个比喻来理解它:
你有一堆沙子(原始点云)。你拿一个筛子(体素网格)去筛。筛子的每个格子大小固定。每个格子里,只留一粒沙子。其他沙子,全部筛掉。
留下来的那粒沙子,通常是这个格子内所有点的重心(质心),或者直接取格子中心点。
所以,体素滤波有两个关键步骤:
- 划分体素网格:根据你设定的体素大小,把整个点云空间切成无数个小立方体。
- 体素内降采样:每个小立方体内,只保留一个代表点。
这里有个细节,我踩过坑——体素内的代表点怎么选?
PCL 默认用的是体素内所有点的重心(质心)。这个做法比较稳,能保留局部几何特征。但如果你追求极致速度,也可以直接用体素的中心点。不过,我建议你用默认的质心方式,精度会好一些。
核心结论:体素滤波的本质,是用一个点代表一个局部空间。体素越小,保留的细节越多;体素越大,降采样越狠,但信息损失也越大。
1.2 体素大小对滤波效果的影响
这是体素滤波里最关键的参数。没有之一。
我见过很多新手,上来就设一个 0.1m 的体素,结果点云还是几十万个点,滤波了个寂寞。也有人设 1m,结果一堵墙变成了几个孤零零的点,后续配准直接崩了。
体素大小的影响,我总结成一张表:
| 体素大小 | 效果 | 适用场景 |
|---|---|---|
| 很小(如 0.01m) | 几乎不降采样,保留全部细节 | 高精度建模、精细测量 |
| 适中(如 0.05m - 0.1m) | 降采样明显,保留主要几何结构 | SLAM前端、配准、一般建图 |
| 较大(如 0.5m - 1m) | 严重降采样,细节丢失 | 快速预览、粗配准、大场景概览 |
我个人习惯,在 SLAM 前端做配准时,体素大小设在 0.05m 到 0.1m 之间。这个范围既能保证配准精度,又能把点云数量压到几千到几万点,计算量刚刚好。
但要注意,体素大小不是固定的。它跟你的场景尺度有关。室内小场景,0.02m 可能都嫌大。室外大场景,0.2m 可能都嫌小。我建议你根据点云的平均点间距来设定。一般取平均点间距的 2-5 倍,效果比较稳妥。
避坑指南:我曾经在一个项目中,把体素设得太大,结果点云变得稀疏无比,导致后续的 ICP 配准直接发散。后来花了半天排查,才发现是体素滤波把关键特征点都滤掉了。所以,体素大小一定要跟你的应用场景匹配。宁小勿大,小了只是慢一点,大了可能直接导致算法失效。
二、PCL 代码实现:手把手教你调参
理论讲完了,咱们直接上代码。PCL 的体素滤波实现非常成熟,核心就几个 API。
下面是我常用的一个代码模板:
#include <pcl/point_types.h>
#include <pcl/filters/voxel_grid.h>
// 输入点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_in(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_out(new pcl::PointCloud<pcl::PointXYZ>);
// 创建体素滤波器对象
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
// 设置输入点云
voxel_filter.setInputCloud(cloud_in);
// 设置体素大小(单位:米)
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);
// 执行滤波
voxel_filter.filter(*cloud_out);
// 输出结果
std::cout << "滤波前点数: " << cloud_in->size() << std::endl;
std::cout << "滤波后点数: " << cloud_out->size() << std::endl;
代码很简单,对吧?但有几个参数细节,我得跟你聊聊。
2.1 setLeafSize 的三个值
setLeafSize 接受三个参数,分别是 x、y、z 方向的体素大小。大多数情况下,三个值设成一样就行,也就是立方体体素。
但有一种情况例外——如果你的点云在某个方向特别稀疏,比如无人机激光雷达扫出来的点云,Z 方向变化不大,你可以把 Z 方向的体素设大一点,减少计算量。
我一般保持三个值一致,省心。
2.2 滤波后的点云数量控制
体素滤波不会直接告诉你输出多少个点。你需要通过调整体素大小来控制。
我有个经验公式:
输出点数 ≈ 点云包围盒体积 / 体素体积
比如,你的点云包围盒是 10m x 10m x 5m = 500 立方米。体素大小 0.1m,体素体积 0.001 立方米。那么输出点数大约 500 / 0.001 = 500,000 个点。
当然,这是理想情况。实际会有空体素,所以点数会少一些。但这个公式能帮你快速估算。
调参技巧:如果你想要输出大约 1 万个点,可以先估算包围盒体积,然后反推体素大小。比如包围盒 500 立方米,想要 1 万个点,体素体积 ≈ 500 / 10000 = 0.05 立方米,体素边长 ≈ 0.37m。然后在这个值附近微调即可。
2.3 进阶用法:保留点云颜色或强度
如果你的点云带有颜色或强度信息,记得用对应的点类型。PCL 支持得很全:
// 带颜色的点云
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_color(new pcl::PointCloud<pcl::PointXYZRGB>);
pcl::VoxelGrid<pcl::PointXYZRGB> voxel_filter_color;
voxel_filter_color.setInputCloud(cloud_color);
voxel_filter_color.setLeafSize(0.1f, 0.1f, 0.1f);
voxel_filter_color.filter(*cloud_out_color);
注意,体素滤波在计算重心时,会同时平均颜色或强度值。所以输出点的颜色是体素内所有点的平均色。这个效果在视觉上很平滑,我个人挺喜欢的。
三、体素滤波的核心逻辑图
为了让你更直观地理解整个过程,我画了一张流程图:
这张图把整个流程串起来了。从输入点云开始,设定体素大小,然后划分网格,最后在每个体素内取一个代表点。每一步都很清晰。
四、参数调优:我的实战经验
调参这件事,光看理论没用。我分享几个实际项目中的经验。
4.1 场景不同,体素不同
- 室内走廊:点云密集,体素 0.02m - 0.05m 即可。太大会丢失墙角等细节。
- 室外道路:点云稀疏,体素 0.1m - 0.2m 合适。太小了滤波效果不明显。
- 大范围地图:比如整个园区,体素 0.5m - 1m 都行。主要看你要保留多少细节。
4.2 体素滤波的“副作用”
体素滤波会改变点云的局部密度。原本密集的区域,滤波后依然密集(因为体素小)。原本稀疏的区域,滤波后可能直接消失(因为体素内没点)。
所以,如果你的点云密度分布极不均匀,体素滤波可能会把稀疏区域的点全部滤掉。这时候,我建议你先做一次半径滤波或统计滤波,把离群点处理一下,再做体素滤波。
注意:体素滤波不是万能的。它适合降采样,但不适合去噪。如果你需要去除离群点,请用统计滤波或半径滤波。体素滤波只管“降采样”,不管“去噪”。
4.3 性能优化小技巧
体素滤波的计算量跟点云数量成正比。对于百万级点云,PCL 的体素滤波大概需要几十到几百毫秒。如果你需要实时处理,可以考虑:
- 先对点云做下采样(比如每隔几个点取一个),减少输入点数。
- 使用多线程版本的体素滤波(PCL 1.8+ 支持)。
- 如果体素大小固定,可以缓存体素网格,避免重复划分。
我一般在实时 SLAM 系统中,把体素滤波放在单独的线程里跑,不影响主循环的帧率。
五、总结
体素滤波是点云预处理中最基础、最常用的工具。它的核心就一句话:用空间换效率。
体素大小是唯一的参数,也是最重要的参数。调好了,你的 SLAM 系统跑得飞快。调不好,要么点云太密跑不动,要么点云太稀算法失效。
我建议你从 0.05m 开始试,然后根据输出点数和效果微调。记住,宁小勿大。
好了,体素滤波就讲到这里。下一章,我们聊聊统计滤波——那个专门用来干掉离群点的家伙。
课后练习:找一帧激光雷达数据,分别用 0.01m、0.05m、0.1m、0.5m 的体素大小做滤波,观察输出点数和点云形态的变化。你会发现,体素大小对结果的影响,比你想的还要大。
可可精品资料库