3、体素滤波原理:体素网格构建、体矢重心计算、体素滤波的数学原理

体素滤波,说白了就是给点云“降采样”。

你想想看,激光雷达扫一圈,动辄几十万甚至上百万个点。这么多点直接处理,CPU 直接冒烟。我早年做自动驾驶项目时,就吃过这个亏——一帧点云处理了 200 毫秒,车都撞墙了算法还没跑完。

体素滤波的核心思路很简单:把空间切成小方块,每个方块里只留一个代表点。这样既保留了形状特征,又把数据量压下去了。

3.1 体素网格构建

体素网格,就是三维空间里的“像素”。

二维图像有像素,三维点云就有体素。我们设定一个边长 L,把整个点云空间划分成一个个边长为 L 的立方体格子。每个格子就是一个体素。

关键参数:体素边长 L

L 越大,降采样越狠,点云越稀疏。
L 越小,保留细节越多,但压缩效果差。

我一般建议:L 取点云平均点间距的 2~5 倍。太大会丢失结构,太小没效果。

构建步骤其实就三步:

  1. 计算包围盒:找到点云在 X、Y、Z 三个方向上的最小值和最大值。
  2. 确定网格数量:每个方向的格子数 = ceil((max - min) / L)。
  3. 分配点云:对每个点,计算它落在哪个格子。公式很简单:
// 伪代码:计算点 p 所在的体素索引
int ix = floor((p.x - min_x) / L);
int iy = floor((p.y - min_y) / L);
int iz = floor((p.z - min_z) / L);

嗯,这里要注意:浮点数精度问题。我曾经在项目里直接用 int 强转,结果边界上的点经常跑错格子。后来统一用 std::floor 才稳定下来。

3.2 体素重心计算

格子分好了,每个格子里可能有好几个点。问题来了:留哪个点?

最简单的做法是取格子中心点。但这样会丢失原始点的分布信息。更科学的做法是——计算体素内所有点的重心。

重心公式:

// 体素重心 = 所有点的坐标平均值
centroid_x = (p1.x + p2.x + ... + pn.x) / n
centroid_y = (p1.y + p2.y + ... + pn.y) / n
centroid_z = (p1.z + p2.z + ... + pn.z) / n

说白了,就是把格子里的点“揉”成一个点。这个点最能代表原始点的整体位置。

我的经验:

如果体素内点很少(比如只有 1~2 个),直接用重心没问题。
但如果体素内点特别多(比如上百个),重心可能会偏向密集区域。这时候可以考虑用体素中心 + 最近邻点的组合策略。

3.3 体素滤波的数学原理

体素滤波本质上是一个空间哈希 + 下采样的过程。它的数学基础其实不复杂,但理解透了能帮你调参。

核心思想:用离散的体素网格近似连续的三维空间。

每个体素可以看作一个 3D 桶(bucket)。点云中的每个点,根据它的空间坐标,被哈希到对应的桶里。然后每个桶输出一个代表点。

数学表达:

给定点集 P = {p1, p2, ..., pN}
体素边长 L
输出点集 Q = {q1, q2, ..., qM},其中 M ≤ N

对于每个体素 V(i,j,k):
  q = (1/|V|) * Σ p,  p ∈ V

这里 |V| 是体素内的点数。如果 |V| = 0,这个体素就跳过。

体素滤波的三大特性:

  • 均匀性:输出点云在空间上分布均匀,不会出现局部过密或过疏。
  • 可控性:通过调整 L,可以精确控制输出点云密度。
  • 保形性:保留了原始点云的整体几何形状,不会产生畸变。

为什么说它保形?因为每个体素只取一个代表点,不会改变点云的相对位置关系。你想想看,如果两个点原本相距 0.5 米,体素滤波后它们可能被合并成一个点,但不会凭空多出点来。

注意:体素滤波的局限性

体素滤波对薄壁结构不友好。比如一根细长的电线杆,体素边长如果大于电线杆直径,整个杆子可能被滤成一个点。我做过一个电力线巡检项目,就因为这个原因把电线杆全滤没了……后来不得不把体素边长改小。

3.4 体素滤波流程图

下面我用一张图把整个流程串起来:

体素滤波核心流程 输入原始点云 计算包围盒 (min/max) 划分体素网格 分配点云到体素 计算体素重心 关键参数 体素边长 L 控制降采样强度 包围盒范围 决定网格数量 体素内点数 影响重心精度 输出点数量 M ≤ N ⚠ 注意事项 薄壁结构易丢失 边界点精度问题 体素大小需调参

3.5 代码实现要点

实际写代码时,有几个坑我帮你踩过了:

// C++ 体素滤波核心实现(PCL 风格)
class VoxelFilter {
public:
    void setLeafSize(float lx, float ly, float lz) {
        leaf_x_ = lx;
        leaf_y_ = ly;
        leaf_z_ = lz;
    }
    
    void filter(const PointCloud<PointXYZ>& input, 
                PointCloud<PointXYZ>& output) {
        // 1. 计算包围盒
        // 2. 创建哈希表:体素索引 -> 点列表
        // 3. 遍历所有点,分配到体素
        // 4. 遍历所有体素,计算重心
        // 5. 输出结果
    }
    
private:
    float leaf_x_, leaf_y_, leaf_z_;
};

性能优化小技巧:

  • 用 哈希表 代替三维数组,避免内存浪费(体素可能很稀疏)。
  • 体素索引可以用 64 位整数 编码:hash = ix * 1000000 + iy * 1000 + iz。
  • 多线程并行处理:每个体素的计算是独立的,可以开线程池加速。

好了,体素滤波的原理就这些。说白了就是“切格子、算平均”。但就是这简单的操作,能帮你把点云从百万级压到万级,而且形状基本不变。我这些年做过的项目,从自动驾驶到工业检测,体素滤波几乎是必用的第一步。