点云库PCL从入门到精通
上QQ阅读APP看书,第一时间看更新

3.2 IO入门级实例解析

3.2.1 PCD文件格式

本小节描述PCD(点云数据)文件格式及其在点云库(PCL)中应用的方法。

为什么用一种新的文件格式

PCD文件格式并非白费力气地做重复工作,现有的文件结构因本身组成的原因不支持由PCL库引进n维点类型机制处理过程中的某些扩展,而PCD文件格式能够很好地补足这一点。PCD不是第一个支持3D点云数据的文件类型,尤其是计算机图形学和计算几何学领域,已经创建了很多格式来描述任意多边形和激光扫描仪获取的点云,包括下面几种格式。

• PLY是一种多边形文件格式,由Stanford大学的Turk等人设计开发。

• STL是3D Systems公司创建的模型文件格式,主要应用于CAD、CAM领域。

• OBJ是从几何学上定义的文件格式,首先由Wavefront Technologies公司开发。

• X3D是符合ISO标准的基于XML的文件格式,用于表示3D计算机图形数据。

以上所有的文件格式都有缺点,在下一节会讲到。这是很自然的,因为它们是在不同时间为了不同的使用目的所创建的,那时新的传感器技术和算法都还没有发明出来。

PCD版本

在点云库(PCL)1.0版本发布之前,PCD 文件格式有不同的修订号。这些修订号用PCD_Vx来编号(例如,PCD_V5、PCD_V6、PCD_V7等),代表PCD 文件的0.x版本号。然而PCL中正式发布的PCD文件格式是0.7版本(PCD_V7)。

文件头格式

每一个PCD文件都包含一个文件头,它确定和声明文件中存储的点云数据的某种特性。PCD文件头必须用ASCII码来编码。PCD文件中指定的每一个文件头字段以及ASCII点数据都用一个新行(\n)分开了,从0.7版本开始,PCD文件头包含下面的这些字段。

• VERSION:指定PCD文件版本。

• FIELDS:指定一个点可以有的每一个维度和字段的名字。例如:

• SIZE:用字节数指定每一个维度的大小。例如:

• TYPE:用一个字符指定每一个维度的类型。现在被接受的类型有:

I表示有符号类型int8(char)、int16(short)和int32(int);

U表示无符号类型uint8(unsigned char)、uint16(unsigned short)和uint32(unsigned int);

F表示浮点类型。

• COUNT:指定每一个维度包含的元素数目。例如,x这个数据通常有一个元素,但是像VFH这样的特征描述子就有308个。实际上这是在给每一点引入n维直方图描述符的方法,把它们当作单个的连续存储块。默认情况下,如果没有COUNT,所有维度的数目被设置成1。

• WIDTH:用点的数量表示点云数据集的宽度。根据是有序点云还是无序点云,WIDTH有两层解释。

(1)它能确定无序数据集的点云中点的个数(和下面的POINTS一样)。

(2)它能确定有序点云数据集的宽度(一行中点的数目)。

注意:有序点云数据集,意味着点云是类似于图像(或者矩阵)的结构,数据分为行和列。这种点云的实例包括立体摄像机和时间飞行摄像机生成的数据。有序数据集的优势在于,预先了解相邻点(和像素点类似)的关系,邻域操作更加高效,这样就加速了计算并降低了PCL中某些算法的成本。

例如:

• HEIGHT:用点的数目表示点云数据集的高度。类似于WIDTH,HEIGHT也有两层解释。

(1)它表示有序点云数据集的高度(行的总数)。

(2)对于无序数据集,它被设置成1(被用来检查一个数据集是有序还是无序)。

有序点云例子:

无序点云例子:

• VIEWPOINT:指定数据集中点云的获取视点。VIEWPOINT有可能在不同坐标系之间转换的时候应用,在辅助获取其他特征时也比较有用,例如曲面法线,在判断方向一致性时,需要知道视点的方位。

视点信息被指定为平移(txtytz)+四元数(qwqxqyqz),默认值如下。

VIEWPOINT 0 0 0 1 0 0 0

• POINTS:指定点云中点的总数。从0.7版本开始,该字段就有点多余了,因此有可能在将来的版本中将它移除。

例子:

• DATA:指定存储点云数据的数据类型。从0.7版本开始,支持两种数据类型:ASCII和二进制。查看下一节可以获得更多细节。

注意:文件头最后一行(DATA)的下一个字节就被看成是点云的数据部分了,它会被解释为点云数据。

警告:PCD文件的文件头部分必须以上面的顺序精确指定,也就是按照VERSION、FIELDS、SIZE、TYPE、COUNT、WIDTH、HEIGHT、VIEWPOINT、POINTS、DATA的顺序,并且彼此之间用换行隔开。

数据存储类型

在0.7版本中,PCD文件格式用ASCII和二进制两种模式存储数据。

如果以ASCII形式,每一点占据一个新行。

注意:从PCL 1.0.1版本开始,用字符串“nan”表示NaN,此字符表示该点的值不存在或非法。

如果以二进制形式,这里数据是数组(向量)pcl::PointCloud.points的一份完整拷贝,在Linux系统上,我们用mmap/munmap操作来尽可能快地读写数据,存储点云数据可以用简单的ASCII形式,每点占据一行,用空格键或Tab键分开,没有其他任何字符。也可以用二进制存储格式,它既简单又快速,当然这依赖于用户应用。ASCII格式允许用户打开点云文件,使用例如gunplot这样的标准软件工具更改点云文件数据,或者用sed、awk等工具来对它们进行操作。

相对其他文件格式的优势

用PCD作为(另一种)文件格式可能被看成是没有必要的一项工作。但实际中,情况不是这样的,因为上面提到的文件格式无一能提高PCD文件的适用性和速度。PCD文件格式包括以下几个明显的优势。

• 存储和处理有序点云数据集的能力——这一点对于实时应用,例如增强现实、机器人学等领域十分重要。

• 二进制mmap/munmap数据类型是把数据下载和存储到磁盘上最快的方法。

• 存储不同的数据类型(支持所有的基本类型:char、short、int、float、double)——使得点云数据在存储和处理过程中适应性强并且高效,其中无效的点通常存储为NAN类型。

• 特征描述子的n维直方图——对于3D识别和计算机视觉应用十分重要。

另一个优势是通过控制文件格式,我们能够使其最大程度上适应PCL,这样能获得PCL应用程序的最好性能,而不用把一种不同的文件格式改变成PCL的内部格式,否则通过转换函数会引起额外的延时。

注意:尽管PCD(点云数据)是PCL中的内部文件格式,但pcl_io库也提供对前面提到的所有其他文件格式中保存和加载数据的支持。

例子

下面给出了PCD文件的一个片段。请读者试着解析这些数据,看看它的组成。

3.2.2 PCD文件IO操作

从PCD文件中读取点云数据

在本小节我们学习如何从PCD文件中读取点云数据。

代码

首先,在本书提供的第3章例1文件夹中,打开名为pcd_read.cpp的代码文件。同目录下可找到测试点云文件test_pcd.pcd。

解释说明

现在,我们解析上面打开的代码:

与本程序相关的头文件声明。

创建一个PointCloud<PointXYZ> boost共享指针并进行实例化。

从磁盘上加载PointCloud数据(假设test_pcd.pcd文件已经被创建了)到二进制存储块中。或者,你可以读取PointCloud2存储块(仅仅在PCL 1.x 中可用)。由于点云的动态性质,我们更愿意以二进制块来读取,然后转换成我们要使用的表示方式。

把二进制块读取并转换到模板化的PointCloud格式里,这里用pcl::PointXYZ作为点类型。

最后在标准输出上打印出从文件中加载的数据。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,并生成相应的可执行文件,生成可执行文件之后,就可以运行了。在CMD中键入以下命令。

运行之后,可看到图3-3所示的输出结果,在本例中提供的是点云文件版本0.7,所以在代码中没有涉及PointCloud2,如果用户需要读取版本较低的PCD文件,需要替换点云读取代码为上面描述的PointCloud2读取方式。

图3-3 点云读取例子运行结果

注意,如果test_pcd.pcd文件不存在(没有创建或者被删除了),将会提示如下错误信息。

向PCD文件写入点云数据

本小节我们将学习如何向PCD文件写入点云数据。

代码

首先,在本书提供的第3章例2文件夹中,打开名为pcd_write.cpp的代码文件。

解释说明

现在,我们解析上面打开的代码:

pcl/io/pcd_io.h头文件中包含了PCD输入输出操作的声明,pcl/point_types.h头文件则包含一些PointT类型结构体的声明(本例中是pcl::PointXYZ)。

描述我们将要实例化的模板类PointCloud,每一个点的类型都被设置成pcl::PointXYZ,作为模板类实例化的参数,其他类型请参考第2章点类型介绍一节,pcl::PointXYZ具体定义如下。

下面这几行用来创建点云。用随机点的值填充PointCloud点云对象,并设置适当的参数(width、height、is_dense)。

然后,把PointCloud对象数据存储在test_pcd.pcd文件中。

最后打印输出存储的点云数据。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,并生成相应的可执行文件,生成可执行文件之后,就可以运行了,在CMD中使用以下命令。

如图3-4所示,将在CMD界面看到类似下面的内容。

图3-4 点云写入例子运行结果

Linux下的用户可以方便地用下面的命令检查test_pcd.pcd文件的内容,在Window上的用户直接用一般的Word等就可以对PCD文件进行打开(只限于0.7版本的,低版本的打开会显示不完整)。

3.2.3 两个点云中的字段或数据连接

本小节中我们学习如何连接两个不同点云为一个点云,进行连接操作前要确保两个数据集中字段的类型相同和维度相等。同时也学习如何连接两个不同点云的字段(例如,颜色、法线),这种操作的强制约束条件是两个数据集中点的数目必须一样,例如,点云A是N个点的xyz点,点云B是N个点的rgb点,则连接两个字段形成的点云C是N个点的xyzrgb类型。

代码

首先,在本书提供的第3章例3文件夹中,打开名为 concatenate_clouds.cpp的代码文件。

解释说明

现在,解析上面打开的源代码。

上述代码中,我们定义了连接点云会用到的五个点云对象:三个输入(cloud_a、cloud_b和n_cloud_b),两个输出(cloud_c和p_n_cloud_c)。然后我们为两个输入点云(cloud_a和cloud_b或者cloud_a和n_cloud_b)填充数据。

然后,用下面几行代码把cloud_a和cloud_b或n_cloud_b(取决于命令行参数)的数据打印在标准输出上。

如果我们需要连接点云,则使用下面的代码,即把cloud_a和cloud_b连接在一起创建了cloud_c。

另外如果要连接字段,则使用下面的代码,即通过把cloud_a和n_cloud_b字段连接在一起创建了p_n_cloud_c。

最后使用如下代码。

或者使用如下代码。

上面两种代码中的一段用来把cloud_c或者p_n_cloud_c的内容显示在屏幕上,这取决于我们连接的是点云还是字段。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,并生成相应的可执行文件,生成可执行文件之后,就可以运行了。在CMD中键入以下命令来连接点。

或者键入以下命令来连接字段。

运行程序结果如图3-5所示,先是点云字段间连接,后面是点云连接。

图3-5 点云连接例子运行结果

3.2.4 基于OpenNI接口的点云数据获取

从PCL 1.0开始,PCL提供了一个通用采集接口,这样可以方便地连接到不同的设备及其驱动、文件格式和其他数据源。PCL集成的第一个数据获取驱动是新的OpenNI Grabber,它使得从OpenNI兼容的设备请求数据流变得十分简单。本小节展示如何设置并使用该采集卡。这里要注意的是1.8版本中,PCL不仅仅集成了OpenNI,同时也对OpenNI2进行了封装,OpenNI2并不与OpenNI向下兼容,所以在编译安装时,需要同时安装,以确保能够使用OpenNI开发的以前的产品,OpenNI2支持的硬件比较典型的是Carmine 1.09和Kinect v2,其同时支持华硕的Xtion系列等。OpenNI2下载链接在http://structure.io/openni。OpenNI的下载链接还是在http://www.pointclouds.org/downloads/windows.html(不过这里的驱动和OpenNI小版本号进行了更新)。它们的源码链接都在https://github.com/OpenNI,本例程是需要安装OpenNI的。

简单的例子

PCL测试例子中在visualization(可视化)中有一段非常简短的代码,它包含了设置pcl::PointCloud<XYZ>或者pcl::PointCloud<XYZRGB>回调函数所需要的所有东西。下面图3-6及图3-7所示是运行PCL OpenNI Viewer的实时截图,其使用的正是OpenNI Grabber类。

图3-6 PCL OpenNI Viewer运行效果

图3-7 openni_viewer_simple测试结果

我们看看代码。代码在PCL源码目录下的visualization/tools/openni_viewer_simple.cpp文件中。

就像你看到的,SimpleOpenNIViewer 的run()函数首先创建了一个新的OpenNIGrabber接口,下面的一行第一眼看起来挺吓人的,但是并非这样。我们用回调函数cloud_cb_地址创建boost::bind对象,给SimpleOpenNIViewer传递一个引用和参数_1作为占位符。

该bind被固定到boost::function对象中,boost::function对象的函数类型是实例化的模板函数,在本例中是void(constpcl::PointCloud<pcl::PointXYZ>::ConstPtr&)。生成的函数对象可以在 OpenNIGrabber 中注册,随后开始在 OpenNIGrabber 起作用。注意不需要调用stop()方法,因为析构函数可以完成相应的功能。

关键函数

OpenNIGrabber提供不止一种数据类型,这是PCL把Grabber接口设计得如此通用的原因,而这也使得boost::bind一行相对复杂。实际上,我们可以注册下面几种类型的回调函数。

这仅仅提供内置摄像头生成的RGB图像。

这个提供深度图像,不带任何颜色或者亮度信息。

当注册上面这种类型的回调函数时,采集器会发送RGB图像和深度图像,以及一个常数(1/焦距),该常数用于用户自定义进行视差转换计算。所有需要深度图像和RGB图像流的回调函数类型都会启用一个同步机制,它能保证一致的深度和图像数据。这样引入了一个小的时延,因为同步机制在发送第一张图像之前至少需要等待采集到一组图片。

开始和停止数据流

调用registerCallback将返回一个boost::signals2::connection对象,上面的例子里我们忽略了它。然而,如果你想要中断或者取消一个或多个注册数据流,只需要断开与回调函数的连接,而不用停止整个采集器,这样其他还在进行处理的回调函数可以正常工作。

基准测试程序代码

下面的代码段尝试请求访问深度和颜色数据流,它是作为一种基准来测试你的采集系统能否正常使用而提供的。如果你的电脑太慢,可能达不到29Hz以上。

首先,在本书提供的第3章例4文件夹中,打开名为openni_grabber.cpp的代码文件。

解释说明

现在,解析上面打开的源代码,在下面几行声明相关头文件,其中openni_grabber.h是openni相关类接口定义的头文件。

下面是类SimpleOpenNIProcessor的回调函数,作为在获取数据时对数据进行处理的回调函数的封装,在本例中并没有什么处理,只是实时在标准输出设备打印出一些信息。

下面是类SimpleOpenNIProcessor的run()函数,实现对设备的打开和数据的采集,并同时实现对回调函数的注册。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,并生成相应的可执行文件。运行可执行文件后,结果如图3-8所示,结束数据获取用Ctrl+C组合键,笔者测试是用华硕的设备进行的,在调试程序时需要注意设备打开后可以看到设备的摄像头发射红色散斑,有时程序退出但未关闭设备的话,设备摄像头仍然有红色散斑,用这一点可以判断设备是否正常关闭。

图3-8 基准测试程序运行结果

3.2.5 PLY、LAS等常见数据格式与PCD的转换

1.LAS数据格式与PCD数据格式的转换

LAS是美国摄影测量与遥感协会 (ASPRS) 所创建和维护的行业格式。LAS 是一种用于激光雷达数据交换的已发布标准文件格式,它保留与激光雷达数据有关的特定信息。供应商与客户可通过它来交换数据和保留特定于此类数据的所有信息。每个 LAS 文件都在页眉块中包含激光雷达测量的元数据,然后是所记录的每个激光雷达脉冲的所有记录。每个 LAS 文件的页眉部分都保留有激光雷达测量本身的属性信息:数据范围、飞行日期、飞行时间、点记录数、返回的点数、使用的所有数据偏移以及使用的所有比例因子。为 LAS 文件的每个激光雷达脉冲保留以下激光雷达点属性:x,y,z 位置信息、GPS 时间戳、强度、回波编号、回波数目、点分类值、扫描角度、附加 RGB 值、扫描方向、飞行航线的边缘、用户数据、点源 ID 和波形信息。

LibLAS(https://www.liblas.org/)是一套用于处理常见的“LAS” LiDAR 格式数据的C/C++函数库。此处利用LibLAS库结合PCL点云库进行LAS和PCD两种点云类型的转换。主要利用libLAS库中liblas::Reader类和liblas::Writer类分别实现LAS数据的读取和写入。具体地,利用liblas::Reader类读入LAS数据,然后利用reader.GetPoint().GetX()、reader.GetPoint().GetY()、reader.GetPoint().GetZ()获取LAS文件每个点的x,y,z坐标赋予PCL的cloud类存储的点的x,y,z坐标,实现LAS数据格式向PCD数据格式的转换;将PCL的cloud类存储的点的x,y,z坐标提取并赋予liblas::Point类构造的点对象,然后利用liblas::Writer类下的WritePoint函数写入LAS类型文件,实现PCD数据格式向LAS数据格式的转换。由于涉及LibLAS库的具体内容,此处不再详述。下面是结合LibLAS库,将LAS类型数据转换为PCD类型数据的代码。

代码

首先,在本书提供的第3章例5文件夹中,打开名为las2pcd.cpp的代码文件。

解释说明

现在,解析上面打开的源代码的关键语句。下面几行代码声明相关头文件,其中liblas/liblas.hpp是LibLAS库相关类接口定义的头文件。

在下面几行代码中,我们打开并读取LAS文件,并获取LAS数据点的个数。

然后,创建PointCloud类型点云数据,并保证点的个数与LAS数据点的个数一致。

利用LibLAS库reader类的ReadNextPoint函数和GetPoint函数依次读取LAS数据中的每一个点的x,y,z和r,g,b信息,并赋予PCL的PointCloud类型对象,完成LAS类型向PCD类型的转换。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,接下来生成相应的可执行文件,生成可执行文件之后,就可以运行了。此例的可执行文件位于源文件的bin目录下,在CMD中使用以下命令:

运行上述命令后就可以得到名为pointcloud的PCD文件,该文件即为转换结果。其中,*las为需要转换的LAS文件,名为ground.laz的样例LAZ文件位于该例的文件夹中。

2.将PLY数据格式转换为PCD数据格式

PLY是一种常见的点云存储格式,由斯坦福大学开发,其最早主要用于存储三维扫描仪器的点云数据。同PCD格式文件一样,PLY也有两种文件格式:ASCII和二进制。

代码

首先,在本书提供的第3章例6文件夹中,打开名为ply2pcd.cpp的代码文件,在同一文件夹下有点云文件test.ply。

解释说明

现在,解析上面打开的源代码的关键语句。首先说明源代码中定义的loadCloud函数,该函数主要利用pcl::PLYReader类将点云数据由PLY格式数据读取并存为PCLPointCloud2格式。

接着说明源代码中定义的saveCloud函数,该函数主要利用pcl::PCDWriter类将PCLPointCloud2格式的点云存储为PCD格式的文件,当writer类下的write函数中的bool变量format为true时,将被保存为二进制格式,为false时,保存为ASCII码格式。

main函数中主要调用上述介绍的loadCloud函数和saveCloud函数实现PLY文件向PCD文件的转换。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,接下来生成相应的可执行文件,生成可执行文件之后,就可以运行了,在CMD中使用以下命令。

运行上述命令后就可以得到名为test的PCD文件,该文件即为转换结果。

3.将PCD数据格式转换为PLY数据格式

代码

首先,在本书提供的第3章例7文件夹中,打开名为pcd2ply.cpp的代码文件,在同一文件夹下有点云文件test.pcd。

解释说明

现在,解析上面打开源代码的关键语句。首先说明源代码中定义的loadCloud函数,该函数主要利用loadPCDFile函数将点云数据由PCD格式数据读取并存储为PCLPointCloud2格式。

接着说明源代码中定义的saveCloud函数,该函数主要利用pcl::PLYWriter类将PCLPointCloud2格式的点云存储为PLY格式的文件,当writer类下的write函数中的bool变量format为true时,将被保存为二进制格式,为false时,保存为ASCII码格式。

main函数中主要调用上述介绍的loadCloud函数和saveCloud函数实现PCD文件向PLY文件的转换。

编译并运行该程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,接下来生成相应的可执行文件,生成可执行文件之后,就可以运行了,在CMD中使用以下命令。

运行上述命令后即可得到名为test的PLY文件,该文件即为转换结果。

3.2.6 利用Kinect2获取点云数据

Kinect 传感器是微软出品的一套能够获取环境深度信息的相机,其最初应用于游戏娱乐,但是自2010年11月发布以来,很多研究者已经成功地将其用于机器人领域,慢慢成为了入门级室内移动机器人的标配传感器。Kinect的原理可以简单概述为结构光(Structured Light),Kinect2的原理是基于飞行时间(Time of light,TOF),相比于Kinect一代,二代Kinect对环境光的干扰具有更强的鲁棒性。本小节将具体说明如何利用Kinect2结合PCL点云库获取点云数据。

环境搭建

首先参考前面章节配置PCL环境,在确保点云库环境搭建成功的基础上,安装Kinect2 SDK,可在微软官网(https://www.microsoft.com/en-us/download/details.aspx?id=44561)下载该SDK,其安装环境推荐配置如图3-9所示,注意在安装SDK的过程中Kinect2不能与电脑连接,SDK安装完成后连接Kinect2便会自动安装驱动。

图3-9 Kinect2 SDK推荐配置

图3-10所示为用Kinect2 SDK自带的Kinect v2 Configuration Verifier 测试Kinect2是否安装成功,左下角的图像为利用Kinect2实时获取的RGB图像,右下角为深度图像。

图3-10 Kinect V2 Configuration Verifier 示意图

代码

首先,在本书提供的第3章例8文件夹中,打开名为main.cpp和kinect2_grabber.h的代码文件。

解释分析

下面解析源文件的关键语句,首先来看 kinect2_grabber.h。头文件中,主要定义了Kinect2 Grabber 类,该类主要用于从Kinect2设备中读取点云数据,该类从pcl::Grabber类派生而来。

接下来,解析上面打开的 main.cpp代码文件,下面几行代码用于声明相关头文件,需要包含kinect2_grabber.h头文件。

下面的代码用于定义回调函数,作为在数据获取时对数据进行处理的回调函数的封装,本例中未进行处理,读者可以在此处添加需要的处理。

定义一个Kinect2Grabber类型的对象,用于采集点云数据,实现对Kinect2设备的打开和数据的采集,并同时实现对回调函数的注册。

以下代码用于显示采集到的点云数据。

最后停止采集数据。

编译和运行程序

利用提供的CMakeLists.txt文件,在CMake中建立工程文件,并生成相应的可执行文件,生成执行文件后,就可以运行了。如果你想显示Kinect2采集到的点云数据,只需执行如下代码。

运行之后就可以实时显示Kinect2获取的点云数据了,获取点云数据的可视化结果如图3-11所示。

图3-11 Kinect2获取的实时点云结果