3. 点云预处理:体素滤波降采样、统计滤波去噪、半径滤波去噪
点云数据拿到手,第一件事是什么?
不是急着做配准,也不是急着做分割。我个人的习惯是——先做预处理。
为什么?你想想看,激光雷达扫出来的原始点云,动辄几十万甚至上百万个点。这里面有大量的冗余数据,还有各种噪声点、离群点。直接拿这些数据去跑算法,不仅慢,而且结果往往一塌糊涂。
说白了,预处理就是给点云「洗个澡」,把脏东西洗掉,把多余的水分拧干。今天我们就聊三个最常用的预处理手段:体素滤波、统计滤波、半径滤波。
核心逻辑:降采样解决「点太多」的问题,去噪解决「点太脏」的问题。两者缺一不可。
3.1 体素滤波降采样
体素滤波,说白了就是把三维空间切成一个个小立方体(体素),每个立方体里只保留一个代表点。
我刚开始做点云处理时,总想着保留所有点,生怕丢了信息。后来发现,相邻的点其实长得差不多,保留一个就够了。体素滤波就是干这个的。
工作原理:
- 设定体素大小(比如 0.1m × 0.1m × 0.1m)
- 把点云划分到各个体素中
- 每个体素内计算所有点的重心(或中心),作为代表点
我的经验:体素大小怎么选?室内场景我一般用 0.05m,室外场景用 0.1m-0.2m。太小了降采样效果不明显,太大了会丢失细节。我曾经在一个隧道项目里用了 0.5m 的体素,结果把墙面的细节全抹平了,后来改回 0.15m 才解决问题。
import open3d as o3d
# 读取点云
pcd = o3d.io.read_point_cloud("raw_cloud.pcd")
# 体素滤波降采样,体素大小 0.1m
voxel_size = 0.1
down_pcd = pcd.voxel_down_sample(voxel_size)
print(f"原始点数: {len(pcd.points)}")
print(f"降采样后点数: {len(down_pcd.points)}")
# 输出示例:原始点数: 1,200,000 → 降采样后点数: 85,000
| 体素大小 | 适用场景 | 点数减少比例 |
|---|---|---|
| 0.05m | 室内精细建模 | 约 70%-80% |
| 0.1m | 通用场景 | 约 85%-95% |
| 0.2m | 室外大场景 | 约 95%-99% |
注意:体素滤波会改变点的精确位置(因为用了重心)。如果你的应用对几何精度要求极高(比如工业测量),可以考虑用体素中心代替重心,或者改用随机降采样。
3.2 统计滤波去噪
降采样之后,点云里可能还有一些「野点」——就是那些飘在空中的孤立噪声点。统计滤波就是专门收拾它们的。
核心思想:每个点周围都有邻居。如果一个点离它的邻居太远了,那它大概率是噪声。
具体做法是:
- 对每个点,计算它到所有 k 个最近邻的平均距离
- 所有点的平均距离会形成一个分布(近似高斯分布)
- 设定一个标准差倍数阈值,超过这个阈值的点就被判定为离群点
嗯,这里要注意:k 值(邻居数)和 std_ratio(标准差倍数)是两个关键参数。
import open3d as o3d
# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
nb_neighbors=20, # 考虑20个最近邻
std_ratio=2.0 # 标准差倍数阈值
)
# 提取内点(干净点云)
clean_pcd = down_pcd.select_by_index(ind)
# 提取外点(噪声点云)
outlier_pcd = down_pcd.select_by_index(ind, invert=True)
print(f"内点数量: {len(clean_pcd.points)}")
print(f"噪声点数量: {len(outlier_pcd.points)}")
避坑指南:我曾经在一个户外 LiDAR 数据集上把 std_ratio 设成了 1.0,结果把很多正常的墙面点也删掉了。后来发现,户外场景点云分布比较稀疏,标准差倍数设到 2.0-3.0 更合适。室内场景可以设小一点,1.5-2.0 就够用。
3.3 半径滤波去噪
统计滤波好用,但它有个小毛病——它假设点云密度是均匀的。如果场景里有的地方密、有的地方稀,统计滤波可能会误删。
这时候,半径滤波就派上用场了。
半径滤波的逻辑更直接:以每个点为中心,画一个半径为 r 的球。如果球内的邻居数少于某个阈值,就把这个点删掉。
说白了,就是「你周围没人陪你,那你就是孤魂野鬼,删掉」。
import open3d as o3d
# 半径滤波去噪
clean_pcd_radius, ind = clean_pcd.remove_radius_outlier(
nb_points=6, # 半径内至少要有6个点
radius=0.5 # 搜索半径 0.5m
)
outlier_radius = clean_pcd.select_by_index(ind, invert=True)
print(f"半径滤波后点数: {len(clean_pcd_radius.points)}")
print(f"被删除的孤立点: {len(outlier_radius.points)}")
| 滤波方法 | 核心参数 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 统计滤波 | k, std_ratio | 密度均匀的场景 | 自适应强,参数好调 | 密度不均时可能误删 |
| 半径滤波 | r, nb_points | 密度变化大的场景 | 物理意义明确,直观 | 参数需要根据场景调整 |
我的建议:实际项目中,我通常把两个滤波串联使用。先做统计滤波去掉明显的离群点,再做半径滤波清理残留的孤立点。顺序不要搞反——先粗筛再精筛,效果最好。
3.4 完整预处理流程
把上面三个步骤串起来,就是一个完整的预处理管线:
import open3d as o3d
def preprocess_point_cloud(pcd_path, voxel_size=0.1,
nb_neighbors=20, std_ratio=2.0,
radius=0.5, min_points=6):
"""
点云预处理完整流程
"""
# 1. 读取原始点云
pcd = o3d.io.read_point_cloud(pcd_path)
print(f"原始点数: {len(pcd.points)}")
# 2. 体素滤波降采样
pcd_down = pcd.voxel_down_sample(voxel_size)
print(f"降采样后: {len(pcd_down.points)}")
# 3. 统计滤波去噪
cl, ind = pcd_down.remove_statistical_outlier(
nb_neighbors=nb_neighbors, std_ratio=std_ratio
)
pcd_stat = pcd_down.select_by_index(ind)
# 4. 半径滤波去噪
pcd_final, ind = pcd_stat.remove_radius_outlier(
nb_points=min_points, radius=radius
)
print(f"最终点数: {len(pcd_final.points)}")
return pcd_final
# 使用示例
clean_cloud = preprocess_point_cloud("raw_cloud.pcd")
小技巧:如果你不确定参数怎么设,可以先可视化一下点云,看看噪声的分布情况。Open3D 的 draw_geometries() 函数可以帮你快速预览。我每次调参前都会先看一眼数据,心里有个底再动手。
好了,这就是点云预处理的三个核心操作。体素滤波负责「减量」,统计滤波和半径滤波负责「提质」。三者配合,才能得到干净、轻量的点云数据,为后续的配准、分割、识别打好基础。
记住一句话:预处理做得好,后面少加班。
公众号:蓝海资料掘金营,微信deep3321