4、点云基础处理:体素滤波降采样、直通滤波、半径滤波与统计滤波去噪
点云数据拿到手,第一件事是什么?
不是急着做检测,而是先「洗数据」。
我刚开始做激光雷达感知那会儿,上来就把原始点云丢进检测模型,结果帧率直接掉到个位数,还全是噪点。后来才明白——原始点云里大量冗余点、离群噪点、无关区域,不处理根本没法用。
这一章,我们就聊聊点云预处理里最常用的四个滤波方法。说白了,就是给点云「瘦身」和「去杂质」。
4.1 体素滤波降采样
体素滤波,我习惯叫它「点云瘦身术」。
你想想看,一个64线激光雷达,一帧能扫出十几万个点。这么多点,很多是重复的、冗余的。比如一面墙,你扫出1000个点和扫出100个点,对检测来说差别不大,但计算量差了一个数量级。
原理其实很简单:
- 把三维空间划分成一个个小立方体(体素)
- 每个体素内只保留一个点(通常是重心或中心点)
- 体素越大,降采样越狠
我在项目中遇到过一个问题:体素大小设得太小,降采样效果不明显;设得太大,远处的行人直接变成一两个点,检测器根本认不出来。后来我总结了一个经验——体素边长一般设为0.1m~0.5m,具体看你的场景和算力。
核心参数:leaf_size(体素边长)
经验值:城区道路0.2m,高速场景0.3m~0.5m
import open3d as o3d
# 读取点云
pcd = o3d.io.read_point_cloud("lidar_frame.pcd")
# 体素滤波降采样
voxel_size = 0.2 # 单位:米
down_pcd = pcd.voxel_down_sample(voxel_size)
print(f"原始点数: {len(pcd.points)}")
print(f"降采样后: {len(down_pcd.points)}")
我的小技巧:如果你用PCL库,记得先计算点云包围盒,再根据包围盒尺寸动态调整体素大小。这样能自适应不同场景。
4.2 直通滤波
直通滤波,说白了就是「切一刀」。
激光雷达扫出来的点云,范围很大。但我们的检测区域往往只关心前方几十米、左右几个车道。后面的、太远的、太高的点,留着干嘛?
应用场景:
- 只保留前方0~50m的点云
- 去掉地面以上的点(比如天桥、树叶)
- 去掉左右两侧超出车道范围的点
我曾经犯过一个低级错误:直通滤波时只设了x轴范围,忘了设y轴。结果路边的灌木丛全被当成障碍物,导致车辆频繁误刹车。嗯,从那以后我每次都会检查三个轴的范围。
import numpy as np
def passthrough_filter(points, axis=0, min_val=-10, max_val=50):
"""
直通滤波
:param points: Nx3 点云数组
:param axis: 0=x, 1=y, 2=z
:param min_val: 最小值
:param max_val: 最大值
"""
mask = (points[:, axis] >= min_val) & (points[:, axis] <= max_val)
return points[mask]
# 示例:保留前方0~50m,左右±5m,高度-1~2m
points = np.asarray(pcd.points)
filtered = passthrough_filter(points, 0, 0, 50) # x轴
filtered = passthrough_filter(filtered, 1, -5, 5) # y轴
filtered = passthrough_filter(filtered, 2, -1, 2) # z轴
注意:直通滤波的顺序会影响结果。我建议先做z轴(高度)滤波,再做x、y轴。因为先去掉地面和天空的点,能减少后续计算量。
4.3 半径滤波
半径滤波,用来干掉「孤魂野鬼」。
激光雷达扫到一些稀疏的噪点,比如飞鸟、灰尘、传感器噪声。这些点周围没有邻居,一看就是「不合群」的。
判断逻辑:
- 对每个点,统计它半径r范围内的邻居数量
- 如果邻居数少于阈值k,就判定为离群点,删除
我习惯用半径滤波来处理多径反射造成的鬼影点。有一次在隧道里,激光雷达打到墙壁再反射回来,产生了一堆虚假点。半径滤波一上,全清掉了。
# Open3D 半径滤波
cl, ind = down_pcd.remove_radius_outlier(nb_points=6, radius=0.5)
filtered_pcd = down_pcd.select_by_index(ind)
print(f"滤波前: {len(down_pcd.points)}")
print(f"滤波后: {len(filtered_pcd.points)}")
参数调优建议:
- 半径:一般设为0.3~1.0m,取决于点云密度
- 邻居数阈值:通常设为3~10,密度越大阈值越高
- 先做体素降采样,再做半径滤波,效果更好
4.4 统计滤波
统计滤波,比半径滤波更「科学」一点。
它不是用固定半径,而是用统计学的思路——计算每个点到它k个最近邻的平均距离。如果这个平均距离偏离整体分布太远,就认为是离群点。
具体步骤:
- 对每个点,找它的k个最近邻
- 计算到这些邻居的平均距离
- 所有点的平均距离构成一个分布(近似高斯分布)
- 设定一个标准差倍数std_ratio,超出范围的剔除
说白了,就是「大多数人都在这个距离范围内,你离大家太远,那你就是异类」。
# Open3D 统计滤波
cl, ind = filtered_pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
clean_pcd = filtered_pcd.select_by_index(ind)
print(f"统计滤波前: {len(filtered_pcd.points)}")
print(f"统计滤波后: {len(clean_pcd.points)}")
我的经验:std_ratio一般取1.5~3.0。取1.5比较激进,会去掉较多点;取3.0比较保守。我通常先用2.0试跑一遍,看效果再微调。
4.5 四种滤波方法对比
| 滤波方法 | 主要用途 | 核心参数 | 适用场景 |
|---|---|---|---|
| 体素滤波 | 降采样、减少点数 | leaf_size | 所有场景,预处理第一步 |
| 直通滤波 | 裁剪ROI区域 | axis, min, max | 固定场景,如车道检测 |
| 半径滤波 | 去除稀疏离群点 | radius, nb_points | 噪点较少的场景 |
| 统计滤波 | 去除统计离群点 | nb_neighbors, std_ratio | 噪点分布不均匀的场景 |
4.6 实战流程:一个完整的预处理管线
在实际项目中,我不会只用一种滤波。通常的流程是:
- 直通滤波:先切出ROI区域,去掉无关点
- 体素滤波:降采样,减少计算量
- 统计滤波:去除离群噪点
- (可选)半径滤波:如果还有残留噪点,再补一刀
这个顺序我踩过坑才总结出来的。以前我先把体素滤波放在第一步,结果直通滤波时还要处理大量冗余点,白白浪费算力。先切ROI再降采样,效率高很多。
def preprocess_pipeline(pcd, roi_range, voxel_size, nb_neighbors=20, std_ratio=2.0):
# 1. 直通滤波
points = np.asarray(pcd.points)
mask_x = (points[:, 0] >= roi_range[0]) & (points[:, 0] <= roi_range[1])
mask_y = (points[:, 1] >= roi_range[2]) & (points[:, 1] <= roi_range[3])
mask_z = (points[:, 2] >= roi_range[4]) & (points[:, 2] <= roi_range[5])
mask = mask_x & mask_y & mask_z
pcd.points = o3d.utility.Vector3dVector(points[mask])
# 2. 体素滤波
pcd = pcd.voxel_down_sample(voxel_size)
# 3. 统计滤波
_, ind = pcd.remove_statistical_outlier(nb_neighbors, std_ratio)
pcd = pcd.select_by_index(ind)
return pcd
# 使用示例
roi = [0, 50, -5, 5, -1, 2] # x, y, z范围
clean_pcd = preprocess_pipeline(pcd, roi, voxel_size=0.2)
避坑指南:
- 体素滤波后,点云密度会下降,后续的统计滤波参数要相应调低
- 直通滤波的边界值不要设得太紧,留一点余量,防止目标被误切
- 如果场景中有大量动态物体(如行人、车辆),统计滤波的std_ratio建议取大一点
好了,四种滤波方法讲完了。你可能会问:这么多参数,怎么调?
我的建议是:先拿一帧数据,可视化出来,肉眼观察噪点和目标分布。然后从体素滤波开始,逐步调整。调参这事,说白了就是「试」出来的。多试几次,你就有感觉了。
可可精品资料库