点云预处理:从原始数据到可用特征
大家好,我是你们的老朋友。今天我们来聊聊点云预处理。说实话,很多刚入行的朋友拿到点云数据就直接往上怼分割算法,结果效果惨不忍睹。我当年也犯过这个错——有一次项目赶进度,跳过预处理直接跑分割,结果出来的结果连我自己都看不下去。后来我学乖了:预处理做得好,后续工作省一半力气。
点云预处理说白了就是给原始数据「洗个澡」。激光雷达扫出来的点云,里面啥都有:噪声点、离群点、冗余点,还有各种乱七八糟的干扰。我们要做的,就是把这些「脏东西」清理干净,同时把有用的特征提取出来。
1. 体素滤波降采样:给点云「减肥」
先说说体素滤波。你想想看,激光雷达一秒钟能扫几十万个点,这么多点全拿来算,你的显卡受得了吗?反正我的笔记本曾经因为点云太密直接蓝屏了...所以第一步,降采样。
体素滤波的原理其实很简单:把空间切成一个个小立方体(体素),每个立方体里只保留一个点。这个点可以是重心,也可以是中心点。我个人习惯用重心,因为能更好地保留原始形状。
import open3d as o3d
# 读取点云
pcd = o3d.io.read_point_cloud("scene.pcd")
print(f"原始点数:{len(pcd.points)}")
# 体素滤波降采样
voxel_size = 0.05 # 5厘米体素
down_pcd = pcd.voxel_down_sample(voxel_size)
print(f"降采样后点数:{len(down_pcd.points)}")
# 可视化对比
o3d.visualization.draw_geometries([pcd], window_name="原始点云")
o3d.visualization.draw_geometries([down_pcd], window_name="降采样后")
2. 统计滤波去噪:干掉「飘在空中的点」
降采样之后,接下来就是去噪。统计滤波是我最常用的去噪方法之一。它干的事情很简单:每个点周围找K个邻居,算平均距离。如果这个距离太大,说明它是离群点,干掉它。
为什么会这样?因为真实物体表面的点,邻居距离都比较均匀。而那些噪声点——比如飞鸟、扬尘——它们周围空荡荡的,距离一下子就大了。
# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
nb_neighbors=20, # 邻居点数
std_ratio=2.0 # 标准差倍数阈值
)
clean_pcd = down_pcd.select_by_index(ind)
# 被剔除的噪声点
noise_pcd = down_pcd.select_by_index(ind, invert=True)
print(f"保留的点数:{len(clean_pcd.points)}")
print(f"剔除的噪声点数:{len(noise_pcd.points)}")
3. 半径滤波去噪:另一种思路
半径滤波和统计滤波思路不同。它是在每个点周围画个球,如果球里的邻居太少,就认为这个点是孤立的,删掉。
什么时候用半径滤波?我个人的经验是:当你的点云密度不均匀时,半径滤波比统计滤波更靠谱。比如室内场景,近处点密、远处点稀,统计滤波容易把远处的正常点误删。
# 半径滤波去噪
clean_pcd_r = clean_pcd.remove_radius_outlier(
nb_points=16, # 半径内至少需要的点数
radius=0.05 # 搜索半径(米)
)[0]
| 方法 | 适用场景 | 关键参数 |
|---|---|---|
| 统计滤波 | 密度均匀的场景 | nb_neighbors, std_ratio |
| 半径滤波 | 密度不均匀的场景 | nb_points, radius |
4. 直通滤波:快速「切」出感兴趣区域
直通滤波,说白了就是「切一刀」。你只想要某个范围内的点,比如地面以上1米到2米之间的区域,那就设个范围,把外面的全扔掉。
这个操作在自动驾驶里特别常用。比如我只想看行人,那就把地面以下和头顶以上的点全切掉。速度快,效果好。
import numpy as np
# 将点云转为numpy数组
points = np.asarray(clean_pcd.points)
# 直通滤波:只保留Z轴在0.5到2.0米之间的点
z_min, z_max = 0.5, 2.0
mask = (points[:, 2] >= z_min) & (points[:, 2] <= z_max)
filtered_points = points[mask]
# 创建新的点云对象
filtered_pcd = o3d.geometry.PointCloud()
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
print(f"直通滤波后点数:{len(filtered_pcd.points)}")
5. 点云法向量估计:给点云「长毛」
最后一步,法向量估计。法向量是什么?就是每个点所在表面的朝向。有了法向量,你才能做分割、识别、配准这些高级操作。
法向量估计的原理:找每个点的K个邻居,拟合一个平面,平面的法线就是法向量。嗯,这里要注意:法向量的方向是模糊的,需要做定向处理。
# 法向量估计
filtered_pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(
radius=0.1, # 搜索半径
max_nn=30 # 最大邻居数
)
)
# 可视化法向量
o3d.visualization.draw_geometries(
[filtered_pcd],
point_show_normal=True,
window_name="法向量可视化"
)
# 获取法向量数据
normals = np.asarray(filtered_pcd.normals)
print(f"法向量形状:{normals.shape}") # (N, 3)
完整预处理流程
好了,上面这些步骤串起来,就是一个完整的预处理管线。我一般按这个顺序来:
- 体素滤波降采样 — 先减肥,减少计算量
- 统计滤波去噪 — 干掉明显的离群点
- 半径滤波去噪 — 补刀,处理密度不均的情况
- 直通滤波 — 切出感兴趣区域
- 法向量估计 — 提取几何特征
def preprocess_point_cloud(pcd, voxel_size=0.05):
"""完整的点云预处理管线"""
# 1. 降采样
pcd_down = pcd.voxel_down_sample(voxel_size)
# 2. 统计滤波
pcd_clean, _ = pcd_down.remove_statistical_outlier(20, 2.0)
# 3. 半径滤波
pcd_clean = pcd_clean.remove_radius_outlier(16, 0.05)[0]
# 4. 直通滤波(Z轴)
points = np.asarray(pcd_clean.points)
mask = (points[:, 2] >= 0.3) & (points[:, 2] <= 3.0)
pcd_filtered = o3d.geometry.PointCloud()
pcd_filtered.points = o3d.utility.Vector3dVector(points[mask])
# 5. 法向量估计
pcd_filtered.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(0.1, 30)
)
return pcd_filtered
# 一键预处理
processed_pcd = preprocess_point_cloud(pcd)
print(f"预处理完成,最终点数:{len(processed_pcd.points)}")
说实话,这套流程我用了好几年,在室内场景、室外场景、机器人导航这些项目里都验证过。当然,具体参数要根据你的数据微调。比如激光雷达型号不同,点云密度差很多,体素大小和滤波参数都得跟着变。
嗯,今天就先聊到这儿。预处理这块看似基础,但真的决定了后续算法的上限。下次你们跑分割模型效果不好,先别急着调网络结构,回头看看预处理有没有做好——我敢打赌,一半的问题出在这里。