4. 直通滤波:原理讲解、PassThrough滤波器参数设置、代码实战与效果对比

各位同学,今天我们来聊聊点云滤波里最基础、也最常用的一招——直通滤波。

说实话,我刚入行那会儿,拿到一坨点云数据,第一反应就是“这玩意儿怎么这么多噪点?”后来带我的老工程师跟我说了一句话,我记到现在:“先切一刀,再看别的。” 这一刀,就是直通滤波。

4.1 直通滤波的原理:说白了就是“切一刀”

直通滤波的原理非常简单。你想象一下,你面前有一堆散落的点,每个点都有三维坐标 (x, y, z)。直通滤波就是让你指定一个轴(比如 X 轴),然后设定一个范围(比如 0 到 10 米)。在这个范围之外的点,全部干掉。

为什么会这样?因为很多场景下,我们只关心某个区域内的物体。比如自动驾驶里,车顶的激光雷达扫到的点云,可能包含了 100 米外的建筑物。但如果你只关心前方 30 米内的障碍物,那 100 米外的点就是干扰。直通滤波就是帮你把不关心的区域“切掉”。

我个人习惯把直通滤波叫做“空间裁剪器”。它不关心点的颜色、强度,只看坐标。所以它特别快,特别适合做预处理的第一步。

核心公式(其实不算公式):

对于点 P(x, y, z),如果 x 在 [x_min, x_max] 范围内,则保留;否则剔除。

同理可对 y 轴、z 轴操作。可以单轴,也可以多轴组合。

我在项目中遇到过一个问题:一个客户给的激光雷达数据,地面点特别多,导致后续的聚类算法直接崩溃。我当时就是先用直通滤波沿着 Z 轴切了一刀,把地面以下和车顶以上的点全滤掉,瞬间数据量少了 40%,聚类也正常了。嗯,这就是直通滤波的威力。

4.2 PassThrough 滤波器参数设置

在 PCL(Point Cloud Library)里,直通滤波对应的就是 pcl::PassThrough 滤波器。它的参数不多,但每个都很关键。我给大家列个表:

参数名 类型 说明 我的建议
setFilterFieldName() string 指定要过滤的轴,如 "x", "y", "z" 一般先滤 Z 轴(地面),再滤 X 轴(前方)
setFilterLimits() float, float 设置范围 [min, max],闭区间 注意单位是米,别搞成毫米了
setFilterLimitsNegative() bool 是否取反。true 表示保留范围外的点 偶尔用,比如你想保留“除了某个区域之外”的点
setInputCloud() PointCloud::Ptr 输入点云 记得用共享指针
filter() PointCloud::Ptr 执行滤波,输出结果 输出点云要提前分配内存

小技巧: 如果你不确定范围设多少合适,可以先打印点云在对应轴上的最大最小值。用 pcl::getMinMax3D() 这个函数,看一眼数据分布,再定范围。我曾经因为手抖把 Z 轴范围设成 0.0 到 0.1,结果把整片地面都滤掉了,找了半天 bug……

4.3 代码实战:手把手写一个直通滤波

好了,理论说完了,咱们直接上代码。我习惯用 C++ 写 PCL,因为效率高。下面这个例子,我会加载一个点云文件,然后沿着 X 轴和 Z 轴分别做一次直通滤波。

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

int main() {
    // 1. 加载点云
    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;

    // 2. 创建直通滤波器对象
    pcl::PassThrough<pcl::PointXYZ> pass;
    pass.setInputCloud(cloud);

    // 3. 设置过滤轴为 X 轴,范围 0.0 到 5.0 米
    pass.setFilterFieldName("x");
    pass.setFilterLimits(0.0, 5.0);
    // 注意:这里不取反,所以保留范围内的点

    // 4. 执行第一次滤波
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered_x(new pcl::PointCloud<pcl::PointXYZ>);
    pass.filter(*cloud_filtered_x);
    std::cout << "X轴滤波后点数: " << cloud_filtered_x->points.size() << std::endl;

    // 5. 再对 Z 轴做一次滤波,滤掉地面(假设地面在 Z=0 附近)
    pass.setInputCloud(cloud_filtered_x);
    pass.setFilterFieldName("z");
    pass.setFilterLimits(0.2, 2.0);  // 保留 0.2 米到 2.0 米之间的点
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered_z(new pcl::PointCloud<pcl::PointXYZ>);
    pass.filter(*cloud_filtered_z);
    std::cout << "Z轴滤波后点数: " << cloud_filtered_z->points.size() << std::endl;

    // 6. 可视化对比
    pcl::visualization::PCLVisualizer viewer("直通滤波效果对比");
    int v1(0), v2(1);
    viewer.createViewPort(0.0, 0.0, 0.5, 1.0, v1);
    viewer.createViewPort(0.5, 0.0, 1.0, 1.0, v2);

    // 原始点云用白色显示
    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cloud_color(cloud, 255, 255, 255);
    viewer.addPointCloud(cloud, cloud_color, "cloud_original", v1);

    // 滤波后点云用绿色显示
    pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> filtered_color(cloud_filtered_z, 0, 255, 0);
    viewer.addPointCloud(cloud_filtered_z, filtered_color, "cloud_filtered", v2);

    viewer.spin();
    return 0;
}

注意: 代码里我用了两次 setInputCloud(),第一次传入原始点云,第二次传入第一次滤波后的结果。这是链式滤波的常见写法。但要注意,每次 filter() 之后,输出点云是全新的,不会影响输入。所以你可以放心地串起来用。

4.4 效果对比:眼见为实

咱们来看看效果。假设原始点云是一个室内的场景,有墙壁、地面、桌子和一些杂物。原始点云大概有 30 万个点。

  • 原始点云: 点很密,但包含了很多远处的墙壁和天花板上的点,这些点对后续处理没什么用。
  • X 轴滤波后: 只保留了前方 0 到 5 米的点。远处的墙壁被切掉了,点云数量降到 15 万左右。
  • Z 轴滤波后: 进一步滤掉了地面(Z < 0.2 米)和天花板(Z > 2.0 米)。剩下的点主要集中在桌子和人形目标上,数量大约 8 万。

你想想看,从 30 万降到 8 万,数据量减少了 70% 以上。后续的聚类、分割算法跑起来,速度能快不少。而且因为去掉了干扰,准确率也会提升。

我记得有一次做室内机器人导航,激光雷达扫到的点云里包含了隔壁房间的墙。机器人死活找不到路,就是因为直通滤波的范围没设好。后来我把 X 轴范围限制在 3 米以内,机器人瞬间就清醒了。所以说,滤波参数调得好,算法下班早。

4.5 知识体系总览

下面这张图,是我自己画的直通滤波知识结构图。你可以把它当作一个快速复习的索引。

直通滤波 原理:沿指定轴裁剪点云 关键参数:轴、范围、取反标志 代码流程:加载→设置→滤波→输出 效果:数据量减少70%+,干扰剔除 直通滤波知识体系总览

好了,直通滤波的内容就讲到这里。它虽然简单,但真的很实用。下次你拿到一坨点云,别急着上高大上的算法,先切一刀试试。

可可精品资料库