第三章 体素滤波(VoxelGrid):点云降采样的核心武器
体素滤波,说白了就是给点云做「瘦身」。
你想想看,一帧64线激光雷达的数据,动辄几十万甚至上百万个点。这么多点,直接拿去跑配准或建图,计算量太大了。我刚开始做SLAM时,就吃过这个亏——点云不预处理,算法跑得比乌龟还慢。
体素滤波就是解决这个问题的。它能在保留点云几何结构的前提下,大幅减少点的数量。今天我们就把它彻底讲透。
3.1 体素滤波的原理
体素滤波的核心思想很简单:把三维空间切成一个个小立方体,每个立方体就叫一个「体素」(Voxel)。
然后,对每个体素内的所有点,取它们的重心(或中心)来代表这个体素。这样一来,原本体素里的几十个点,就变成了一个点。
核心公式:
体素内所有点的重心 = (Σx/n, Σy/n, Σz/n)
其中 n 是该体素内的点数
嗯,这里要注意:PCL默认用的是体素内所有点的重心,而不是体素的几何中心。这两者有区别——重心更贴近原始点云的分布,几何中心则可能偏离实际点云位置。
我画了一张图,帮你理解这个过程:
3.2 PCL实现:代码实战
理论讲完了,直接上代码。这是PCL中体素滤波的标准用法:
#include <pcl/filters/voxel_grid.h>
// 创建体素滤波对象
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
// 设置输入点云
voxel_filter.setInputCloud(input_cloud);
// 设置体素边长(leaf size),单位:米
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);
// 执行滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>());
voxel_filter.filter(*filtered_cloud);
// 输出结果
std::cout << "原始点数: " << input_cloud->size() << std::endl;
std::cout << "滤波后点数: " << filtered_cloud->size() << std::endl;
代码就这么几行,但参数调起来有讲究。我个人习惯把leaf size设成0.05米到0.2米之间,具体看你的应用场景。
避坑指南:
我曾经在项目中直接把leaf size设成0.5米,结果点云变得稀疏无比,墙角都变成了一团模糊的点。后来才意识到,体素滤波不是越粗越好,得根据你的传感器精度和场景来调。
3.3 参数(leaf size)对结果的影响
leaf size是体素滤波最重要的参数。它决定了每个体素的边长,单位是米。
我整理了一张表,帮你快速理解不同leaf size的效果:
| leaf size (米) | 点数保留比例 | 几何细节保留 | 适用场景 |
|---|---|---|---|
| 0.01 | ~80% | 几乎完整 | 高精度建模、精细扫描 |
| 0.05 | ~30% | 较好 | SLAM前端、实时配准 |
| 0.10 | ~10% | 一般 | 快速建图、大场景概览 |
| 0.20 | ~3% | 较差 | 粗匹配、可视化加速 |
| 0.50 | <1% | 严重丢失 | 不推荐,除非你只要轮廓 |
为什么会这样?说白了,leaf size越大,每个体素覆盖的空间越大,里面包含的点就越多,最终合并成一个点后丢失的信息也越多。
我建议你从0.05米开始试。如果点云还是太密,就加大到0.1米;如果细节丢失太多,就减小到0.02米。嗯,调参就是这么回事——没有万能参数,只有适合你场景的参数。
3.4 实战:对原始点云进行降采样
下面是一个完整的实战案例。我们用PCL读取一个PCD文件,做体素滤波,然后保存结果。
#include <pcl/io/pcd_io.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/visualization/pcl_visualizer.h>
int main(int argc, char** argv) {
// 1. 读取点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>());
if (pcl::io::loadPCDFile<pcl::PointXYZ>("input.pcd", *cloud) == -1) {
PCL_ERROR("读取点云失败\n");
return -1;
}
std::cout << "原始点数: " << cloud->size() << std::endl;
// 2. 创建体素滤波对象
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud);
vg.setLeafSize(0.1f, 0.1f, 0.1f); // 试试改成0.05或0.2看看效果
// 3. 执行滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>());
vg.filter(*cloud_filtered);
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;
// 4. 保存结果
pcl::io::savePCDFileASCII("output_filtered.pcd", *cloud_filtered);
// 5. 可视化对比
pcl::visualization::PCLVisualizer viewer("体素滤波对比");
int v1(0), v2(1);
viewer.createViewPort(0.0, 0.0, 0.5, 1.0, v1);
viewer.createViewPort(0.5, 0.0, 1.0, 1.0, v2);
viewer.addPointCloud(cloud, "original", v1);
viewer.addPointCloud(cloud_filtered, "filtered", v2);
viewer.spin();
return 0;
}
运行这段代码,你会看到左右两个窗口:左边是原始点云,密密麻麻;右边是降采样后的点云,稀疏但结构清晰。
注意:
体素滤波会改变点的精确位置(因为取的是重心)。如果你的应用对点的位置精度要求极高(比如毫米级测量),建议用更小的leaf size,或者考虑其他滤波方法。
我个人在实际项目中,通常把体素滤波放在预处理的第一步。先降采样,再去做离群点剔除、平滑等操作。这样后续处理的计算量会小很多,而且效果不会差太多。
好了,体素滤波就讲到这里。记住一句话:leaf size是体素滤波的灵魂,调好它,你的点云处理就成功了一半。
本章核心要点:
- 体素滤波通过划分三维网格,取每个网格内点的重心来实现降采样
- leaf size 控制降采样程度,建议从0.05米开始调试
- PCL中VoxelGrid类使用简单,核心就三个步骤:设置输入、设leaf size、执行filter
- 体素滤波会改变点的精确位置,高精度场景需谨慎