2.3 PCL编码风格简介
2.3.1 PCL编程规范
架构师为了确保在PCL中所有代码风格的一致性,使得其他开发者及用户容易理解源码,PCL开发者制定并遵循着一套严格的编写规范,PCL的开发者都默认遵循,除非有充足的理由才可以不遵循这些规范。当然这些规范也并不是一成不变的,但提出和更改规则的人需要考虑兼容性,那就是让新加的规则与现有的所有代码相适应。
1.PCL推荐的命名规范
文件命名
所有的文件名单词之间应该用下划线隔开,例如unordered_map.hpp。
• 头文件的扩展名为.h。
• 模板类实现文件的扩展名是.hpp。
• 源文件的扩展名是.cpp。
目录命名
所有的目录及其子目录命名时,如果由多个单词组成,其之间用下划线隔开,PCL中各个目录遵循以下规则。
• 头文件都应放在源码目录树中的include/下。
• 模板类实现文件都应放在目录树中的include/impl/下。
• 源文件都应放在目录树中的src/下。
include语句
当文件在同一目录下时include指示语句用双引号,在其他情况下则用尖括号,例如:
宏定义命名
宏定义中字母都采用大写格式,为头文件所定义的宏最后面还需要加上下划线,并且名称从include下目录开始,例如 pcl/filters/bilateral.h 对应 PCL_FILTERS_BILATERAL_H_。#ifndef 和#define定义放在BSD协议后面,代码前面。#endif定义一直在文件结尾,并且加上一句注释掉的宏对应头文件的宏定义,例如:
命名空间的命名
命名空间多于一个单词的,单词之间应该用下划线连接,例如:
类/结构命名
类名(和其他自定义类型的名称)应该是CamelCased(驼峰命名)命名规范,也就是连写单词组成命名,每个单词首字母大写。但是有例外:如果类名包含一个缩写,这个缩写应该全部大写,类名和结构名最好是名词组成的名字。例如:PFHEstimation代替了EstimatePFH,下面是正确的命名代码的例子。
函数/成员函数命名
函数和类的成员函数的命名应该采用CamelCased,也就是连写单词组成命名,除了首个单词首字母小写,其他单词首字母大写,它们的参数命名单词之间用下划线隔开,函数和类的成员函数命名最好采用动词,应该确保这些名字能清楚地表达函数和类成员函数的功能,例如:checkForErrors()而不是errorCheck(), dumpDataToFile()而不是dataFiledump()。下面是正确的用法。
变量命名
命名变量时,单词之间应该用下划线隔开,例如:
a.迭代子变量命名
迭代子变量应该反映出它们迭代的对象,例如:
b.常量命名
常量的名字应该是全大写,例如:
c.成员变量命名
命名类的成员变量时,单词之间用下划线隔开并以下划线结尾,例如:
return语句
return语句需要在圆括号内有返回值,即规定return语句必须有返回值,但是,return语句如果没有返回值也会编译,例如:
2.PCL推荐的缩进与格式
命名空间缩进格式
在头文件里,命名空间的内容应该缩进两个空格,例如:
在一个实现文件里,对每一个类成员函数或函数的命名必须添加命名空间限定,例如:
类格式
一个模板类的模板参数必须与类定义在不同行,例如:
函数/类成员函数格式
每一个函数的返回类型声明必须与函数声明放在不同的行,例如:
在函数实现的时候也一样,返回类型声明必须与函数声明放在不同的行,例如:
或者:
或者:
花括号
花括号成对出现,另起一行定义,必须闭合才组成合理的程序块,例如:
下面的情况花括号就可以省略:
空格格式
再强调一次,在PCL中的每一个代码块的标准缩进是两个空格,这里用单个空格来隔开函数/类成员函数名字与其参数列表,例如:
如果在头文件内嵌套应用了命名空间名,需要将其缩进两个空格,例如:
类和结构成员采用两个空格进行缩进,访问权限限定(public、private和protected)与类成员一级,而在其限定下的成员,则需要缩进两个空格。例如:
自动格式化代码
PCL提供下面一套规则文件,这样通过多种不同的集成开发环境、编辑器等可以自动格式化编码。
(1)Emacs
你可以利用PCL C/C++(http://dev.pointclouds.org/attachments/download/748/pcl-c-style.el),下载并存储此文件,再按如下操作进行。
打开Emacs编辑器,在C/C++hook下添加下面的代码。
(2)Uncrustify 等其他配置
PCL在快速发展和更新阶段,笔者测试发现其他IDE上的配置文件不稳定,关于其IDE的配置文件,读者可以去网站看实时的帮助文件更新。
3.设计结构
类和应用程序接口
对于PCL的大多数类而言,接口(所有公有成员)不包含变量,仅包含两种类型的方法。
第一种方法是get/set类型,该类型允许修改被类使用的参数和输入数据。
第二种方法用于执行类功能,并得到如计算,滤波,分割等结果。
参数传递
get/set类型的方式遵循下面的规则。
• 如果有大量的数据需要传送(常见的例子是在PCL中输入数据),优先采用boost共享指针,而不是传送实际的数据。
• 成对的get与set类型成员函数总是需要采用一致的数据类型。
• 对于get类型成员函数而言,如果只有一个参数需要被传递则会通过返回值;如果是两个或两个以上的参数需要传递,则通过引用方式进行传递。
对于运算、滤波、分割等类型的参数遵循以下规则。
• 无论传递数据的大小,返回参数最好是非指针型参数。
• 总是通过引用方式来传递输出参数。
2.3.2 如何编写新的PCL类
把代码转换成符合PCL思路和句法的代码,对于第一次接触该基础架构的人会显得比较困难,会提出若干疑问。本小节将讲述如何编写新的PCL类以及经常碰到的问题,也将解释在PCL目录树下,与全球PCL用户共享你的代码有哪些优势,这里提倡共享,你也可以把这种观念应用于其他类似的项目中,无论读者是自己直接写,还是改写已有代码,本节的内容都很有帮助,最重要的是可以帮助读者快速阅读了解PCL中的源码。
1.优势:为什么加入PCL开源开发模式
大家可能问到的第一个问题,也是我们要回答的问题,就是:为什么要加入PCL,它的优势在什么地方?这个问题假设你已经认定PCL提供的这套工具和库对你的工程是有用的,因此你已经是一个用户了。因为开源项目大多都是志愿工作,开发者通常来自世界各地,所以开发过程有一定进度,有一定的反复是正常的。这意味着:
• 开发者不可能事先考虑他们编写的代码片段所有可能的用途,但也奠定了一定基础;
• 由于有限的资源和时间,发现解决方案以及应用程序中所有可能出现的漏洞是困难的,由于资源的限制,可能开始的时候进行处理也是不合适的。
在这两种情况下,每个人都很可能遇到了这种困境:要么他们找不到需要的算法或方法,要么现存的方法和应用有漏洞。因此下一步很明显:改变现有代码以适应你的应用程序或问题。当我们在下一节中讨论如何去做的时候,还是要提供我们提出的第一个问题的答案,也就是“为什么要在PCL中共享劳动成果呢?”,依我们的观点,有很多优势存在,引用Eric Raymond的Linus法则“given enough eyeballs, all bugs are shallow(给予足够的关注,所有的漏洞都是显而易见的)”,它的意思是公布你的代码,允许其他人去查看,你的代码就有机会优化和提高,尤其是出现在一个活跃的社区里,PCL就是一个这样的社区。除了上面讲到的,你的贡献可能会有以下众多的作用。
• 别人以你的代码为基础建立新的项目。
• 你学习其他人新的用法(例如,你设计的时候没有考虑的非常有用的设计)。
• 无忧无虑的维护者身份(例如,你可以休假一段时间,回来看到你的代码还在更新维护中。其他贡献者会配置它以适应最新的平台、最新的编译器等等)。
• 你在社区的名声会提高——人人都喜欢受人敬仰。
对于大多数人来讲,上面所提到的都适用。对于另一些人,只有一些适用。
为了举例说明代码转换过程,我们选择下面的例子,实现对给定点云中的强度数据应用双边滤波器,把结果保存到磁盘。
首先,在本书提供的第2章例2文件夹中,打开名为mainBilateralFilter.cpp的代码文件,这里打开的源代码段包括以下步骤。
(1)输入输出代码块:从磁盘读数据,向磁盘写数据。
(2)初始化代码块:用KD树建立一种搜索最近邻的方法。
(3)实际算法代码块:对每个点进行双边滤波处理。
我们的目的是把给出的源码程序转换成PCL的类,以便能够在其他地方重复使用。
2.建立文件结构
有两种不同的方法来建立结构:(1)分别编写代码,作为独立的PCL类,在PCL代码树之外;(2)直接把文件建立在PCL代码目录树里面。我们来阐述后者的操作方式,因为后者的最终结果有利于PCL库发展壮大,也是因为它有一点复杂(也就是,它包含几个附加的步骤)。对于前者,你可以同样操作,只是不需要你在PCL代码目录树中建立对应的文件组织形式,也不需要了解CMake的使用。
假设我们想要新的算法成为PCL滤波库的一部分,我们开始先在代码树目录filters下新建三个不同的文件。
• include/pcl/filters/bilateral.h——包含所有的定义和声明。
• include/pcl/filters/impl/bilateral.hpp——包含模板类的具体实现。
• src/bilateral.cpp——包含具体的不同点类型的模板类实例化。
我们需要给新的类命名,把它称为BilateralFilter,PCL滤波器接口规定每个算法必须有两个声明和实现可供使用:一个操作PointCloud<T>,一个操作PointCloud2。本小节只讲解前者操作PointCloud<T>的实现。
bilateral.h
前面提到过,bilateral.h头文件包含所有和BilateralFilter类相关的声明,下面是最小的框架。
bilateral.hpp
新建bilateral.hpp和bilateral.cpp两个文件,首先是bilateral.hpp。
这个应该是明确的,还没有给BilateralFilter声明任何方法,因此它没有任何具体实现。
bilateral.cpp
再编写bilateral.cpp文件。
因为在PCL(1.X)中编写模板代码,这里模板参数是点的类型,我们要显式地在bilateral.cpp 中说明所有点类型对应模板类的定义,因此用户在使用我们定义的BilateralFilter编译代码时不必花费额外的编译时间。为此,我们需要添加include模板类实例化头文件(bilateral.hpp)。
CMakeLists.txt
把所有新建的文件增加到PCL滤波器的CMakeLists.txt(在\PCL源码根目录\filters\下)文件中,就可以开始编译链接过程。对于CMakeLists.txt的更改完成后,剩下的就是下面对类相关文件内容的填充了。
3.填写类的内容
如果你正确无误地编辑了以上所有文件,在适当的地方使用新的滤波器类重编译PCL源码应该没有问题。这一小节中,我们开始填充每一个文件中的代码,先从bilateral.cpp文件开始,因为它的内容是最短的。
bilateral.cpp
如前所述,我们准备为BilateralFilter类实例化并预编译实例化若干模板。尽管这可能增加PCL滤波库的编译时间,但是在用户编写的代码中使用该类的时候,能够使编译模板速度提高。为此最简单的做法就是像下面在bilateral.cpp文件中手工添加模板实例化代码,每行对应一个需要手工预编译的模板实例。
然而,随着PCL支持的点的类型增加,上面一一对应的定义方式会使文件迅速增大,导致随后在维护PCL更新时变得更麻烦。因此,我们准备使用一个特殊的称为PCL_INSTANTI-ATE的宏,并对以上代码做如下改变。
这个例子,可以对所有在point_types.h文件中定义的XYZ点类型对应的BilateralFilter模板类进行实例化,继而在编译类的时候就省去了PCL用户使用时的编译时间。
仔细看例子中的代码,我们注意到像cloud->points[point_id].intensity这样的引用,这表明我们的滤波器需要在点类型中有强度成员,因此,使用PCL_XYZ_POINT_TYPES就不起作用,因为不是所有声明了的类型都有强度数据。实际上,很容易注意到只有两个类型包含强度成员,也就是PointXYZI和PointXYZINormal,因此替换掉PCL_XYZ_POINT_TYPES,最后的bilateral.cpp文件请参见本章源码文件例3文件夹中的相应文件。
注意:现在还没有为BilateralFilter声明PCL_INSTANTIATE模板类,实际上也没有实现抽象类pcl::Filter中的纯虚函数,因此如果试图编译代码将产生如下错误。
bilateral.h
开始填充BilateralFilter类的内容,首先声明构造函数和它的成员变量。因为该双边滤波器算法有两个参数,我们存储起来作为类的成员,并按照PCL 1.X的API范例来为它们实现各种设值函数(setters)和取值函数(getters),代码如下。
到现在为止都是最普通的C++代码,除了构造函数BilateralFilter(),这里我们给了两个参数默认值。因为我们的类是从pcl::Filter继承的,而pcl::Filter是从pcl::PCLBase继承的,可以用setInputCloud()方法给算法传递输入数据(作为input_存储),因此下面我们增加了一个using声明。
使用using声明能够确保我们的类不需键入完整引用方式就可以访问成员其父类的成员变量input_。下面我们定义applyFilter函数,由于其是纯虚函数,所有从pcl::Filter继承的类都必须实现该方法成员,因此有如下声明。
后面在bilateral.hpp 文件中会给出applyFilter的具体实现。其中构造了一个类型定义(typedef),这样不用键入整个引用就能使用PointCloud类。在例子中查看原始代码,我们注意到该算法是由对点云中每个点进行相同的操作组成的,为保持applyFilter清晰,我们声明一个叫作computePointWeight的方法。
此外,我们注意到例子中构造了一个Kd树对象,用于获取给定点的近邻,因此我们增加下面的内容。
最后,增加核心计算函数G (float x, float sigma)为内联(inline)函数,以便能够加速滤波的计算。因为此方法仅仅在该算法的环境里有用,我们把它设置为私有,完整的头文件见本章源码文件例3文件夹下的bilateral.h。
bilateral.hpp
最后需要编写的是.hpp文件,完成对声明函数的具体实现。这里我们需要实现两个方法,就是applyFilter和computePointWeight。
computePointWeight方法应该很简单,因为它几乎和实例计算代码一样,通过传递一个要计算强度重量的point索引,索引所指示的点由欧氏空间的邻域组成。在applyFilter 中,我们首先利用输入数据构建Kd树,把所有输入数据复制到输出,然后开始计算新的点的强度,赋值于输出点云数据output。此时要为该类声明PCL_INSTANTIATE来实例化模板类了。
此时需要做的另一件事就是检查错误。
• 是否给定了sigma_s_和sigma_r_两个参数。
• 是否设置了搜索方法的对象(例如,tree_)。
对于前者,检查sigma_s_的值,它默认被设置成0,它对算法的行为有十分重要的意义(它实际上是定义了算法所支持滤波的范围大小),因此,如果代码执行的时候其值仍然是0,我们就用宏PCL_ERROR打印一个错误并返回。
就搜索方法来说,我们可以做同样的操作,或者为用户提供默认的选项,最好的默认选项如下。
• 如果点云是有序的,通过pcl::OrganizedDataIndex使用有序搜索方法。
• 如果点云是无序的,通过pcl::KdTreeFLANN使用通用的Kd树。
该模板类完整的实现头文件见本章源码文件例3文件夹下的bilateral.hpp。
利用PCL其他机制
点索引机制,向PCL算法传递点云数据的标准方法是通过访问setInputCloud()。另外,PCL也可通过setIndices()传递用户感兴趣区域或点云集,而不是整个点云。所有的类都从PCLBase继承了以下行为:如果用户没有给出一点的索引,类就会建立一个虚的索引并且在算法的整个运行期间使用。这意味着我们能够很容易地改变上面的实现代码来对<cloud,indices>元组进行操作。这样的好处就是,如果用户确实传递了点的索引,将会使用传递索引对应的点云;如果没有传递,将使用整个点云。
新的bilateral.hpp类就成为下面的样子。
完整的模板类实现头文件参见本章源码文件例4文件夹下的bilateral.hpp。
为了使indices_不用键入整个约束就能运行,我们需要给bilateral.h文件增加一行using语句来指示indices_的位置。
4.许可
建议每一个文件包含一个描述代码作者的许可,这对于用户了解使用该代码会受到何种约束是十分有用的,PCL是100%的BSD许可的,我们在文件中以C++注释的形式嵌入该许可证,详见本章源码文件夹下的License.txt。
如果需要声明其他的版权(或者原始著作权被改变),添加其他类似的内容就行了,例如:
5.合理命名
到目前为止,我们在例程中使用诸如setSigmaS或setSigmaR等简单的词汇来命名set和get功能的函数,在实际中,应该使用更好的命名方法,以便能够真正表示对应参数的功能,在代码的最终版本中我们将重新把setters和getters命名成set/getHalfSize和set/getStdDev以及类似的名字。
6.代码注释
PCL试图在用户和API文档方面保持高标准,支持Doxygen的文档生成的注释已经在上面的例子中删减掉。实际中,我们的bilateral.h头文件类部分如下。
上述代码很明显比前面代码中的注释多了不少,并且都符合一定的格式,这样就是标准的PCL编码风格了,既方便代码的维护,又方便用户的使用和学习,bilateral.hpp文件部分如下。
完成的bilateral.h和bilateral.hpp文件见本章源码文件夹下的例5文件夹。
7.测试新建的类
测试新的类很容易,我们用上面提到的第一个代码段作为例子,转而使用pcl::BilateralFilter类,利用提供的CMakeLists.txt和testfilter.cpp文件,在CMake中建立工程文件,并生成相应的可执行文件,生成执行文件后,就可以运行测试前面定义的类。
2.3.3 PCL的点类型以及如何增加自定义的点类型
本小节不仅解释如何增加你自己的PointT点类型,也介绍了PCL中的模板point类型,以及它们的用处和定义。PCL从开始就伴随着各种预定义的point类型,从用于XYZ数据到更复杂的n维直方图表示法,例如PFH(点特征直方图)。这些类型应该足够支持在PCL中应用的算法及方法,然而,有时用户也希望定义新的类型。
注意:由于PCL的快速更新,本节内容仅仅对PCL 0.x和1.x版本兼容,撰写本书的时候预期在PCL 2.x中会有新的改变,但大的架构不变,只是添加了一些新定义的描述子等点类型。
1.为什么用PointT类型
PCL的PointT可以追溯到它在ROS中作为开源库被开发的时候,大家一致认为,点云是复杂的n维结构,它应该能表示不同类型的信息,然而用户应该知道并理解需要传送什么样的信息,从而使代码更易于调试、优化等。下面给出一个例子,是对XYZ数据的简单操作,对带SSE功能的处理器,最高效的方法是存储三维坐标为浮点型,然后紧跟着一个浮点型数据作为填补位数以满足存储对齐要求。
然而,当用户在嵌入式平台上寻找编译PCL的时候,增加额外的填补就是浪费存储空间,因此,可以用一个简单的不带最后浮点数的PointXYZ结构来替代。此外,如果你的应用程序需要一个包含XYZ三维数据、RGB信息(颜色)和每个点的估计法线的PointXYZRGBNormal类型,定义包含以上所有内容的结构是很简单的,由于PCL中所有的算法都是模板化的,除了更改的自定义结构之外,不需要做其他的更改,从而增加了代码的重用性和可读性。
2.PCL中有哪些可用的PointT类型
为了涵盖能想到的所有可能的情况,PCL中定义了大量的point类型。下面是一小段,在point_types.hpp中有完整目录,这个列表很重要,因为在定义你自己的类型之前,需要了解已有的类型,如果你需要的类型已经存在于PCL,那么就不需要重复定义了。
PointXYZ-成员变量: float x, y, z;
PointXYZ是使用最常见的一个点数据类型,因为它只包含三维xyz坐标信息,这三个浮点数附加一个浮点数来满足存储对齐,用户可利用points[i].data[0],或者points[i].x访问点的x坐标值。
PointXYZI-成员变量: float x, y, z, intensity;
PointXYZI是一个简单的XYZ坐标加intensity的point类型,理想情况下,这四个变量将新建单独一个结构体,并且满足存储对齐,然而,由于point的大部分操作会把data[4]元素设置成0或1(用于变换),不能让intensity与xyz在同一个结构体中,如果这样的话其内容将会被覆盖。例如,两个点的点积会把它们的第四个元素设置成0,否则该点积没有意义,诸如此类。因此,对于兼容存储对齐,用三个额外的浮点数来填补intensity,这样在存储方面效率较低,但是符合存储对齐要求,运行效率较高。
PointXYZRGBA-成员变量:float x,y,z;uint32_t rgba;
除了rgba信息被包含在一个整型变量中,其他的和PointXYZI类似。
PointXYZRGB-float x, y, z, rgb;
除了rgb信息被包含在一个浮点型变量中,其他和PointXYZRGB类似。rgb数据被压缩到一个浮点数里的原因在于早期PCL是作为ROS项目的一部分来开发的,那里RGB数据是用浮点数来传送的,PCL设计者希望所有遗留代码会重新更改(在PCL 2.x中很可能这样做),可能取消此数据类型。
简单的二维x-y point结构
InterestPoint-float x, y, z, strength;
除了strength是用来表示关键点的强度的测量值外,其他的和PointXYZI类似。
Normal-float normal[3],curvature;
另一个最常用的数据类型,Normal结构体表示给定点所在样本曲面上的法线方向,以及对应曲率的测量值(通过曲面块特征值之间关系获得——查看NormalEstimation类API以便获得更多信息,后续章节有介绍),由于在PCL中对曲面法线的操作很普遍,还是用第四个元素来占位,这样就兼容SSE和高效计算,例如,用户访问法向量的第一个坐标,可以通过points[i].data_n[0]、points[i].normal[0]或者points[i].normal_x。再一次强调,曲率不能被存储在同一个结构体中,因为它会被普通的数据操作覆盖掉。
PointNormal-float x, y, z; float normal[3],curvature;
PointNormal是存储XYZ数据的point结构体,并且包括采样点对应法线和曲率。
PointXYZRGBNormal-float x, y, z, rgb, normal[3],curvature;
PointXYZRGBNormal存储XYZ数据和RGB颜色的point结构体,并且包括曲面法线和曲率。
PointXYZINormal-float x, y, z, intensity, normal[3],curvature;
PointXYZINormal存储XYZ数据和强度值的point结构体,并且包括曲面法线和曲率。
PointWithRange-float x, y, z (union with float point[4]), range;
PointWithRange除了range包含从所获得的视点到采样点的距离测量值之外,其他与PointXYZI类似。
PointWithViewpoint-float x,y,z,vp_x,vp_y,vp_z;
PointWithViewpoint除了vp_x、vp_y和vp_z以三维点表示所获得的视点之外,其他与PointXYZI一样。
MomentInvariants-float j1, j2, j3;
MomentInvariants是一个包含采样曲面上面片的三个不变矩的point类型,描述面片上质量的分布情况(查看MomentInvariantsEstimation以获得更多信息)。
PrincipalRadiiRSD-float r_min,r_max;
PrincipalRadiiRSD是一个包含曲面块上两个RSD半径的point类型(查看RSDEstimation以获得更多信息)。
Boundary-uint8_t boundary_point;
Boundary存储一个点是否位于曲面边界上的简单point类型(查看BoundaryEstimation以获得更多信息)。
PrincipalCurvatures-float principal_curvature[3],pc1,pc2;
PrincipalCurvatures包含给定点主曲率的简单point类型(查看PrincipalCurvaturesEstimation以获得更多信息)。
PFHSignature125-float pfh[125];
PFHSignature125包含给定点的PFH(点特征直方图)的简单point类型(查看PFHEstimation以获得更多信息)。
FPFHSignature33-float fpfh[33];
FPFHSignature33包含给定点的FPFH(快速点特征直方图)的简单point类型(查看FPFHEstimation以获得更多信息)。
VFHSignature308-float vfh[308];
VFHSignature308包含给定点VFH(视点特征直方图)的简单point类型(查看VFHEstimation以获得更多信息)。
Narf36-float x, y, z, roll, pitch, yaw; float descriptor[36];
Narf36包含给定点NARF(归一化对齐半径特征)的简单point类型(查看NARFEstimation以获得更多信息)。
BorderDescription-int x, y; BorderTraits traits;
BorderDescription包含给定点边界类型的简单point类型(查看BorderEstimation以获得更多信息)。
IntensityGradient-float gradient[3];
IntensityGradient包含给定点强度的梯度point类型(查看IntensityGradientEstimation以获得更多信息)。
Histogram-float histogram[N];
Histogram用来存储一般用途的n维直方图。
PointWithScale-float x, y, z, scale;
PointWithScale除了scale表示某点用于几何操作的尺度(例如,计算最近邻所用的球体半径、窗口尺寸等等)外,其他的和PointXYZI一样。
PointSurfel-float x, y, z, normal[3],rgba, radius, confidence, curvature;
PointSurfel存储XYZ坐标、曲面法线、RGB信息、半径、可信度和曲面曲率的复杂point类型。
4.如何增加新的PointT类型
为了增加新的point类型,首先需要进行定义,例如:
然后,得确保你的代码包含了PCL中特定的类/算法的模板头文件的实现,它将和新point类型MyPointType共同使用。例如,你想使用pcl::PassThrough,则只需要使用下面的代码即可。
如果你的代码是库的一部分,可以被他人使用,则需要为你自己的MyPointType类型进行显示实例化。
实例
下面的代码段创建了包含XYZ数据的新point类型,连同一个test的浮点型数据,这样满足存储对齐。
2.3.4 PCL中的异常处理机制
本节我们主要讨论PCL在编写和应用过程中如何利用PCL的异常机制,提高程序的健壮性。首先从PCL开发者角度,解释如何定义和抛出自己的异常,最后从PCL使用者角度出发,解释用户如何捕获异常及处理异常。
1.开发者如何增加一个新的异常类
为了增强程序的健壮性,PCL提供了异常处理机制。作为PCL的开发者,需要通过自定义异常以及抛出异常,告诉调用者出现了什么错误,并提示其如何处理。在PCL中,规定任何一个新定义的PCL异常类都需要继承于PCLException类,其具体定义在文件pcl/exceptions.h 中,这样才能够使用PCL中其他和异常处理相关的机制和宏定义等。
上面是一个最简单的自定义异常类,只定义了空的重构函数,但也足可以完成对一般异常信息进行抛出处理等功能了。
2.如何使用自定义的异常
在PCL中,为了方便开发者使用自定义的异常,需要定义下面的宏定义。
在异常抛出时使用就相当简单,添加下面的代码即可完成对异常的抛出处理。
如此,通过宏调用,就可以实现对异常的抛出处理,此处抛出的异常包含异常信息、发生异常的文件名,以及异常发生的行号。当然这里的异常信息可以包含很多内容,主要因为在宏定义中通过使用ostringstream的对象,开发者可以任意自定义自己的异常信息,例如添加运行过程当中一些重要的参数名或变量名以及其值等,这样就给异常捕获者更多有用的信息,方便异常处理。这里需要说明另一个问题,以下面的代码为例。
PCL开发者在自定义函数中,如果使用了异常抛出,则需要添加Doxygen格式的注释,这样可以在最终的API文档中产生帮助信息,使用者通过文档可以知道,在调用该函数时需要捕获异常和进行异常处理,本例中,在用户调用myFunction(int nb)函数时,就需要捕获处理MyException异常。
3.异常的处理
作为PCL的使用者来说,为了能更好地处理异常,你需要使用try...catch程序块。此处和其他异常处理基本一样,例如下面的实例:
异常的处理与其自身的上下文关系很大,并没有一般的规律可循,此处列举一些最常用的处理方式。
• 如果捕获的异常很关键,那就终止运行;
• 修改异常抛出函数的当前调用参数,再次调用该函数;
• 抛出明确而对用户有意义的异常消息;
• 继续运行该程序,这种选择慎用。
本小节只是简单对PCL异常处理机制进行简单实例解说,在PCL官方论坛中有关异常处理的讨论比较多,读者可以自行参考(如http://www.pcl-developers.org/to-throw-or-not-to-throw-td4828759.html)。
本章系统地对PCL在Windows和ROS平台上的开发环境搭建进行了介绍,最后用实例演示了如何建立自己的PCL应用程序。由于针对不同的系统和各个依赖库的版本不同,同时PCL的源码不断在更新,其CMake选项等也随之改变,所以在编译过程中常会遇到各种问题,读者可以在支持本书出版的非营利性组织PCLCN(Point Cloud Learning in China)的论坛(http://www.pclcn.org/bbs/forum.php)上查找或发帖求助,该组织目前正在和笔者合作进行PCL点云处理技术平台在中国的传播、授业、解惑。