【PCL自学】: .pcd格式文件和I/O模块介绍及示例代码
一、初识PCD格式点云
1、PCD格式背景
这一章节描述了PCD(点云数据)文件格式,以及它在点云库(PCL)中使用的方式。
为什么要定义一个后缀为.pcd的文件呢?
PCD文件格式并不是为了另头换面,而是为了补充现有的文件格式,因为这样或那样的原因,这些文件格式不支持PCL为n维点云处理带来的一些扩展。
PCD不是第一个支持3D点云数据的文件类型。特别是计算机图形学和计算几何社区,已经创建了许多格式来描述使用激光扫描仪获得的任意多边形和点云。这些格式包括:
PLY - 一个多边形文件格式,由斯坦福大学的Turk等人开发
STL - 由3D系统创建的立体光刻CAD软件的一种文件格式
OBJ - 一种几何定义文件格式,最初由波前技术公司开发
X3D - ISO标准的基于xml的文件格式,用于表示三维计算机图形数据
以上所有的文件格式都有一些缺点,因为在今天的传感技术和算法发明之前,它们是在很早以前出于不同的目的和不同的时间而创建的,没有统一性。
2、PCD的版本问题
在点云库(PCL) 1.0版本发布之前,PCD文件格式可能有不同的修订号。它们用PCD_Vx(例如,PCD_V5、PCD_V6、PCD_V7等)编号。
PCL中PCD文件格式的官方入口应该是0.7版(PCD_V7)。
3、文件格式详解
每个PCD文件都包含一个头,用于标识和声明存储在文件中的点云数据的某些属性。PCD的头必须用ASCII编码。
从0.7版本开始,PCD报头包含以下条目:
①VERSION - PCD文件的版本号
②FIELDS - 指定点可以拥有的每个维度/字段的名称。
FIELDS x y z # XYZ data
FIELDS x y z rgb # XYZ + colors
FIELDS x y z normal_x normal_y normal_z # XYZ + surface normals
FIELDS j1 j2 j3 # moment invariants
③SIZE - 指定每个维度的大小(以字节为单位).
④TYPE - 将每个维度的类型指定为一个字符。当前接受的类型是:
I - represents signed types int8 (char), int16 (short), and int32 (int)
U - represents unsigned types uint8 (unsigned char), uint16 (unsigned short), uint32 (unsigned int)
F - represents float type
⑤COUNT - 指定每个维度有多少个元素。例如,x数据通常有一个元素,但像VFH这样的特征描述符有308个元素。基本上,这是一种在每个点引入n-维直方图描述符的方法,并将它们视为单个连续的内存块。默认情况下,如果COUNT不存在,则所有维度计数都设置为1.
⑥WIDTH -指定点云数据集的宽度,以点的数量表示。
WIDTH有两个含义:
1.它可以为无序的数据集指定云中的点总数(与下面的points相同)
2.它也可以是指定有序的点云数据集的宽度(行中点的总数)。
⑦HEIGHT - 指定点云数据集的高度(以点的数量表示)。
HEIGHT也有两个含义:
1.它可以指定一个有序的点云数据集的高度(总行数);
2.对于无序的数据集,它被设置为1(因此也可以用于检查数据集是否有序)。
⑧VIEWPOINT -为数据集中的点指定一个获取视点。这可能稍后用于构建不同坐标系统之间的转换,或用于帮助创建需要一致方向的曲面法线等特征。视点信息被指定为平移(tx ty tz) +四元数(qw qx qy qz)。默认值为 VIEWPOINT 0 0 0 1 0 0 0
⑨POINTS - 指定云中的点总数。在0.7版本后,它的用途有点多余。
⑩DATA - 指定存储点云数据的数据类型。从0.7版本开始,支持三种数据类型:ascii、binary和binary_compressed。
在文件头中的定义顺序必须按照以上顺序严格定义,否则会读取错误。
文件格式示例如下:
VERSION .7
FIELDS x y z rgb
SIZE 4 4 4 4
TYPE F F F F
COUNT 1 1 1 1
WIDTH 213
HEIGHT 1
VIEWPOINT 0 0 0 1 0 0 0
POINTS 7
DATA ascii
0.93773 0.33763 0 4.2108e+06
0.90805 0.35641 0 4.2108e+06
0.81915 0.32 0 4.2108e+06
0.97192 0.278 0 4.2108e+06
0.944 0.29474 0 4.2108e+06
0.98111 0.24247 0 4.2108e+06
0.93655 0.26143 0 4.2108e+06
二、读入和写出PCD格式点云文件
1.从pcd格式文件中读入点云
上一节我们介绍了PCD格式文件可以存储点云数据,那我们如何使用PCL去从存储器中读取这个文件呢?
请看如下程序示例:
#include <iostream>
#include <pcl/io/pcd_io.h> // IO模块
#include <pcl/point_types.h> // 点类型
int main ()
{
// [1]首先我们使用以下语句创建一个指向pcl::PointXYZ类型的共享指针cloud,此处pcl::PointXYZ类型指的是只有XYZ三个维度位置信息的点云类型。
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
// [2]从磁盘加载PointCloud数据(例如文件名为test_pcd.pcd的文件),未成功读取则返回-1
if (pcl::io::loadPCDFile<pcl::PointXYZ> ("test_pcd.pcd", *cloud) == -1) //* load the file
{
PCL_ERROR ("Couldn't read file test_pcd.pcd \n");
return (-1);
}
// [3]打印pcd文件中点云的数据量(宽度*高度);
std::cout << "Loaded "
<< cloud->width * cloud->height
<< " data points from test_pcd.pcd with the following fields: "
<< std::endl;
// [4]打印出文件中点的位置信息,以下方式常用,需熟练使用。
for (const auto& point: *cloud)
std::cout << " " << point.x
<< " " << point.y
<< " " << point.z << std::endl;
return (0);
}
2.向pcd格式文件中写入点云
当我们学会如何读取点云了,那么我们对点云进行一系列操作后,想要将处理后的点云写入pcd文件又该如何操作呢?
参考如下示例代码:
#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
int main ()
{
// 创建点云的基本参数,以下指令描述了我们将要创建的模板化的PointCloud结构。每个点的类型都设置为pcl::PointXYZ,点云宽度为5,高度为1。
pcl::PointCloud<pcl::PointXYZ> cloud;
cloud.width = 5;
cloud.height = 1;
cloud.is_dense = false;
cloud.resize (cloud.width * cloud.height);
// 用随机生成的点向点云模板结构中填入数据
for (auto& point: cloud)
{
point.x = 1024 * rand () / (RAND_MAX + 1.0f);
point.y = 1024 * rand () / (RAND_MAX + 1.0f);
point.z = 1024 * rand () / (RAND_MAX + 1.0f);
}
// 将点云以ASCII码形式保存成PCD格式文件
pcl::io::savePCDFileASCII ("test_pcd.pcd", cloud);
std::cerr << "Saved " << cloud.size () << " data points to test_pcd.pcd." << std::endl;
// 打印点云
for (const auto& point: cloud)
std::cerr << " " << point.x << " " << point.y << " " << point.z << std::endl;
return (0);
}
三、合并两片点云
在某些情况下,我们需要将两个pcd文件合并,本节将学习如何合并两个不同的点云文件的点。合并点云的前提是两个数据集中点云的类型必须相等。我们还将学习如何连接两个不同点云的维度。这里施加的约束是,两个数据集中的点数必须相等。
请参考以下示例代码学习:
#include <iostream>
#include <pcl/point_types.h>
#include <pcl/common/io.h> // for concatenateFields
int main (int argc, char** argv)
{
if (argc != 2)
{
std::cerr << "please specify command line arg '-f' or '-p'" << std::endl;
exit(0);
}
// ********************第一部分****************
pcl::PointCloud<pcl::PointXYZ> cloud_a, cloud_b, cloud_c;
pcl::PointCloud<pcl::Normal> n_cloud_b;
pcl::PointCloud<pcl::PointNormal> p_n_cloud_c;
// Fill in the cloud data
cloud_a.width = 5;
cloud_a.height = cloud_b.height = n_cloud_b.height = 1;
cloud_a.resize (cloud_a.width * cloud_a.height);
if (strcmp(argv[1], "-p") == 0)
{
cloud_b.width = 3;
cloud_b.resize (cloud_b.width * cloud_b.height);
}
else{
n_cloud_b.width = 5;
n_cloud_b.resize (n_cloud_b.width * n_cloud_b.height);
}
for (std::size_t i = 0; i < cloud_a.size (); ++i)
{
cloud_a[i].x = 1024 * rand () / (RAND_MAX + 1.0f);
cloud_a[i].y = 1024 * rand () / (RAND_MAX + 1.0f);
cloud_a[i].z = 1024 * rand () / (RAND_MAX + 1.0f);
}
if (strcmp(argv[1], "-p") == 0)
for (std::size_t i = 0; i < cloud_b.size (); ++i)
{
cloud_b[i].x = 1024 * rand () / (RAND_MAX + 1.0f);
cloud_b[i].y = 1024 * rand () / (RAND_MAX + 1.0f);
cloud_b[i].z = 1024 * rand () / (RAND_MAX + 1.0f);
}
else
for (std::size_t i = 0; i < n_cloud_b.size (); ++i)
{
n_cloud_b[i].normal[0] = 1024 * rand () / (RAND_MAX + 1.0f);
n_cloud_b[i].normal[1] = 1024 * rand () / (RAND_MAX + 1.0f);
n_cloud_b[i].normal[2] = 1024 * rand () / (RAND_MAX + 1.0f);
}
std::cerr << "Cloud A: " << std::endl;
for (std::size_t i = 0; i < cloud_a.size (); ++i)
std::cerr << " " << cloud_a[i].x << " " << cloud_a[i].y << " " << cloud_a[i].z << std::endl;
std::cerr << "Cloud B: " << std::endl;
if (strcmp(argv[1], "-p") == 0)
for (std::size_t i = 0; i < cloud_b.size (); ++i)
std::cerr << " " << cloud_b[i].x << " " << cloud_b[i].y << " " << cloud_b[i].z << std::endl;
else
for (std::size_t i = 0; i < n_cloud_b.size (); ++i)
std::cerr << " " << n_cloud_b[i].normal[0] << " " << n_cloud_b[i].normal[1] << " " << n_cloud_b[i].normal[2] << std::endl;
// ********************第二部分****************
// Copy the point cloud data
if (strcmp(argv[1], "-p") == 0)
{
cloud_c = cloud_a;
cloud_c += cloud_b;
std::cerr << "Cloud C: " << std::endl;
for (std::size_t i = 0; i < cloud_c.size (); ++i)
std::cerr << " " << cloud_c[i].x << " " << cloud_c[i].y << " " << cloud_c[i].z << " " << std::endl;
}
else
{
pcl::concatenateFields (cloud_a, n_cloud_b, p_n_cloud_c);
std::cerr << "Cloud C: " << std::endl;
for (std::size_t i = 0; i < p_n_cloud_c.size (); ++i)
std::cerr << " " <<
p_n_cloud_c[i].x << " " << p_n_cloud_c[i].y << " " << p_n_cloud_c[i].z << " " <<
p_n_cloud_c[i].normal[0] << " " << p_n_cloud_c[i].normal[1] << " " << p_n_cloud_c[i].normal[2] << std::endl;
}
return (0);
}
首先来看第一部分,定义了用于连接云的五个点云:三个输入点云(cloud_a, cloud_b和n_cloud_b),两个输出(cloud_c和p_n_cloud_c)。然后,我们为我们正在使用的两个输入点云填写数据(对于cloud_a和cloud_b填入位置数据,对于n_cloud_b填入法向数据),并打印出创建好的数据。
其次来看第二部分,通过指令
cloud_c = cloud_a;
cloud_c += cloud_b;
将cloud_a和cloud_b的点连接在一起来构造cloud_c。
如果想要链接两块不同维度,例如给只有位置信息的cloud_a,增加法向信息n_cloud_b,也即将cloud_a和n_cloud_b的字段连接在一起,创建p_n_cloud_c。参考如下指令:
pcl::concatenateFields (cloud_a, n_cloud_b, p_n_cloud_c);
四、PCL中各类点云抓取器框架
1、PCL中的OpenNI抓取器(用于硬件通信)
从PCL 1.0开始,提供了一个新的通用抓取器接口,以提供对不同设备及其驱动程序、文件格式和其他数据源的访问,通用性极强。
PCL库合并的第一个驱动程序是OpenNI Grabber,它极大方便的从任何兼容OpenNI的相机(例如XBOX360的kinect深度相机)请求数据流。本节介绍了如何设置和使用OpenNI抓取器。
示例:
下面是PCL OpenNI Viewer的截图和视频,它使用了OpenNI抓取器。
让我们来看看示例程序的代码。visualization/tools/openni_viewer_simple.cpp
#include <pcl/io/openni_grabber.h>
#include <pcl/visualization/cloud_viewer.h>
class SimpleOpenNIViewer
{
public:
SimpleOpenNIViewer () : viewer ("PCL OpenNI Viewer") {}
void cloud_cb_ (const pcl::PointCloud<pcl::PointXYZ>::ConstPtr &cloud)
{
if (!viewer.wasStopped())
viewer.showCloud (cloud);
}
void run ()
{
//pcl::Grabber* interface = new pcl::OpenNIGrabber(); // 老版本OpenNI
pcl::Grabber* interface = new pcl::io::OpenNI2Grabber();// 新版本OpenNI2
// 老版本回调方式
std::function<void (const pcl::PointCloud<pcl::PointXYZ>::ConstPtr&)> f =
[this] (const pcl::PointCloud<pcl::PointXYZ>::ConstPtr& cloud) { cloud_cb_ (cloud); };
// 新版本回调方式
boost::function<void (const pcl::PointCloud<pcl::PointXYZ>::ConstPtr&)> f =
boost::bind (&SimpleOpenNIViewer::cloud_cb_, this, _1);
interface->registerCallback (f);
interface->start ();
while (!viewer.wasStopped())
{
boost::this_thread::sleep (boost::posix_time::seconds (1));
}
interface->stop ();
}
pcl::visualization::CloudViewer viewer;
};
int main ()
{
SimpleOpenNIViewer v;
v.run ();
return 0;
}
如上所示,SimpleOpenNIViewer的run()函数首先创建一个新的OpenNIGrabber接口。下一行看起来有点难以理解。首先创建一个调用cloud_cb_的lambda对象,我们捕获该对象的一个副本以获得一个指向SimpleOpenNIViewer的指针,以便可以调用cloud_cb_。
lambda然后被强制转换为一个std::function对象,该对象是在回调函数类型上模板化的,在本例中是void (const pcl::PointCloud<pcl::PointXYZ>::ConstPtr&)。得到的函数对象可以注册到OpenNIGrabber并随后启动。注意,stop()方法不一定需要被调用,因为析构函数会处理这个问题。
2、Velodyne高清晰度激光雷达(HDL)抓取器
待添加
3、PCL Dinast抓取器框架
待添加
4、从Ensenso相机捕捉点云
待添加
5、从davidSDK扫描仪抓取点云/网格
待添加
6、从DepthSense相机捕捉点云
待添加