第三章 体素滤波(VoxelGrid):点云降采样的核心武器

体素滤波,说白了就是给点云做「瘦身」。

你想想看,一帧64线激光雷达的数据,动辄几十万甚至上百万个点。这么多点,直接拿去跑配准或建图,计算量太大了。我刚开始做SLAM时,就吃过这个亏——点云不预处理,算法跑得比乌龟还慢。

体素滤波就是解决这个问题的。它能在保留点云几何结构的前提下,大幅减少点的数量。今天我们就把它彻底讲透。

3.1 体素滤波的原理

体素滤波的核心思想很简单:把三维空间切成一个个小立方体,每个立方体就叫一个「体素」(Voxel)。

然后,对每个体素内的所有点,取它们的重心(或中心)来代表这个体素。这样一来,原本体素里的几十个点,就变成了一个点。

核心公式:

体素内所有点的重心 = (Σx/n, Σy/n, Σz/n)

其中 n 是该体素内的点数

嗯,这里要注意:PCL默认用的是体素内所有点的重心,而不是体素的几何中心。这两者有区别——重心更贴近原始点云的分布,几何中心则可能偏离实际点云位置。

我画了一张图,帮你理解这个过程:

体素滤波(VoxelGrid)原理示意图 原始点云 降采样后 每个体素内多个点 → 取重心 → 变成一个点 点数量大幅减少,几何结构保留 leaf size(体素边长)控制降采样程度:边长越大,点越少

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
  • 体素滤波会改变点的精确位置,高精度场景需谨慎
可可精品资料库