一、点云预处理技术:让原始数据变得“可用”
做激光雷达点云深度学习,有个残酷的现实我得先告诉你:原始点云数据,基本没法直接用。
你想想看,一帧64线激光雷达的数据,动辄十几万个点。这里面有噪声、有离群点、有地面、有天空,甚至还有雨滴和飞虫反射回来的假点。如果直接把这种“脏数据”扔进网络,模型大概率会学歪。
所以,预处理这一步,说白了就是给数据“洗澡”。我做了这么多年点云项目,可以负责任地说:预处理做得好,模型训练事半功倍;预处理偷懒,后面调参调到哭。
今天我们就来聊聊点云预处理的几个核心操作:降采样、去噪、滤波、法向量估计。嗯,一个一个来。
核心观点:预处理不是“可选项”,而是“必选项”。它直接决定了后续特征提取和模型收敛的质量。
1.1 体素滤波降采样:用“格子”来压缩数据
体素滤波,英文叫Voxel Grid Filter。原理特别简单:把空间切成一个个小立方体(体素),每个体素里只保留一个点。
这个点怎么选?通常是体素内所有点的重心,或者直接取中心点。我个人习惯用重心,因为能保留更多局部形状信息。
为什么要降采样?原因有两个:
- 计算量太大——十几万个点,网络根本吃不消
- 冗余太多——同一平面上的点,其实信息高度重复
体素大小怎么设?我一般设0.05米到0.1米。太小了降采样效果不明显,太大了会把细节磨平。我在做自动驾驶路沿检测时,试过0.2米的体素,结果路沿直接变成了一坨“毛边”,根本没法用。
import open3d as o3d
# 读取点云
pcd = o3d.io.read_point_cloud("lidar_frame.pcd")
# 体素滤波,体素大小0.05m
down_pcd = pcd.voxel_down_sample(voxel_size=0.05)
print(f"原始点数: {len(pcd.points)}")
print(f"降采样后: {len(down_pcd.points)}")
我的小技巧:体素大小最好根据场景动态调整。比如城区道路用0.05m,高速场景可以放宽到0.1m。别死板套一个参数。
1.2 统计滤波去噪:干掉“孤魂野鬼”
统计滤波,英文Statistical Outlier Removal。它的逻辑是:如果一个点周围邻居太少,那它大概率是噪声。
具体做法是:对每个点,计算它到k个最近邻的平均距离。如果这个距离超过全局均值+若干倍标准差,就判定为离群点,直接删掉。
我遇到过最典型的场景:下雨天采集的数据,雨滴反射回来的点就是典型的“孤魂野鬼”。统计滤波一过,干干净净。
参数怎么调?两个关键值:
- k(邻居数):一般设20-50。太小了容易误删,太大了去噪效果变差
- std_ratio(标准差倍数):一般设1.0-2.0。值越小,去噪越狠
# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
nb_neighbors=20,
std_ratio=2.0
)
clean_pcd = down_pcd.select_by_index(ind)
print(f"去噪前: {len(down_pcd.points)}")
print(f"去噪后: {len(clean_pcd.points)}")
注意:统计滤波对稀疏场景不友好。比如远距离的点,本身邻居就少,容易被误删。我曾经在高速场景吃过这个亏,后来改用半径滤波才解决。
1.3 半径滤波去噪:另一种“社交距离”判断
半径滤波和统计滤波思路类似,但判断标准不同:如果一个点周围指定半径内的邻居数少于阈值,就删掉。
说白了,统计滤波看的是“距离分布”,半径滤波看的是“绝对密度”。
什么时候用半径滤波?我个人的经验是:当场景密度不均匀时,半径滤波更靠谱。比如近处点很密、远处点很稀的场景,统计滤波容易把远处的有效点也删掉,但半径滤波只要设好半径和最小点数,就能保留下来。
# 半径滤波去噪
cl, ind = clean_pcd.remove_radius_outlier(
nb_points=16,
radius=0.05
)
final_pcd = clean_pcd.select_by_index(ind)
避坑指南:我曾经在隧道场景里用半径滤波,结果隧道壁上的点被大量误删。后来发现是半径设得太小。记住:半径要大于点云的平均点间距,否则等于没滤波。
1.4 直通滤波:快速“切”出感兴趣区域
直通滤波,PassThrough Filter,是预处理里最简单、最暴力的操作。直接按坐标轴范围切一刀。
比如我只关心前方50米内的障碍物,那就把x轴范围设在0到50米。或者我只关心地面以上2米内的物体,那就把z轴范围设在-0.5到2.0米。
为什么需要直通滤波?因为很多场景下,我们只对特定区域感兴趣。比如自动驾驶,车顶的树叶、远处的天空,这些点对模型训练毫无意义,反而会引入噪声。
import numpy as np
# 手动实现直通滤波
points = np.asarray(final_pcd.points)
# 只保留前方30米,高度-0.5到2.0米
mask = (points[:, 0] > 0) & (points[:, 0] < 30) & \
(points[:, 2] > -0.5) & (points[:, 2] < 2.0)
filtered_points = points[mask]
经验之谈:直通滤波一定要放在预处理的第一步。先把无关区域切掉,后面的降采样和去噪计算量能减少一半以上。
1.5 点云法向量估计:给每个点“定方向”
法向量,就是点云表面每个点的朝向。它有什么用?法向量是很多高级特征的基础。
比如:
- 地面分割——地面点的法向量基本朝上
- 边缘检测——法向量突变的地方就是边缘
- 点云配准——法向量能帮助找到对应点
法向量怎么算?最常用的方法是PCA(主成分分析)。对每个点,找它周围的k个邻居,然后计算这些点的协方差矩阵,最小特征值对应的特征向量就是法向量。
嗯,这里要注意:法向量有方向歧义性。同一个平面,法向量可以朝上也可以朝下。所以一般需要做“法向量定向”,让所有法向量都指向同一个方向(比如朝向视点)。
# 计算法向量
final_pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamHybrid(
radius=0.1, max_nn=30
)
)
# 查看法向量
normals = np.asarray(final_pcd.normals)
print(f"法向量形状: {normals.shape}")
我的习惯:法向量估计的搜索半径,一般设为体素大小的2-3倍。比如体素0.05m,搜索半径就设0.1-0.15m。这样既能保证邻居数量,又不会跨过物体边界。
1.6 特征计算:从几何到语义的“桥梁”
有了法向量,我们就可以计算更丰富的几何特征了。常见的点云特征包括:
| 特征名称 | 含义 | 典型应用 |
|---|---|---|
| 曲率 | 表面弯曲程度 | 角点检测、平面分割 |
| 粗糙度 | 局部表面起伏程度 | 地面与非地面分类 |
| FPFH | 快速点特征直方图 | 点云配准、物体识别 |
| SHOT | 方向直方图特征 | 3D物体识别 |
这些特征说白了,就是把点云的“几何形状”翻译成“数字向量”,让深度学习模型能理解。
我个人最常用的是曲率和粗糙度。为什么?因为计算快,而且对大部分任务都够用。FPFH虽然更鲁棒,但计算量太大,实时场景基本扛不住。
# 计算曲率(通过PCA特征值)
from sklearn.decomposition import PCA
def compute_curvature(points, k=30):
curvatures = []
for p in points:
# 找k个最近邻
# 计算协方差矩阵
# 特征值分解
# 曲率 = 最小特征值 / 特征值之和
pass # 具体实现略
return curvatures
总结一下:预处理不是“做完就扔”的步骤。它直接决定了后续模型能学到什么。体素滤波解决数据量问题,统计/半径滤波解决噪声问题,直通滤波解决范围问题,法向量和特征计算解决“语义鸿沟”问题。每一步都踩过坑,才敢说这些经验。