上周猛男快乐开发时遇到个bug,要用pcl的函数对自定义的点云进行处理。一起解决问题时遇到了很多问题,解决后整理出来分享给各位参考,以免踩一样的坑😊。文章中自定义的点我用PointT
来表示,自定义点云一般指的是pcl::PointCloud<PointXYZITNormalVelocity> cloud
。
(修这个bug真的烧了很多脑子🧠)
文章目录
- 1. 构造自定义PointT类型
- 2. 用PCL的函数处理自定义点云
- 3. 解决PCL和OpenCV同时使用导致的"no member named 'serialize' "问题
- 4. 小结
1. 构造自定义PointT类型
为了涵盖日常开发中可能使用的点的类型,PCL中已经定义了很多种数据类型。因此在确定要构造自定义类型时,可以先去头文件pcl/impl/point_types.hpp里查看下是否有满足自己需求的点类型。
利用下面这个模板可以完成自定义PointT类型的定义,其中EIGEN_ALIGN16
和PCL_MAKE_ALIGNED_OPERATOR_NEW
切记要加上。PCL中大量利用SSE指令集来加速,所以内存对齐是很必要的。
struct EIGEN_ALIGN16 _PointT // EIGEN_ALIGN16表示16字节对齐,[必须项]
{
// 添加自定义点内部的字段信息 PCL_ADD_RGB;
// ...
// 必须项,确保内存正确对其,旧版本是EIGEN_MAKE_ALIGNED_OPERATOR_NEW
PCL_MAKE_ALIGNED_OPERATOR_NEW
};
struct EIGEN_ALIGN16 PointT: public _PointT
{
// 参考下面的方式补充构造参数,当输入参数是constexpr,那么产生的对象的所有成员都是constexpr。
// inline constexpr PointT (/*输入参数列表*/): /*初始化参数列表*/ {}
// 重载运算符 <<,这样就可以通过std::cout << point 来查看点的信息。
friend std::ostream& operator << (std::ostream& os, const PointT& p);
PCL_MAKE_ALIGNED_OPERATOR_NEW
};
// 点云注册,这里的字段名影响PointCloud<PointT>等相关函数的拷贝、保存或加载功能。
POINT_CLOUD_REGISTER_POINT_STRUCT (_PointT,
// 输入要注册的字段名,比如
// (float, x, x)
// (float, vx, vx)
)
POINT_CLOUD_REGISTER_POINT_WRAPPER(PointT, _PointT)
基于这个模板,我们来定义一个点PointXYZITNormalVelocity
,其包含位置、法向、强度、速度、时间信息。
- 第一步:定义基础类 _PointXYZITNormalVelocity。在添加字段时,参考如下规则:
- 每组信息,满足16字节的优先构建。比如位置,法向,速度,这些数据组。其他信息比如强度,时间等放在后面定义。
- 尽可能利用PCL提供的数据组构建方式。PCL中提供的数据组构建方式列举如下,一般16字节的数据组,都有个
float [4]
来填充,这个对应的字段名在定义的时候要记得防止冲突。切记:尽可能使用已有的,且不要尝试往字段的预留区域添加信息,比如位置信息PCL_ADD_POINT4D
这个,pcl在做点云运算时,可能会将第四个字节也就是data[3]这里赋值为0或1来加速运算。PCL_ADD_POINT4D
[16字节]:定义字段float x,y,z
。16字节对应的字段名为float data[4]
PCL_ADD_NORMAL4D
[16字节]:定义字段float normal_x, normal_y, normal_z
,或者可以通过字段float normal[3]
来访问。16字节对应的字段名为float data_n[4]
PCL_ADD_RGB
:定义字段uint8_t b, g, r, a
,或者可以通过字段uint32_t rgba
来访问。PCL_ADD_INTENSITY
:定义字段float intensity
。PCL_ADD_INTENSITY_8U
:定义字段uint8_t intensity
。PCL_ADD_INTENSITY_32U
:定义字段uint32_t intensity
。
基于上述信息,对应定义代码如下:
struct EIGEN_ALIGN16 _PointXYZITNormalVelocity
{
PCL_ADD_POINT4D; // XYZ [16 bytes]
PCL_ADD_NORMAL4D; // normal [16 bytes]
union // Velocity [16 bytes]
{
float data_v[4];
float velocity[3];
struct
{
float v_x;
float v_y;
float v_z;
};
};
PCL_ADD_INTENSITY;
double time;
};
- 第二步:构造最终点云类型PointXYZITNormalVelocity。
- 完善构造函数,补充几种可能用到的赋值情况。至少得定义个所有字段得赋值方式。
- 补充运算符
<<
的重载。
struct EIGEN_ALIGN16 PointXYZITNormalVelocity: public _PointXYZITNormalVelocity
{
// 几种可能用得到的构造函数,根据需求来定义即可
inline constexpr PointXYZITNormalVelocity (const _PointXYZITNormalVelocity &p) :
PointXYZITNormalVelocity {p.x, p.y, p.z, p.normal_x, p.normal_y, p.normal_z,
p.v_x, p.v_y, p.v_z, p.intensity, p.time} {}
inline constexpr PointXYZITNormalVelocity (float _x, float _y, float _z,
float _nx, float _ny, float _nz,
float _vx, float _vy, float _vz,
float _intensity = 0.f, double _time = 0.0) :
_PointXYZITNormalVelocity{{{_x, _y, _z, 1.0f}}, {{_nx, _ny, _nz, 0.0f}},
{{_vx, _vy, _vz}}, _intensity, _time} {}
inline constexpr PointXYZITNormalVelocity (float _x, float _y, float _z):
PointXYZITNormalVelocity (_x, _y, _z, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f) {}
inline constexpr PointXYZITNormalVelocity (float _x, float _y, float _z,
float _vx, float _vy, float _vz):
PointXYZITNormalVelocity (_x, _y, _z, 0.0f, 0.0f, 0.0f, _vx, _vy, _vz) {}
// 运算符重载
friend std::ostream& operator << (std::ostream& os, const PointXYZITNormalVelocity& p)
{
os << "(" << "xyz: [" << p.x << "," << p.y << "," << p.z << "], ";
os << "v: [" << p.v_x << "," << p.v_y << "," << p.v_z << "], ";
os << "int: " << p.intensity << ", time: " << p.time << ")";
return (os);
}
PCL_MAKE_ALIGNED_OPERATOR_NEW
};
- 第三步:注册点云。这步是最简单得了,把所有需要用的字段按序注册即可。
POINT_CLOUD_REGISTER_POINT_STRUCT (_PointXYZITNormalVelocity,
(float, x, x)
(float, y, y)
(float, z, z)
(float, normal_x, normal_x)
(float, normal_y, normal_y)
(float, normal_z, normal_z)
(float, v_x, v_x)
(float, v_y, v_y)
(float, v_z, v_z)
(float, intensity, intensity)
(double, time, time)
)
POINT_CLOUD_REGISTER_POINT_WRAPPER(PointXYZITNormalVelocity, _PointXYZITNormalVelocity)
2. 用PCL的函数处理自定义点云
PCL很多函数是模板函数,但我们在使用PCL库时,动态库里面封装好的是预定义的点云类型的实现。任何用户代码都不需要编译模板化代码,从而加快了编译时间。
基于上述的定义方式,我们已经得到了一个点云pcl::PointCloud<PointXYZITNormalVelocity>::Ptr cloud
。如果我们想直接调用函数
pcl::CropBox<PointXYZITNormalVelocity> crop
时,会出现类似undefined reference to 'pcl::PCLBase<PointXYZITNormalVelocity>::setInputCloud(std::shared_ptr<pcl::PointCloud<PointXYZITNormalVelocity> const> const&)'
的错误。
我分析了下代码细节,发现我们include的头文件里面的函数尽管是模板函数,但是函数实现部分并没有提供。函数细节对应的头文件放置在impl文件夹下。在include的头文件的最后,有个关键代码,即如果你没有定义PCL_NO_PRECOMPILE
的话,编译代码时是无法获取函数的实现部分的,这也就会导致编译时出现undefined reference
的问题。
知道了原因,解决起来就非常容易了,最好的办法是在项目的CMakeLists.txt里面添加add_definitions(-DPCL_NO_PRECOMPILE)
。当然,如果项目不大的话也可以pcl头文件的前面添加#define PCL_NO_PRECOMPILE
。
除此之外,为了提高编译速度,我们还需要在定义点云的头文件的后面,跟上所使用函数的模板类显式实例化声明。(模板隐式实例化没啥问题,但会导致每个cpp编译时,文件较大,尽管link时候会删除,但会影响编译速度)
以我们要使用的pcl::CropBox
为例,假如我们定义的点云放在头文件custom_types.h
中,那么我们在该文件的末尾添加目标函数的实例化声明:template class pcl::CropBox<PointXYZITNormalVelocity>;
。这样在实例化一次后,其他cpp引用这个无需重复实例化。
PS:各位对显示/隐式实例化感兴趣的可以参考另一个人的博客《C++11模板隐式实例化、显式实例化声明、定义》。
3. 解决PCL和OpenCV同时使用导致的"no member named ‘serialize’ "问题
再解决自定义点云的使用之后,编译项目时又出现了error: 'class std::unordered_map<unsigned int, std::vector<unsigned int>>' has no member name 'serialize'
的错误。
经过大量的查阅资料,才搞懂问题原因,PCL和OpenCV都基于flann这个库,这个库
opencv是连接自己的flann库,而pcl是连接的系统的pcl库。
如果想正常编译,在调用pcl时必须得让USE_UNORDERED_MAP
这个值为0。由于先调用的opencv,然后调用了pcl的库,进而触发了这个bug。
解决办法也很简单,在出问题的pcl头文件前,强行定义#define USE_UNORDERED_MAP 0
即可(有的人是定义为1解决的,实际使用时候可以试试)。当然最优的办法是自己调整好头文件调用方式,把opencv和pcl的调用尽可能分开,然后在cpp里调用。但这样成本比较高,我们还是等待opencv优化下这个问题吧😆。
此外,我研究了下__GXX_EXPERIMENTAL_CXX0X__
这个宏的来源。在Common-Predefined-Macros这里找到了对这个宏的说明。就是当编译时使用了c++11的特征时,即除了-std=C++98
和-std=gnu++98
之外,这个宏就会被定义。这个宏已经过时了,在GCC 4.7.0中定义了__cplusplus
宏,因此相关的代码最好用__cplusplus>=201103L
这个方式来处理。
4. 小结
为了解决这个bug,特意去翻了PCL官网提供的教程《adding_custom_ptype》,讲得很不错,感兴趣可以看看。关于自定义点云中一些宏函数的理解,可以看看《PCL-common-定义点类型
》。GCC相关参数的理解还是得去看官方文档GCC文档。
你们猜猜我为啥分享这么多资料→_→,因为为了解决这个bug,我几乎把全网能翻的都翻了😭。