点云过滤
摘要:本文整理直通滤波、体素下采样、统计离群点过滤与半径离群点过滤,从空间范 围、点云密度和邻域分布理解参数作用。每种方法分别配合 PCL 核心代码讲解,说明如何为地面分割、聚类与配准准备输入。
1. 点云过滤的目标
过滤用于限制处理范围、减少点数或去除孤立噪声。不同方法解决的问题不同:直通滤波按坐标范围选择点,体素下采样降低空间采样密度,离群点过滤根据邻域分布剔除异常点。过滤不能直接替代地面分割,也不保证保留下来的点都是有效目标。
下文代码为处理函数内的核心片段,输入 cloud 是非空的 PCL 点云指针,坐标单位为米;配置数值是教学示例,不是传感器通用默认值。
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/filters/filter.h>
#include <vector>
using PointT = pcl::PointXYZ;
using Cloud = pcl::PointCloud<PointT>;
1.1 代码讲解:先移除无效坐标
Cloud::Ptr finite_cloud(new Cloud);
std::vector<int> finite_to_raw;
pcl::removeNaNFromPointCloud(*cloud, *finite_cloud, finite_to_raw);
finite_to_raw[k] 记录过滤后第 k 个点在原始输入中的索引。输入存在无效坐标时,is_dense 必须正确设为 false;该标记若错误地为 true,过滤器可能跳过逐点检查。外部数据不可信时,应先用 pcl::isFinite 检查坐标及密集标记。
删点会改变点数和索引,后续搜索结构应基于过滤后的输入重新建立。含强度、时间或线束字段的数据,需要选用匹配的点类型;直接转成 PointXYZ 会丢失这些字段。
2. PassThrough(直通滤波)
参考:PCL 直通滤波教程。
原理:选定一个字段,保留其值在指定范围内 的点。例如对高度轴:
流程:选择字段 → 设置范围 → 判断每个点 → 输出保留点。
| 配置项 | 示例值 | 作用 |
|---|---|---|
setFilterFieldName | "z" | 指定过滤字段 |
setFilterLimits | -2.0, 3.0 | 限制坐标范围,单位:米 |
setNegative | false | true 时反向选择范围外的点 |
2.1 代码讲解:裁剪高度范围
#include <pcl/filters/passthrough.h>
// 以下过滤语句放在处理函数内部。
Cloud::Ptr roi_cloud(new Cloud);
pcl::PassThrough<PointT> pass;
pass.setInputCloud(finite_cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(-2.0f, 3.0f);
pass.setNegative(false);
pass.filter(*roi_cloud);
setInputCloud 指定当前输入,filter 将结果写入独立输出。范围描述的是当前坐标系中的 z,不能直接理解为离地高度;传感器坐标系与车辆坐标系的原点不同,需要先明确变换关系。
2.2 代码讲解:连续裁剪多个方向
Cloud::Ptr forward_cloud(new Cloud);
pass.setInputCloud(roi_cloud);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0f, 50.0f);
pass.filter(*forward_cloud);
第二次过滤使用第一次的结果,因此保留的是两个条件的交集。多个方向可重复使用直通过滤器;需要旋转包围盒时,可考虑 pcl::CropBox,而不是将单轴范围当成任意方向的空间区域。
3. VoxelGrid(体素下采样)
参考:PCL 体素下采样教程。
原理:建立三维体素网格,用每个非空体素内点的质心代表这些点:
质心不一定是某个原始点,也不同于体素几何中心。体素下采样主要减少冗余采样,不会自动判断一个体素是不是噪声。
| 配置项 | 示例值 | 调参作用 |
|---|---|---|
setLeafSize | 0.2, 0.2, 0.2 | 三个方向的体素尺寸 [m];增大通常减少点数,但损失细节 |
setMinimumPointsNumberPerVoxel | 1 | 体素所需最少原始点数;增大可能去掉稀疏有效结构 |
3.1 代码讲解:设置网格并生成质心
#include <pcl/filters/voxel_grid.h>
Cloud::Ptr sampled_cloud(new Cloud);
pcl::VoxelGrid<PointT> voxel;
voxel.setInputCloud(forward_cloud);
voxel.setLeafSize(0.2f, 0.2f, 0.2f);
voxel.setMinimumPointsNumberPerVoxel(1);
voxel.filter(*sampled_cloud);
三个叶尺寸都必须为正,可以根据场景设置不同的水平与垂直分辨率。网格较大时,小障碍物、路缘或细杆可能被合并,不能只以点数减少作为效果指标。
3.2 代码讲解:检查点数与索引关系
const auto before_count = forward_cloud->size();
const auto after_count = sampled_cloud->size();
const double retained_ratio = before_count == 0 ? 0.0
: static_cast<double>(after_count) / before_count;
retained_ratio 用于记录输出规模,不能直接表示保留了多少原始表面细节。下采样后一个点代表多个原始点,没有天然的一对一索引映射。若需要将聚类结果回填到原始点云,应维护体素与原始索引关系,具体流程见点云聚类算法。
4. StatisticalOutlierRemoval(统计离群点过滤)
参考:PCL 统计离群点过滤教程。
原理:对每个点计算其 K 个近邻的平均距离,再与整帧平均距离分布比较:
设这些平均距离的全局均值与标准差为 ,常用保留判据为:
距离明显偏大的点被认为较孤立。该统计假设不一定适用于密度变化很大的整帧激光雷达点云。
| 配置项 | 示例值 | 调参作用 |
|---|---|---|
setMeanK | 30 | 统计邻域的近邻数量;应结合有效点数与局部密度设置 |
setStddevMulThresh | 1.0 | 标准差倍数;减小会更严格,增大保留更多点 |
setNegative | false | true 时输出被判定为离群的点 |
4.1 代码讲解:计算邻域统计并输出内点
#include <pcl/filters/statistical_outlier_removal.h>
Cloud::Ptr clean_cloud(new Cloud);
pcl::StatisticalOutlierRemoval<PointT> sor;
sor.setInputCloud(sampled_cloud);
sor.setMeanK(30);
sor.setStddevMulThresh(1.0);
sor.setNegative(false);
sor.filter(*clean_cloud);
PCL 内部完成近邻搜索和距离统计。示例在下采样后计算邻域,参数应按下采样后的密度设置;如果先做统计过滤,输入规模与统计分布都不同,不能沿用同一组数值并期待相同结果。
4.2 代码讲解:查看被剔除点
Cloud::Ptr rejected_cloud(new Cloud);
sor.setNegative(true);
sor.filter(*rejected_cloud);
反向输出可以检查远处物体、细杆或边缘是否被误删。过滤后点云更平滑,不代表结果一定更准确;应将保留点和剔除点一起观察。
5. RadiusOutlierRemoval(半径离群点过滤)
参考:PCL 半径离群点过滤教程。
原理:在固定半径内统计邻居数量,邻居太少则删除。用不含查询点自身的邻域表示:
| 配置项 | 示例值 | 调参作用 |
|---|---|---|
setRadiusSearch | 0.5 | 搜索半径 [m];应结合点间距设置 |
setMinNeighborsInRadius | 3 | 最少邻居数;增大过滤更严格 |
5.1 代码讲解:根据局部点数过滤
#include <pcl/filters/radius_outlier_removal.h>
Cloud::Ptr radius_clean_cloud(new Cloud);
pcl::RadiusOutlierRemoval<PointT> ror;
ror.setInputCloud(sampled_cloud);
ror.setRadiusSearch(0.5);
ror.setMinNeighborsInRadius(3);
ror.setNegative(false);
ror.filter(*radius_clean_cloud);
这里与 SOR 分别使用同一份 sampled_cloud