3. PCL基础
好,咱们正式开始接触PCL。说实话,很多新手一上来就急着调算法,结果连点云数据长什么样都没搞清楚。我当年也是这样,拿着别人的代码跑通了一个滤波,换了自己的数据就崩了——后来才发现,是点云的数据结构没搞明白。
这一节,咱们就把地基打牢。说白了,PCL里的一切操作,都围绕着 pcl::PointCloud 这个核心数据结构展开。你把它玩熟了,后面什么滤波、分割、配准,都是水到渠成的事。
3.1 PointCloud数据结构:点云的本质
点云是什么?一堆三维点的集合。但PCL里的点云,远不止「一堆点」这么简单。
我习惯把 pcl::PointCloud 想象成一个「智能数组」。它里面装着点,还带着一堆元数据。咱们来看看它的核心成员:
| 成员变量 | 类型 | 说明 |
|---|---|---|
width |
uint32_t |
点云的宽度。如果是无序点云,就是点的总数;有序点云(比如深度图转的)就是每行点数。 |
height |
uint32_t |
点云的高度。无序点云为1;有序点云就是行数。 |
points |
std::vector<PointT> |
核心!所有点都存在这个vector里。你可以像操作普通vector一样遍历它。 |
is_dense |
bool |
标记点云是否包含无效点(NaN或Inf)。true 表示所有点都有效。 |
sensor_origin_ |
Eigen::Vector4f |
传感器采集时的位置(平移向量)。 |
sensor_orientation_ |
Eigen::Quaternionf |
传感器采集时的朝向(四元数)。 |
嗯,这里要注意 width 和 height。我见过有人把有序点云当无序处理,结果滤波出来的结果完全不对。你想想看,如果一帧64线的激光雷达数据,你把它当成无序点云去搞半径滤波,那邻域搜索的效率会低很多。
再说说 PointT。PCL预定义了很多点类型,最常用的就是 pcl::PointXYZ,它只包含 x、y、z 三个坐标。但实际项目中,我们往往需要更多信息:
- pcl::PointXYZI:带了反射强度
intensity。做地面分割时,强度信息很有用。 - pcl::PointXYZRGB:带了颜色信息。视觉和激光融合时常用。
- pcl::PointXYZRGBNormal:带了颜色和法向量。做特征提取时一步到位。
我个人习惯,在项目初期先用 PointXYZ 把流程跑通,再根据需要升级点类型。别一上来就搞个带法向量的,数据量大了,内存吃不消。
核心要点:cloud->points[i] 就是第 i 个点。遍历点云,最直接的方式就是 for (auto &p : cloud->points)。记住,点云的本质就是一个vector,只不过它知道自己是怎么来的。
3.2 PCD文件读写:你的数据入口和出口
PCD是PCL的原生格式。为什么不用PLY或LAS?PCD支持多种点类型,读写速度快,而且能保留传感器信息。我在项目中经常用PCD做中间结果的缓存,调试起来特别方便。
3.2.1 写PCD文件
写文件很简单,核心就一个函数 pcl::io::savePCDFile。但有几个坑,我帮你踩过了:
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
// 创建一个点云对象
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 填充点云数据
cloud->width = 5;
cloud->height = 1; // 无序点云
cloud->is_dense = false;
cloud->points.resize(cloud->width * cloud->height);
for (size_t i = 0; i < cloud->points.size(); ++i) {
cloud->points[i].x = 1024 * rand() / (RAND_MAX + 1.0f);
cloud->points[i].y = 1024 * rand() / (RAND_MAX + 1.0f);
cloud->points[i].z = 1024 * rand() / (RAND_MAX + 1.0f);
}
// 保存为PCD文件
pcl::io::savePCDFileASCII("test_pcd.pcd", *cloud);
std::cout << "Saved " << cloud->points.size() << " data points to test_pcd.pcd." << std::endl;
这里我用了 savePCDFileASCII,生成的是人类可读的文本格式。调试阶段用这个,方便查看。但如果你要保存几十万甚至上百万的点,记得用 savePCDFileBinary,文件体积小,读写速度快一个数量级。
我的经验:调试时用ASCII格式,部署时用二进制格式。我曾经在调试一个地面分割算法时,每次保存都写二进制,结果想看看中间结果,还得写个转换脚本。后来学乖了,调试阶段一律用ASCII。
3.2.2 读PCD文件
读文件同样简单,但有个细节很多人会忽略:
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
if (pcl::io::loadPCDFile<pcl::PointXYZ>("test_pcd.pcd", *cloud) == -1) {
PCL_ERROR("Couldn't read file test_pcd.pcd \n");
return (-1);
}
std::cout << "Loaded " << cloud->width * cloud->height
<< " data points from test_pcd.pcd with the following fields: "
<< pcl::getFieldsList(*cloud) << std::endl;
注意那个 pcl::getFieldsList,它能告诉你这个PCD文件里到底存了哪些字段。我遇到过一种情况:同事给我的PCD文件,他以为是 PointXYZ,结果里面其实带了 intensity。我用 PointXYZ 去读,数据就乱了。所以,养成好习惯,读文件后先检查字段列表。
避坑指南:我曾经在读取一个从ROS bag转换来的PCD文件时,程序直接崩溃。后来发现,那个文件里包含了NaN点。PCL的 loadPCDFile 不会自动过滤NaN,你需要自己处理。建议读取后调用 cloud->is_dense 检查一下,如果为false,就用 pcl::removeNaNFromPointCloud 清理一下。
3.3 可视化工具pcl_viewer:调试利器
写代码看不到点云,就像闭着眼睛开车。pcl_viewer 是PCL自带的命令行可视化工具,轻量、快速、无需写代码。
3.3.1 基本用法
在终端里直接敲:
pcl_viewer test_pcd.pcd
窗口就弹出来了。你可以用鼠标拖拽旋转,滚轮缩放。按 r 键重置视角,按 q 键退出。
我个人最常用的快捷键:
| 快捷键 | 功能 |
|---|---|
r |
重置视角到初始位置 |
+ / - |
增大/减小点的大小 |
g |
切换背景色(黑/白) |
s |
切换显示模式(点/网格/表面) |
Ctrl + 左键 |
测量两点之间的距离 |
3.3.2 同时查看多个点云
调试时经常需要对比原始数据和滤波后的结果。pcl_viewer 支持同时加载多个文件:
pcl_viewer original.pcd filtered.pcd
默认情况下,两个点云会叠加显示,颜色不同。你可以按 1、2 键分别控制第一个和第二个点云的显隐。这个功能在调试滤波参数时特别有用——左边看原始,右边看结果,一目了然。
3.3.3 进阶技巧
pcl_viewer 还支持从标准输入读取点云数据。你可以把其他程序的处理结果直接管道进来:
cat output.pcd | pcl_viewer
嗯,这个技巧在写脚本批量处理时很实用。我经常写一个shell脚本,循环处理几十个PCD文件,每个都用pcl_viewer看一眼,确认没问题再继续。
核心要点:pcl_viewer 不是玩具,它是你调试SLAM和点云处理算法的第一道防线。我每次写完一个滤波模块,第一件事就是用pcl_viewer看效果。肉眼能发现的问题,比跑100个量化指标都来得快。
3.4 本章知识体系
为了让你更直观地理解这三块内容的关系,我画了一张图:
这张图把三者的关系说得很清楚了。数据结构是骨架,文件读写是血脉,可视化是眼睛。三者缺一不可。
好了,这一节的内容就到这里。你先把这些基础操作练熟,后面咱们开始玩真正的滤波算法。