第1章:体素滤波(VoxelGrid)——点云“瘦身”的第一课
做点云处理的朋友,十有八九都跟体素滤波打过交道。我刚开始接触激光雷达数据时,第一反应就是——这数据量也太吓人了吧?一帧64线雷达的点云,动不动就十几万个点。直接拿去跑配准?算力扛不住,实时性更是别想。这时候,体素滤波就是你的救星。
说白了,体素滤波干的事就一件:在保持点云几何结构的前提下,大幅降低点的数量。它不是随机扔掉一些点,而是用一种“空间平均”的思路,让点云变轻、变整齐。
体素化思想:把连续空间切成小方块
想象一下,你手里有一块巨大的乐高底板。体素滤波的做法,就是在这块底板上画出一个三维网格。每个小格子,就是一个“体素”(Voxel)。
体素这个词,其实是“体积像素”(Volume Pixel)的缩写。二维图像有像素,三维空间就有体素。每个体素就是一个边长固定的立方体小盒子。
具体怎么操作呢?
- 确定包围盒:找到点云在X、Y、Z三个方向上的最小值和最大值,形成一个长方体。
- 划分网格:设定一个体素边长(比如0.1米),然后把包围盒切成一个个小立方体。
- 分配点云:遍历每个点,计算它落在哪个体素里。每个体素里可能包含0个、1个或多个点。
- 计算重心:对每个非空体素,计算内部所有点的重心(即坐标平均值),用这个重心点代表整个体素。
嗯,这里要注意:体素滤波不是简单的“每个体素只留一个点”。它用的是重心替代,而不是随机采样。这一点很关键,我后面会细说。
核心思想一句话:用空间网格的“平均代表”替代原始密集点云,在降采样同时保留几何特征。
下采样原理:为什么重心比随机采样更靠谱?
你可能会问:为什么不直接随机扔掉一些点?那样多简单啊。
我在项目里试过随机下采样,效果嘛……怎么说呢,有时候还行,但遇到复杂场景就露馅了。随机采样的问题在于——它不关心点的分布密度。一个区域点很密集,另一个区域点很稀疏,随机采样可能把稀疏区域的点全扔了,导致几何结构断裂。
体素滤波就不一样。它先划分网格,再在每个网格里取重心。这样做有两个好处:
- 均匀性:不管原始点云密度如何,每个体素最多贡献一个点。点云分布变得均匀。
- 保形性:重心点能代表该体素内所有点的平均位置,不会偏离原始几何形状太远。
举个例子。假设一个体素里有5个点,坐标分别是(0,0,0)、(0.01,0,0)、(0,0.01,0)、(0.01,0.01,0)、(0.005,0.005,0)。它们的重心是(0.005,0.005,0)。这个点恰好位于体素的中心附近,能很好地代表这5个点。如果随机选一个,万一选到边角上的点,就会引入偏差。
我的经验:在SLAM前端做帧间配准时,体素滤波后的点云比随机采样后的点云,匹配成功率能提升10%-15%。原因就是重心点更稳定,噪声更小。
体素大小选择:这个参数决定了你的命运
体素滤波只有一个关键参数——体素边长。但这个参数选得好不好,直接决定滤波效果。
我见过不少新手,上来就设个0.01米,结果点云几乎没变化,该卡还是卡。也有人设个1米,点云直接变成一坨一坨的,几何细节全没了。
体素大小的选择,本质上是在计算效率和几何保真度之间做权衡。
| 体素大小 | 效果 | 适用场景 |
|---|---|---|
| 过小(< 0.02m) | 点云几乎不变,降采样效果微弱 | 高精度建模,算力充足时 |
| 适中(0.05m - 0.1m) | 点云减少80%-90%,几何结构清晰 | SLAM前端、实时配准、一般滤波 |
| 过大(> 0.3m) | 点云稀疏,细节丢失严重 | 快速预览、粗配准、大尺度场景 |
我个人习惯,做室内SLAM时体素大小设在0.05米左右。室外大场景,比如自动驾驶,0.1米到0.2米比较合适。当然,这跟雷达线束密度也有关系,你得根据实际情况微调。
避坑指南:我曾经在一个项目里,把体素设成0.08米,结果发现墙角处的点云出现了“锯齿状”变形。后来才意识到,体素大小跟墙面的平整度有关。如果墙面本身不平,体素太大就会把弯曲部分“拉直”。遇到这种情况,要么减小体素,要么改用其他滤波方法。
代码实战:手写一个体素滤波器
理论说完了,咱们来点实际的。下面我用Python实现一个简单的体素滤波。这里用的是Open3D库,它封装了体素滤波的核心逻辑,但我会把底层思路拆开讲。
import open3d as o3d
import numpy as np
def voxel_filter(pcd, voxel_size=0.05):
"""
体素滤波函数
:param pcd: open3d点云对象
:param voxel_size: 体素边长(米)
:return: 滤波后的点云
"""
# 方法一:直接调用Open3D内置函数(推荐)
filtered_pcd = pcd.voxel_down_sample(voxel_size)
return filtered_pcd
# 加载点云
pcd = o3d.io.read_point_cloud("example.pcd")
print(f"原始点云点数:{len(pcd.points)}")
# 执行体素滤波
filtered = voxel_filter(pcd, voxel_size=0.05)
print(f"滤波后点数:{len(filtered.points)}")
# 可视化对比
o3d.visualization.draw_geometries([pcd], window_name="原始点云")
o3d.visualization.draw_geometries([filtered], window_name="体素滤波后")
这段代码很简单,但背后其实做了不少事。Open3D的voxel_down_sample函数,内部实现就是我前面说的那四步:计算包围盒、划分网格、分配点、求重心。
如果你想自己手写底层逻辑,核心代码大概长这样:
def voxel_filter_manual(points, voxel_size):
"""
手动实现体素滤波
:param points: Nx3 numpy数组
:param voxel_size: 体素边长
:return: 滤波后的点云
"""
# 计算每个点所属的体素索引
indices = np.floor(points / voxel_size).astype(int)
# 将体素索引转换为唯一标识(用哈希思想)
# 这里用字典来聚合同一个体素内的点
voxel_dict = {}
for i, idx in enumerate(indices):
key = tuple(idx) # 体素索引作为字典键
if key not in voxel_dict:
voxel_dict[key] = []
voxel_dict[key].append(points[i])
# 对每个体素计算重心
filtered_points = []
for key, pts in voxel_dict.items():
centroid = np.mean(pts, axis=0)
filtered_points.append(centroid)
return np.array(filtered_points)
你看,核心逻辑就这么几行。但实际工程中,Open3D的版本做了大量优化,比如用哈希表加速查找、用并行计算处理大规模点云。自己手写的话,处理百万级点云可能会慢一些。
效果对比:眼见为实
为了让你直观感受体素滤波的效果,我拿一帧室内点云做了测试。原始点云有12万个点,体素大小设为0.05米。
- 原始点云:12万点,文件大小约4.5MB
- 滤波后:约1.2万点,文件大小约0.5MB
- 压缩比:90%
从视觉上看,滤波后的点云依然能清晰分辨墙壁、地面、桌子的轮廓。但如果你凑近看,会发现原本密集的墙面点变成了均匀分布的小点,不再有重叠和冗余。
这就是体素滤波的魅力——用10%的数据量,保留了90%以上的几何信息。
小结一下:体素滤波是点云预处理的第一步,也是最重要的一步。它简单、高效、可控。你只需要调一个参数(体素大小),就能在效率和精度之间找到平衡点。我个人建议,做任何点云项目之前,先跑一遍体素滤波,把数据量降下来,后面的事情会轻松很多。
可可精品资料库