4. 点云滤波:体素滤波、统计滤波、半径滤波的原理与Open3D实现

点云数据这东西,说白了就是一堆三维坐标点的集合。但现实世界中的点云,往往带着各种噪声和冗余。我刚开始做点云处理那会儿,拿到的数据动不动就几百万个点,直接处理?电脑风扇转得跟直升机似的。所以,滤波就成了点云预处理的第一道工序。

今天咱们聊聊三种最常用的滤波方法:体素滤波、统计滤波、半径滤波。这三种方法各有各的脾气,用对了地方,效果立竿见影。

4.1 体素滤波(Voxel Grid Filter)

体素滤波,我习惯叫它「下采样」。它的核心思想很简单:把三维空间切成一个个小立方体(体素),每个立方体里只保留一个点。这个点通常是立方体内所有点的重心,或者直接取中心点。

为什么要用体素滤波?

你想想看,一个激光雷达扫描一圈,可能产生几十万甚至上百万个点。这么多点,很多都是冗余的——比如一面平整的墙,其实几十个点就能描述清楚,没必要用几千个。体素滤波就是用来干这个的:在保持物体几何形状的前提下,大幅减少点的数量。

核心参数:体素大小(voxel_size)

体素大小决定了滤波的强度。体素越大,下采样越狠,点云越稀疏。体素越小,保留的细节越多。我一般从0.01米开始试,根据场景调整。

Open3D实现:

import open3d as o3d
import numpy as np

# 读取点云
pcd = o3d.io.read_point_cloud("input.pcd")
print(f"原始点云点数:{len(pcd.points)}")

# 体素滤波,设置体素大小为0.05米
voxel_size = 0.05
downsampled_pcd = pcd.voxel_down_sample(voxel_size)
print(f"体素滤波后点数:{len(downsampled_pcd.points)}")

# 可视化对比
o3d.visualization.draw_geometries([pcd], window_name="原始点云")
o3d.visualization.draw_geometries([downsampled_pcd], window_name="体素滤波后")

我的经验:体素滤波后的点云,点的分布会更均匀。但要注意,如果体素设得太大,小物体会直接消失。我在做室内场景重建时,体素大小一般设在0.02-0.05米之间,既能降采样,又不丢失门框、桌腿这些细节。

4.2 统计滤波(Statistical Outlier Removal)

统计滤波,说白了就是「看邻居」。它假设:如果一个点周围没几个邻居,那它大概率是个离群点(噪声)。

具体怎么算?对每个点,计算它到所有邻居点的平均距离。这些平均距离会形成一个高斯分布。然后,我们把那些平均距离超出「均值 ± k倍标准差」的点干掉。

两个关键参数:

  • nb_neighbors:邻居数量。我一般设20-50,点数多就设大点。
  • std_ratio:标准差倍数。这个值越小,滤波越狠。默认1.0,我常用0.5-1.5之间。

Open3D实现:

import open3d as o3d

pcd = o3d.io.read_point_cloud("input.pcd")

# 统计滤波
cl, ind = pcd.remove_statistical_outlier(
    nb_neighbors=20,     # 邻居点数
    std_ratio=1.0        # 标准差倍数
)

# 提取内点和外点
inlier_pcd = pcd.select_by_index(ind)
outlier_pcd = pcd.select_by_index(ind, invert=True)

print(f"内点数量:{len(inlier_pcd.points)}")
print(f"外点(噪声)数量:{len(outlier_pcd.points)}")

# 可视化:内点白色,噪声红色
inlier_pcd.paint_uniform_color([0.8, 0.8, 0.8])
outlier_pcd.paint_uniform_color([1.0, 0, 0])
o3d.visualization.draw_geometries([inlier_pcd, outlier_pcd])

避坑指南:我曾经在扫描一个金属零件时,发现统计滤波把零件边缘的「真实点」也给滤掉了。后来排查发现,是因为零件边缘的点密度本来就低,统计滤波误判成了噪声。解决办法:把std_ratio调大一点,或者先做体素滤波让点云均匀化,再做统计滤波。

4.3 半径滤波(Radius Outlier Removal)

半径滤波的思路更直接:在指定半径的球体内,如果点的数量少于某个阈值,就把这个点视为噪声干掉。

你可以把它想象成「社交圈检查」——如果你周围的朋友太少,你就被踢出群聊了。

两个参数:

  • radius:搜索半径。这个值取决于点云的密度,我一般先估算一下点间距,再设半径。
  • min_pts:最少点数。半径内少于这个数,就删除。

Open3D实现:

import open3d as o3d

pcd = o3d.io.read_point_cloud("input.pcd")

# 半径滤波
cl, ind = pcd.remove_radius_outlier(
    nb_points=6,         # 半径内最少点数
    radius=0.05          # 搜索半径(米)
)

inlier_pcd = pcd.select_by_index(ind)
outlier_pcd = pcd.select_by_index(ind, invert=True)

print(f"内点数量:{len(inlier_pcd.points)}")
print(f"外点数量:{len(outlier_pcd.points)}")

# 可视化
inlier_pcd.paint_uniform_color([0.8, 0.8, 0.8])
outlier_pcd.paint_uniform_color([1.0, 0, 0])
o3d.visualization.draw_geometries([inlier_pcd, outlier_pcd])

三种滤波对比:

滤波方法 主要用途 核心参数 适用场景
体素滤波 降采样、均匀化 voxel_size 点云数据量过大时
统计滤波 去除离群噪声 nb_neighbors, std_ratio 点云中有稀疏噪声
半径滤波 去除孤立点 radius, min_pts 点云中有孤立小簇噪声

4.4 实战建议:滤波流程怎么搭?

在实际项目中,我很少只用一种滤波。通常的流程是:

  1. 先体素滤波:把几百万个点降到几十万,让后续处理跑得动。
  2. 再统计滤波或半径滤波:根据噪声类型选择。如果是稀疏的离群点,用统计滤波;如果是孤立的小簇噪声,用半径滤波。
  3. 最后再检查:可视化看看有没有误删重要点。

嗯,这里要注意:滤波参数的设置没有标准答案,完全取决于你的数据。我建议你写个脚本,把参数做成滑块,实时调整实时看效果。Open3D的GUI模块可以做到,后面章节会讲到。

一个小技巧:如果你不确定参数设多少,可以先统计一下点云的平均点间距。用pcd.compute_nearest_neighbor_distance()算一下,然后取平均距离的2-3倍作为体素大小或搜索半径,效果通常不错。

好了,三种滤波的原理和实现就讲完了。说白了,滤波就是「去粗取精」的过程——去掉冗余和噪声,保留结构和特征。下一章咱们聊聊点云的特征提取,那才是真正有意思的部分。