2. 点云基础与预处理

大家好,我是你们这堂课的主讲。今天咱们聊聊点云预处理。说实话,我刚入行那会儿,觉得点云不就是一堆三维点嘛,直接扔进网络里训练不就完了?结果被现实狠狠教育了一顿——原始点云数据,那叫一个脏乱差。

你想想看,激光雷达扫一圈下来,少说几万到几十万个点。这里面有地面、有车辆、有行人,还有各种噪声点、离群点。直接处理?算力扛不住,精度也上不去。所以,预处理这一步,说白了就是给数据「洗个澡」,让它变得干净、规整、好用。

点云数据结构:理解你的数据长什么样

先说说点云的数据结构。最常见的格式就是 PCD(Point Cloud Data)和 PLY(Polygon File Format)。我个人习惯用 PCD,因为 ROS 生态里支持得最好。

一个点云文件,本质上就是一个 N×D 的矩阵。N 是点的数量,D 是每个点的维度。最基本的维度是 x、y、z 三个坐标。但实际项目中,我们还会用到强度(intensity)、时间戳、甚至 RGB 颜色信息。

核心概念:点云不是图像,它没有规则的网格结构。每个点都是独立存在的,点与点之间没有固定的邻居关系。这一点非常重要,后面所有的滤波算法都基于这个特性。

举个例子,一个典型的 Velodyne HDL-64E 激光雷达,每秒能产生约 130 万个点。每个点包含:

字段 说明 典型范围
x, y, z 三维坐标(米) -100 ~ 100
intensity 反射强度(0-255) 0 ~ 255
ring 激光线束编号 0 ~ 63
timestamp 相对时间戳(微秒) 0 ~ 1e6

嗯,这里要注意:不同雷达的数据格式差异很大。我曾经在项目里遇到过,把 Ouster 的数据当成 Velodyne 来解析,结果点云全飞到天上去了。所以拿到数据第一步,先确认传感器型号和数据结构。

体素滤波:给点云「降采样」

体素滤波(Voxel Grid Filter)是我用得最多的预处理手段。它的思路很简单:把三维空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点)。

为什么要这么做?说白了,就是降采样。原始点云太密了,尤其是近处的点,密密麻麻全是。直接处理的话,计算量爆炸。体素滤波能有效减少点数,同时保留点云的几何结构。

// C++ 示例:PCL 中的体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);  // 体素大小 10cm
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>());
voxel_filter.filter(*filtered_cloud);

我的经验:体素大小的选择很关键。设得太小(比如 0.01m),降采样效果不明显;设得太大(比如 0.5m),会丢失细节。我一般从 0.1m 开始试,根据实际场景调整。高速场景可以大一点,低速场景小一点。

你可能会问:体素滤波会不会把障碍物弄没了?会的。如果体素太大,小物体(比如行人、锥桶)可能直接被「吞掉」。所以降采样和保真度之间,需要做个权衡。

直通滤波:快速裁剪感兴趣区域

直通滤波(PassThrough Filter)更简单——直接根据坐标轴范围裁剪点云。比如我只想看前方 50 米内的障碍物,那就把 x 轴范围设在 0 到 50 米。

这个操作在工程中非常实用。你想啊,激光雷达能扫 100 多米,但远处的点稀疏且噪声大,对检测没什么帮助。直接切掉,既减少数据量,又避免远处噪声干扰。

// C++ 示例:PCL 中的直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");        // 沿 z 轴(高度)滤波
pass.setFilterLimits(-1.0, 2.0);     // 保留 -1m 到 2m 之间的点
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>());
pass.filter(*filtered_cloud);

避坑指南:我曾经在高速场景中,把直通滤波的 x 轴范围设得太小(只保留 0-30 米),结果前方 50 米处的大货车完全没检测到,差点出事。记住:直通滤波是「裁剪」,不是「优化」。裁剪范围要根据实际感知需求来定,不能一刀切。

统计滤波:干掉离群点

统计滤波(Statistical Outlier Removal)是用来去除噪声点的。它的原理是:计算每个点与其 k 个最近邻的平均距离,如果这个距离超过某个阈值,就认为是离群点。

为什么会需要这个?因为激光雷达在边缘处、或者遇到雨雾天气时,会产生一些「飘」在空中的孤立点。这些点对障碍物检测是纯干扰。

// C++ 示例:PCL 中的统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);              // 考虑 50 个最近邻
sor.setStddevMulThresh(1.0);   // 标准差倍数阈值
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>());
sor.filter(*filtered_cloud);

关键参数:

  • meanK:邻居数量。越大,滤波越平滑,但可能误删真实点。
  • stddevMulThresh:标准差倍数。越小,去除的点越多。我一般用 1.0 到 2.0 之间。

我记得有一次做矿区无人驾驶项目,点云里全是扬尘产生的噪声点。统计滤波设了 meanK=30、stddevMulThresh=1.5,效果出奇的好。但要注意,如果场景本身点云就很稀疏(比如远距离),统计滤波可能会把真实物体也干掉。

点云可视化:眼见为实

预处理做得对不对,光看数字没用。你得把点云画出来看看。我常用的可视化工具是 PCL Visualizer 和 Open3D。

PCL 自带的可视化器虽然丑了点,但胜在轻量。Open3D 的界面更现代,交互也更友好。

// C++ 示例:PCL 可视化
pcl::visualization::PCLVisualizer viewer("Point Cloud Viewer");
viewer.addPointCloud(filtered_cloud, "cloud");
viewer.setBackgroundColor(0, 0, 0);  // 黑色背景
viewer.spin();

我的习惯:可视化时,我会把原始点云和滤波后的点云放在两个窗口对比。这样一眼就能看出滤波效果。另外,给点云上色也很重要——按高度上色(z 轴)能直观看出地面和障碍物的分布。

你想想看,如果预处理做得好,可视化出来的点云应该是:地面平整、障碍物轮廓清晰、没有飘在空中的孤立点。如果看到点云里有一团一团的「毛刺」,那大概率是滤波参数没调好。

本章知识体系

下面这张图,是我自己总结的点云预处理流程。你可以把它当作一个检查清单:

点云预处理流程 原始点云 直通滤波 体素滤波 统计滤波 预处理后点云 裁剪 ROI 区域 降采样,减少点数 去除离群噪声点 输出干净、规整的点云

这张图展示了点云预处理的典型流水线。实际项目中,顺序可以调整。比如先体素滤波再直通滤波,或者先统计滤波再体素滤波,效果差别不大。但有一点要注意:直通滤波最好放在最前面,因为先裁剪掉无关区域,后续滤波的计算量会小很多。

好了,关于点云基础与预处理,我就讲这么多。记住一句话:预处理做得好,后续算法事半功倍;预处理做得糙,后面全是坑。希望你们在实际项目中,能把这些方法用起来,调出最适合自己场景的参数。

可可精品资料库