4、点云预处理:体素化降采样、均匀采样、随机采样、最远点采样(FPS)
各位同学,咱们今天聊聊点云预处理里的采样方法。说实话,我刚开始做三维扫描那会儿,拿到一个点云数据就直接往上怼算法,结果经常跑半天不出结果,或者内存直接爆掉。后来才明白——预处理这一步,省不了。
点云数据量通常很大,动辄几十万甚至上百万个点。直接处理,计算量吃不消。所以我们需要采样,说白了就是用更少的点,保留尽可能多的信息。今天讲四种主流方法:体素化降采样、均匀采样、随机采样、最远点采样(FPS)。
核心思想:采样不是随便扔点,而是要在降低数据量的同时,尽量保持原始点云的几何结构和分布特征。
4.1 体素化降采样(Voxel Grid Downsampling)
这个方法我用的最多。它的思路很简单:把三维空间切成一个个小立方体(体素),每个体素里只保留一个点。
具体怎么做呢?你设定一个体素大小,比如 0.01m。然后算法会把点云划分到对应的体素网格里。每个体素内,你可以取所有点的重心,或者取离体素中心最近的点。
我个人习惯:取重心比取中心点更稳定。重心能反映体素内点的实际分布,而中心点可能选到边缘的离群点。
import open3d as o3d
# 读取点云
pcd = o3d.io.read_point_cloud("scan.ply")
# 体素化降采样,体素大小 0.02m
downsampled = pcd.voxel_down_sample(voxel_size=0.02)
# 可视化对比
o3d.visualization.draw_geometries([downsampled])
优点:速度快,能均匀降低密度,对噪声有一定抑制作用。
缺点:体素大小不好调。调大了丢失细节,调小了降采样效果不明显。
我曾经踩过的坑:体素大小设成 0.1m,结果一个拳头大小的物体只剩几个点,形状完全没了。后来我学乖了——先看点云的平均点间距,体素大小设为平均间距的 2-3 倍比较稳妥。
4.2 均匀采样(Uniform Sampling)
均匀采样,说白了就是每隔固定步长取一个点。比如每 5 个点取一个,或者每 10 个点取一个。
这个方法在点云有序排列时很好用。但问题是——真实扫描的点云往往不是均匀分布的。靠近扫描仪的地方点密,远处点稀。均匀采样会破坏这种密度信息。
import numpy as np
def uniform_sample(points, step=5):
"""均匀采样:每隔 step 个点取一个"""
return points[::step]
# 使用示例
points = np.asarray(pcd.points)
sampled = uniform_sample(points, step=10)
优点:实现简单,计算量极小。
缺点:对点云顺序敏感,容易丢失稀疏区域的点。
适用场景:点云已经过预处理、分布比较均匀时,均匀采样是个轻量级的好选择。但如果是原始扫描数据,我建议慎用。
4.3 随机采样(Random Sampling)
随机采样,就是随机从点云里抽出一部分点。每个点被选中的概率相等。
你想想看,这方法简单粗暴,但有个问题——随机性太强。可能把关键特征点全漏掉,也可能在密集区域重复采样。
import random
def random_sample(points, num_samples):
"""随机采样:从 points 中随机抽取 num_samples 个点"""
if num_samples >= len(points):
return points
indices = random.sample(range(len(points)), num_samples)
return points[indices]
# 使用示例
sampled = random_sample(points, 5000)
优点:实现最简单,速度最快。
缺点:不稳定,多次采样结果差异大,容易丢失结构信息。
我建议:随机采样适合做快速预览,或者作为其他算法的初始化步骤。但别指望它保留精细结构。
4.4 最远点采样(Farthest Point Sampling, FPS)
终于讲到 FPS 了。这个方法在深度学习里很常见,比如 PointNet++ 就用它来做下采样。
FPS 的思路是:每次选一个离已选点集最远的点。这样能保证采样点均匀覆盖整个点云空间。
具体步骤:
- 随机选一个初始点。
- 计算所有点到已选点集的距离,取最小值。
- 选距离最小值最大的那个点加入已选点集。
- 重复步骤 2-3,直到选够点数。
import numpy as np
def fps(points, num_samples):
"""最远点采样"""
n = points.shape[0]
if num_samples >= n:
return points
# 随机选第一个点
selected = [np.random.randint(n)]
distances = np.full(n, np.inf)
for _ in range(1, num_samples):
# 更新距离
last = selected[-1]
dist = np.linalg.norm(points - points[last], axis=1)
distances = np.minimum(distances, dist)
# 选最远点
farthest = np.argmax(distances)
selected.append(farthest)
return points[selected]
# 使用示例
sampled = fps(points, 5000)
优点:采样点分布均匀,能很好保留点云的整体结构。
缺点:计算量大,复杂度 O(n*m),n 是原始点数,m 是采样点数。
我曾经遇到的情况:对一个 50 万点的点云做 FPS 采样到 1 万点,跑了将近 10 秒。后来我改用 KD-Tree 加速距离计算,时间降到了 2 秒以内。所以,大数据量时记得优化一下。
4.5 四种方法对比
| 方法 | 速度 | 均匀性 | 结构保持 | 适用场景 |
|---|---|---|---|---|
| 体素化降采样 | 快 | 好 | 较好 | 通用预处理 |
| 均匀采样 | 极快 | 差 | 差 | 有序点云 |
| 随机采样 | 极快 | 差 | 差 | 快速预览 |
| 最远点采样 | 慢 | 极好 | 极好 | 深度学习、精细处理 |
总结一下我的经验:
- 日常预处理,体素化降采样是首选,平衡了速度和效果。
- 如果要做深度学习训练,FPS 是标配,虽然慢但值得。
- 随机采样和均匀采样,我一般只在调试阶段用,或者数据量实在太大时做快速降维。
嗯,四种方法就讲到这里。每种方法都有自己的脾气,选对了事半功倍。下次你拿到一个点云,不妨先问问自己:我要保留什么?速度优先还是质量优先? 想清楚了再选方法。