3. 直通滤波 (PassThrough)

一、直通滤波到底在干什么?

直通滤波,名字听着挺唬人。说白了,就是一刀切。

你想象一下,你面前有一堆点云数据,里面既有近处的花花草草,也有远处的建筑物。你只想要距离你5米到10米之间的那部分数据。怎么办?

直通滤波就是干这个的。它沿着某个坐标轴(比如X轴、Y轴或Z轴),设定一个范围。在这个范围之内的点,留下;范围之外的,全部砍掉。

我个人习惯把直通滤波叫做「范围过滤器」。它不关心点的颜色、强度,只关心点的位置。简单、粗暴,但非常实用。

核心思想: 设定一个维度(如Z轴),设定一个区间 [min, max],保留区间内的点,剔除区间外的点。

二、为什么需要直通滤波?

你可能会问:「直接处理全部点云不行吗?」

行,但效率太低。我举个例子。

有一次我在做道路检测项目,原始点云有上千万个点。里面包含了天空中的飞鸟、远处的山、还有地面上的车辆。如果直接对这些点做聚类或分割,计算量巨大,而且容易误判。

我只需要地面附近2米范围内的点。这时候,一个直通滤波,沿着Z轴一裁,数据量直接减少80%。后续处理速度飞起。

直通滤波的典型应用场景:

  • 去除背景噪声: 比如去掉远处的点,只保留感兴趣区域。
  • 提取特定高度层: 比如只保留地面以上的点,用于行人检测。
  • 数据预处理: 在复杂算法之前,先快速缩小数据范围。

三、原理:其实就一个条件判断

直通滤波的原理,简单到令人发指。

对于点云中的每一个点,检查它在指定维度上的坐标值。如果这个值在设定的最小值和最大值之间,就保留;否则,丢弃。

用数学公式表达就是:

if min ≤ point.dimension ≤ max:
    保留该点
else:
    丢弃该点

嗯,就这么简单。但为什么它这么有效?因为很多场景下,我们关心的物体确实只存在于某个特定的空间范围内。

比如,自动驾驶中,激光雷达安装在车顶。它扫描到的点,Z轴(高度)方向,地面以下的部分肯定是噪声,车顶以上的部分可能是树枝或桥梁。我们只保留地面到车顶高度之间的点,这就是直通滤波的典型用法。

四、代码实现:手把手教你写

我用的是PCL(Point Cloud Library)的C++版本。当然,Python版本也有,但C++在工业界更常见。我个人推荐初学者先从C++入手,性能更好,理解更深。

4.1 核心代码片段

#include <pcl/filters/passthrough.h>

// 创建直通滤波对象
pcl::PassThrough<pcl::PointXYZ> pass;

// 设置输入点云
pass.setInputCloud(cloud);

// 设置滤波维度:这里选择Z轴
pass.setFilterFieldName("z");

// 设置范围:保留Z轴在0.5米到2.0米之间的点
pass.setFilterLimits(0.5, 2.0);

// 执行滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_filtered);

你看,核心代码就这几行。但有几个细节,我当年踩过坑,必须提醒你。

避坑指南:

  • 维度名称必须正确: 如果你用的是PointXYZ,维度名就是"x"、"y"、"z"。如果你用的是PointXYZRGB,维度名不变。但如果你自定义了点类型,一定要确认字段名。
  • 范围是闭区间: setFilterLimits(0.5, 2.0) 包含0.5和2.0这两个边界点。如果你不想包含边界,可以设置setFilterLimitsNegative(true)。
  • 输入输出指针: 记得cloud_filtered要提前创建,否则会报空指针错误。我曾经因为这个bug调试了半小时。

4.2 完整示例代码

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>

int main()
{
    // 加载点云
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPCDFile<pcl::PointXYZ>("input.pcd", *cloud) == -1)
    {
        PCL_ERROR("Couldn't read file input.pcd \n");
        return -1;
    }

    std::cout << "原始点云点数: " << cloud->points.size() << std::endl;

    // 创建直通滤波对象
    pcl::PassThrough<pcl::PointXYZ> pass;
    pass.setInputCloud(cloud);
    pass.setFilterFieldName("z");
    pass.setFilterLimits(0.5, 2.0);

    // 执行滤波
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
    pass.filter(*cloud_filtered);

    std::cout << "滤波后点云点数: " << cloud_filtered->points.size() << std::endl;

    // 保存结果
    pcl::io::savePCDFileASCII("output.pcd", *cloud_filtered);

    return 0;
}

五、参数调优:别让一刀切得太狠

直通滤波的参数很少,就两个:维度名称和范围。但调起来,还是有点门道的。

参数 说明 调优建议
setFilterFieldName 选择滤波的坐标轴 根据场景选择。地面场景常用Z轴,水平场景常用X或Y轴。
setFilterLimits(min, max) 设置保留范围 先观察点云分布,再设定范围。不要拍脑袋。
setFilterLimitsNegative 是否取反 默认false。设为true时,保留范围之外的点。

调优技巧:

  • 先可视化,再调参: 我习惯先用PCL Visualizer或CloudCompare看一眼点云的分布。比如Z轴的范围是多少,然后根据实际需求设定。不要上来就设个0到100,那跟没滤波一样。
  • 范围宁宽勿窄: 如果你不确定精确范围,先设宽一点。比如你想保留1米到2米,可以先设0.5到2.5。看看效果,再逐步缩小。太窄了会丢失有用数据。
  • 多维度组合使用: 直通滤波可以串联使用。先沿Z轴滤波,再沿X轴滤波。我做过一个项目,先裁掉地面以下的点,再裁掉左右两侧的噪声,效果非常好。

个人经验: 有一次我在做隧道点云处理,隧道内壁的点云非常密集。我原本想用直通滤波去掉隧道顶部的点,结果范围设得太窄,把隧道壁上的关键特征点也去掉了。后来我调整了范围,保留了顶部以下0.5米的缓冲区域,才保住了关键数据。记住,留点余量。

六、实战案例:从道路点云中提取车辆

好了,理论讲完了,代码也写了。咱们来一个真实的案例。

场景: 你有一份道路点云数据,里面包含路面、路边的树木、以及几辆汽车。你的目标是提取出车辆的点云。

思路:

  1. 第一步: 沿Z轴滤波。车辆的高度一般在0.5米到2.5米之间。路面和地面以下的点,直接去掉。
  2. 第二步: 沿Y轴滤波(假设Y轴是道路的横向方向)。车辆在道路中间,左右两侧的树木和建筑物,去掉。
  3. 第三步: 沿X轴滤波(假设X轴是道路的纵向方向)。只保留你感兴趣的路段。

代码实现:

// 第一步:Z轴滤波,提取车辆高度范围
pcl::PassThrough<pcl::PointXYZ> pass_z;
pass_z.setInputCloud(cloud);
pass_z.setFilterFieldName("z");
pass_z.setFilterLimits(0.5, 2.5);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_z(new pcl::PointCloud<pcl::PointXYZ>);
pass_z.filter(*cloud_z);

// 第二步:Y轴滤波,提取道路宽度范围
pcl::PassThrough<pcl::PointXYZ> pass_y;
pass_y.setInputCloud(cloud_z);
pass_y.setFilterFieldName("y");
pass_y.setFilterLimits(-3.0, 3.0);  // 假设道路宽6米
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_y(new pcl::PointCloud<pcl::PointXYZ>);
pass_y.filter(*cloud_y);

// 第三步:X轴滤波,提取感兴趣路段
pcl::PassThrough<pcl::PointXYZ> pass_x;
pass_x.setInputCloud(cloud_y);
pass_x.setFilterFieldName("x");
pass_x.setFilterLimits(10.0, 50.0);  // 提取10米到50米之间的路段
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pass_x.filter(*cloud_filtered);

经过这三步滤波,原始点云从几百万个点,减少到几万个点。而且这些点基本就是车辆和少量残留噪声。后续再用聚类算法,就能轻松提取出每辆车。

效果: 原始点云100万点 → 滤波后3万点。处理时间从5秒降到0.2秒。这就是直通滤波的魅力。

七、知识体系与核心逻辑

为了让你更直观地理解直通滤波在整个点云处理流程中的位置,我画了一张图。

直通滤波知识体系与核心逻辑 原始点云 直通滤波核心步骤 选择维度 → 设定范围 → 条件判断 滤波后点云 应用场景:去噪、提取ROI、数据预处理 关键参数 • 维度:x / y / z • 范围:[min, max] • 取反:true / false 注意事项 • 维度名必须匹配 • 范围是闭区间 • 指针不能为空

八、总结与避坑

直通滤波,说白了就是点云处理中的「入门级」滤波。但它非常实用,几乎每个项目都会用到。

最后,给你几个避坑建议:

  • 不要过度依赖直通滤波: 它只能处理空间范围上的噪声。对于离群点、稀疏噪声,它无能为力。这时候需要配合统计滤波或半径滤波。
  • 注意坐标系: 不同传感器,坐标系定义可能不同。有的激光雷达Z轴朝上,有的朝下。滤波前,先确认坐标系方向。
  • 多维度滤波时注意顺序: 我一般先做Z轴滤波,因为Z轴方向的数据量通常最大,先裁掉可以大幅减少后续计算量。

我的习惯: 每次做直通滤波前,我都会先用可视化工具看一眼点云的直方图分布。这样设定范围时心里有数,不会盲目乱设。你也不妨试试。