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 知识体系总览
下面这张图,是我自己画的直通滤波知识结构图。你可以把它当作一个快速复习的索引。
好了,直通滤波的内容就讲到这里。它虽然简单,但真的很实用。下次你拿到一坨点云,别急着上高大上的算法,先切一刀试试。