在之前一个博客《一文搞懂PCL中自定义点云类型的构建与函数使用》中,清晰地介绍了在PCL中点云的定义与注册方法。我的一个读者很好奇其内部注册的原理以及机制,再加上最近工作中跟猛男开发自定义点云存储的工作,借着这些需求,我也详细得对PCL相关的源码进行了整理与理解。

在正式开始之前,我们对点云注册的需求进行归类整理,进而引出点云注册都是实现哪些功能。点云注册的目的就是让我们在进行数据处理时,对一些常用的功能进行自动重载,减少代码冗余。值得注意的是,PCL中几乎所有的函数和class都是template类型的,目的就是支持各种点云的复用。
首先,我们将点云处理功能分为两类:
- 字段有感功能。字段有感,表示一个功能只会利用固定某些字段,比如我们对点云聚类时只关注字段xyz。如果自定义的点云类型没有所需字段,那么编译就会不通过。此外,在进行函数处理时,不可避免对点云进行进行基本运算操作,即对字段的加减乘除是无感的,引出点云注册必须要实现的功能:点云的加减乘除运算的重载。
- 字段无感功能。字段无感功能一般存在于数据IO(比如pcd点云的读写),点云拷贝(点云类型A拷贝到点云类型B上)。这些功能不会特意对指定字段进行特别的处理,每个字段处理的方式都是类似的。因此,基于这些,字段无感功能需要实现以下几点功能:
- 字段名称。数据类型是字符串,判断两个点云是否有相同字段名可用。
- 字段偏移量。用于拷贝数据使用,offset表示字段在这个数据类型的起点,结合字段字节大小,调用memcpy可以完成字段的拷贝。
- 字段数据类型。同上,数据拷贝需要。
- 字段列表。记录这个点云的字段列表,然后遍历每个字段完成操作
接下来,就按照理解顺序,逐步的对注册过程进行理解。
一 相关Boost宏定义介绍
下面这些宏是在点云注册中用到的宏,这里先对其进行介绍,在阅读时可以先跳过,后面遇到相关宏再来翻看这个即可。
- BOOST_PP_CAT。该宏在展开参数后将其连接起来,定义在
<boost/preprocessor/cat.hpp>
。用法为BOOST_PP_CAT(a, b)
,其中a
和b
为待拼接的操作数。一个例子:
BOOST_PP_CAT(x, BOOST_PP_CAT(y, z)) // 展开为:xyz
- BOOST_PP_SEQ_FOR_EACH 。该宏会对一个序列的每个元素调用一个宏,定义在
<boost/preprocessor/seq/for_each.hpp>
。用法为BOOST_PP_SEQ_FOR_EACH(macro, data, seq)
,其中macro
一个宏,其参数必须为(r, data, elem)
。elem
就是seq
序列的一个元素。data
辅助参数,会传递到macro
中。seq
需要展开的序列。如果seq是(a)(b)(c)
,则会展开为以下序列macro(r, data, a) macro(r, data, b) macro(r, data, c)
,其中r
只是个循环计数,操作中基本不会使用这个,关于r
的内容可以参考这个博客《使用BOOST_PP_SEQ_FOR_EACH宏处理序列》。
#define SEQ (w)(x)(y)(z)
#define MACRO(r, data, elem) BOOST_PP_CAT(elem, data)
BOOST_PP_SEQ_FOR_EACH(MACRO, _, SEQ) // 展开为w_ x_ y_ z_
- BOOST_PP_TUPLE_ELEM 。该宏目的是提取一个tuple操作数的某个数,定义在
<boost/preprocessor/tuple/elem.hpp>
。用法有两种BOOST_PP_TUPLE_ELEM(i, tuple)
,BOOST_PP_TUPLE_ELEM(size, i, tuple)
。size
表示这个tuple的个数。i
提取tuple中的某个元素,有效使用范围是0~size-1。tuple
待提取的操作数。
#define TUPLE (a, b, c, d)
BOOST_PP_TUPLE_ELEM(0, TUPLE) // 展开后结果为: a
BOOST_PP_TUPLE_ELEM(3, TUPLE) // 展开后结果为:d
- BOOST_PP_SEQ_TRANSFORM。该宏目的是对预处理序列(Preprocessor Sequence)中的每个元素进行转换,定义在
<boost/preprocessor/seq/transform.hpp>
。用法BOOST_PP_SEQ_TRANSFORM(TRANSFORM_MACRO, DATA, SEQ)
,参数说明:TRANSFORM_MACRO
: 自定义的转换宏,需接受(r, data, elem)
参数。DATA
: 传递给TRANSFORM_MACRO
的额外数据(可为空)。SEQ
: 输入的预处理序列,格式如 (a)(b)©。
#define SEQ (1)(3)(2)(5)
#define OP(s, data, elem) BOOST_PP_DEC(elem)
BOOST_PP_SEQ_TRANSFORM(OP, 3, SEQ) // expands to (0)(2)(1)(4)
- BOOST_PP_SEQ_ENUM。该宏目的是对序列(Preprocessor Sequence)中的每个元素用
,
进行连接,定义在<boost/preprocessor/seq/enum.hpp>
。用法BOOST_PP_SEQ_ENUM(SEQ)
。
#include <boost/preprocessor/seq/enum.hpp>
#define SEQ (B)(O)(O)(S)(T)
BOOST_PP_SEQ_ENUM(SEQ) // expands to B, O, O, S, T
二 剖析点云注册机制
为了方便各位理解宏相关的操作,我们定义个简单的点云类型_PointXYZIT
,其中包含xyz
,intensity
和一个自定义字段time
。
struct EIGEN_ALIGN16 _PointXYZIT
{
PCL_ADD_POINT4D; // XYZ [16 bytes]
PCL_ADD_INTENSITY;
double time;
};
其对应的点云注册操作如下:
POINT_CLOUD_REGISTER_POINT_STRUCT (_PointXYZIT,
(float, x, x)(float, y, y)(float, z, z)(float, intensity, intensity)(double, time, time)
)
对应POINT_CLOUD_REGISTER_POINT_STRUCT
的定义如下,这里name
就是_PointXYZIT
,fseq
就是(float, x, x)(float, y, y)(float, z, z)(float, intensity, intensity)(double, time, time)
。
#define POINT_CLOUD_REGISTER_POINT_STRUCT(name, fseq) \
POINT_CLOUD_REGISTER_POINT_STRUCT_I(name, \
BOOST_PP_CAT(POINT_CLOUD_REGISTER_POINT_STRUCT_X fseq, 0))
其中:
POINT_CLOUD_REGISTER_POINT_STRUCT_I
:注册每个字段的名称(string),数据类型,偏移量,以及重载该点云的基本加减乘除运算。BOOST_PP_CAT(POINT_CLOUD_REGISTER_POINT_STRUCT_X fseq, 0))
:对fseq
进行后处理,比如将(float, x, x)
变成((float, x, x))
注:宏定义操作目的是构造一个代码块,与我们正常开发函数的方式有点类似,但切记宏定义是代码块,实际编译时候会展开,所以宏这个输入没有什么数据类型的概念。
基于上述内容,引出下面几个更细节的问题:
- 注册序列预处理。()()序列是如何转为(())(())
- 点云注册宏
POINT_CLOUD_REGISTER_POINT_STRUCT
做了啥。 - 自定义点云的保存与加载是如何实现的。
- 点云处理算法是如何适配自定义点云的。
2.1 注册序列预处理
注册序列预处理,是展开BOOST_PP_CAT(POINT_CLOUD_REGISTER_POINT_STRUCT_X fseq, 0)
,前面已说这里的目的是将(float, x, x)
变成((float, x, x))
。关联的宏定义如下:
#define POINT_CLOUD_REGISTER_POINT_STRUCT_X(type, name, tag) \
((type, name, tag)) POINT_CLOUD_REGISTER_POINT_STRUCT_Y
#define POINT_CLOUD_REGISTER_POINT_STRUCT_Y(type, name, tag) \
((type, name, tag)) POINT_CLOUD_REGISTER_POINT_STRUCT_X
#define POINT_CLOUD_REGISTER_POINT_STRUCT_X0
#define POINT_CLOUD_REGISTER_POINT_STRUCT_Y0
这里用了一个非常巧的技巧,递归展开。BOOST_PP_CAT最后面跟了一个参数0,那么最后展开的结果是一定调用了POINT_CLOUD_REGISTER_POINT_STRUCT_X0
或者POINT_CLOUD_REGISTER_POINT_STRUCT_Y0
中的一个,进而避免了无限循环展开。详细展开流程如下,已展开代码用绿色表示,处理的代码用红色表示。
① 展开POINT_CLOUD_REGISTER_POINT_STRUCT_X fseq
:
- POINT_CLOUD_REGISTER_POINT_STRUCT_X(float, x, x)(float, y, y)(float, z, z)(float, intensity, intensity)(double, time, time)
- ((float, x, x))POINT_CLOUD_REGISTER_POINT_STRUCT_Y(float, y, y)(float, z, z)(float, intensity, intensity)(double, time, time)
- ((float, x, x))((float, y, y))POINT_CLOUD_REGISTER_POINT_STRUCT_X(float, z, z)(float, intensity, intensity)(double, time, time)
- 以此类推得到
((float, x, x))((float, y, y))((float, z, z))((float, intensity, intensity))((double, time, time))POINT_CLOUD_REGISTER_POINT_STRUCT_Y
② 拼接BOOST_PP_CAT。BOOST_PP_CAT
已在第一章节介绍,这里BOOST_PP_CAT
第二个参数是0,会基于上一个展开的结果,会将0
拼接在POINT_CLOUD_REGISTER_POINT_STRUCT_Y
后面得到POINT_CLOUD_REGISTER_POINT_STRUCT_Y0
,而这个的宏定义就是空的,避免了无限递归。
③ 最终展开结果:((float, x, x))((float, y, y))((float, z, z))((float, intensity, intensity))((double, time, time))
2.2 为自定义字段构造推断器和常用运算符
注册器的核心就是POINT_CLOUD_REGISTER_POINT_STRUCT_I
,其中name
就是_PointXYZIT
,fseq
就是((float, x, x))...((double, time, time))
,代码实现具体细节如下:
#define POINT_CLOUD_REGISTER_POINT_STRUCT_I(name, seq) \
namespace pcl \
{ \
namespace fields \
{ \
BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_TAG, name, seq) \
} \
namespace traits \
{ \
BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_NAME, name, seq) \
BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_OFFSET, name, seq) \
BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_DATATYPE, name, seq) \
POINT_CLOUD_REGISTER_POINT_FIELD_LIST(name, POINT_CLOUD_EXTRACT_TAGS(seq)) \
} \
namespace common \
{/*common部分就是点云的加减乘除相关运算的重载,单独放在子节里面*/} \
}
为了更容易理解,我将按照common
,fields
,traits
的顺序进行源码解读。
2.2.1 [common] 注册基本运算符
基本运算包含:
- 加法部分:两点相加
pt1+pt2
,自加运算pt1+=pt2
,以及点和一个常数(float)的加法pt1+k, pt1+=k
。 - 减法部分:两点相减
pt1-pt2
,自减运算pt1-=pt2
,以及点和一个常数(float)的减法pt1-k, pt1-=k
。 - 乘除法:点云乘除法只有一个数乘/除,即
pt1 *= k, pt2 = pt1 * k, pt2 = k * pt1
,pt1 /= k, pt2 = pt1 / k, pt2 = k / pt1
。
关键注意:目前点云的加减乘除是正常理解的运算方法,如果有label这种标签属性,还是要慎用这些基本运算。而且对于数乘/除来说,对应的常数只是浮点数,所以整数如果处理之后,会存在精度损失的风险。
(1) 重载点云加法运算
相关代码如下,一个加法运算需要构造5种重载形式,但核心只需要解决点云和点云的加法,点云和一个数的加法。
inline const name& \ // pt1 += pt2
operator+= (name& lhs, const name& rhs) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_PLUSEQ_POINT_TAG, _, seq) \
return (lhs); \
} \
inline const name& \ // pt1 += k
operator+= (name& p, const float& scalar) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_PLUSEQSC_POINT_TAG, _, seq) \
return (p); \
} \
inline const name operator+ (const name& lhs, const name& rhs) \ // pt3 = pt1 + pt2
{ name result = lhs; result += rhs; return (result); } \
inline const name operator+ (const float& scalar, const name& p) \ // pt2 = k + pt1
{ name result = p; result += scalar; return (result); } \
inline const name operator+ (const name& p, const float& scalar) \ // pt2 = pt1 + k
{ name result = p; result += scalar; return (result); } \
以第一个重载函数inline const name& operator+= (name& lhs, const name& rhs)
来理解,这里的name
就是_PointXYZIT
。BOOST_PP_SEQ_FOR_EACH(PCL_PLUSEQ_POINT_TAG, _, seq)
会对seq
,也就是((float, x, x))...((double, time, time))
中的每个元素,来调用宏PCL_PLUSEQ_POINT_TAG
。
#define PCL_PLUSEQ_POINT_TAG(r, data, elem) \
pcl::traits::plus (lhs.BOOST_PP_TUPLE_ELEM(3, 1, elem), \ // 提取elem的第2个元素,即x
rhs.BOOST_PP_TUPLE_ELEM(3, 1, elem));
以第一个元素(float, x, x)
为例,代码展开后为pcl::traits::plus(lhs.x, rhs.x);
,这样被BOOST_PP_SEQ_FOR_EACH
遍历每个元素后,则可以得到这个自定义点云的每个元素的加法形式,其他运算符重载方式类似。
inline const name& operator+= (_PointXYZIT& lhs, const _PointXYZIT& rhs)
{
pcl::traits::plus(lhs.x, rhs.x);
pcl::traits::plus(lhs.y, rhs.y);
pcl::traits::plus(lhs.z, rhs.z);
pcl::traits::plus(lhs.intensity, rhs.intensity);
pcl::traits::plus(lhs.time, rhs.time);
return (lhs);
}
// 如果数据类型T不是个矩阵,则套用正常的加法
template<typename T> inline
std::enable_if_t<!std::is_array<T>::value> plus (T &l, const T &r)
{
l += r;
}
// 如果数据类型T是矩阵,则遍历每个元素进行加法处理
// 根据数据类型来估计这个矩阵的大小
// remove_const_t的目的:假如T是const double,则把其中的const移除掉
template<typename T> inline
std::enable_if_t<std::is_array<T>::value> plus (std::remove_const_t<T> &l, const T &r)
{
// 把一个数组中的数组类型部分移除掉,只保留元素类型。
// 如:int a[12]; 其中a的类型=int[12]; 即去除[12],只保留元素类型 int。
using type = std::remove_all_extents_t<T>;
constexpr std::uint32_t count = sizeof(T) / sizeof(type);
for (std::uint32_t i = 0; i < count; ++i)
l[i] += r[i];
}
这里我们发现注册器还支持数组类型的注册,确实如此。
同样地,对于PCL_PLUSEQSC_POINT_TAG
,以第一个元素(float, x, x)
为例,代码展开后为pcl::traits::plusscalar(lhs.x, scalar);
原理与上面的加法类似。
#define PCL_PLUSEQSC_POINT_TAG(r, data, elem) \
pcl::traits::plusscalar (p.BOOST_PP_TUPLE_ELEM(3, 1, elem), \
scalar);
template<typename T1, typename T2> inline
std::enable_if_t<!std::is_array<T1>::value> plusscalar (T1 &p, const T2 &scalar)
{
p += scalar;
}
template<typename T1, typename T2> inline
std::enable_if_t<std::is_array<T1>::value> plusscalar (T1 &p, const T2 &scalar)
{
using type = std::remove_all_extents_t<T1>;
constexpr std::uint32_t count = sizeof(T1) / sizeof(type);
for (std::uint32_t i = 0; i < count; ++i)
p[i] += scalar;
}
(2) 重载点云减法运算
相关代码如下,一个减法运算需要构造5种重载形式,需要处理的内容基本与加法无益。这里就不进行重复说明了。
inline const name& \ // pt1 -= pt2
operator-= (name& lhs, const name& rhs) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_MINUSEQ_POINT_TAG, _, seq) \
return (lhs); \
} \
inline const name& \ // pt1 -= k
operator-= (name& p, const float& scalar) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_MINUSEQSC_POINT_TAG, _, seq) \
return (p); \
} \
inline const name operator- (const name& lhs, const name& rhs) \ // pt3 = pt1 - pt2
{ name result = lhs; result -= rhs; return (result); } \
inline const name operator- (const float& scalar, const name& p) \ // pt2 = pt1 - k
{ name result = p; result *= -1.0f; result += scalar; return (result); } \
inline const name operator- (const name& p, const float& scalar) \ // pt2 = k - pt1
{ name result = p; result -= scalar; return (result); } \
(3) 重载点云乘除法运算
对于乘除法来说,就只有一个数乘了,这里用法基本与前面相似,没有新的技巧需要介绍。
- 乘除法:点云乘除法只有一个数乘/除,即
pt1 *= k, pt2 = pt1 * k, pt2 = k * pt1
,pt1 /= k, pt2 = pt1 / k, pt2 = k / pt1
。
inline const name& \ // pt1 *= k
operator*= (name& p, const float& scalar) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_MULEQSC_POINT_TAG, _, seq) \
return (p); \
} \
inline const name operator* (const float& scalar, const name& p) \ // pt2 = k * pt1
{ name result = p; result *= scalar; return (result); } \
inline const name operator* (const name& p, const float& scalar) \ // pt2 = pt1 * k
{ name result = p; result *= scalar; return (result); } \
inline const name& \ // pt1 /= k
operator/= (name& p, const float& scalar) \
{ \
BOOST_PP_SEQ_FOR_EACH(PCL_DIVEQSC_POINT_TAG, _, seq) \
return (p); \
} \
inline const name operator/ (const float& scalar, const name& p_in) \ // pt2 = k / pt1
{ name p = p_in; BOOST_PP_SEQ_FOR_EACH(PCL_DIVEQSC2_POINT_TAG, _, seq) \
return (p); } \
inline const name operator/ (const name& p, const float& scalar) \ // pt2 = pt1 / k
{ name result = p; result /= scalar; return (result); } \
2.2.2 [field] 注册字段域

字段域注册代码只有BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_TAG, name, seq)
,对应的宏定义为:
#define POINT_CLOUD_REGISTER_FIELD_TAG(r, name, elem) \
struct BOOST_PP_TUPLE_ELEM(3, 2, elem); \
对于前面的例子_PointXYZIT
,这里展开后结果就是struct x; struct y; struct z; struct intensity; struct time;
然而我们是无法找到struct x;
等属性的相关内容的具体定义,因此这里我们引出PCL的一个非常巧的一个定义方式:不完全类型。
不完全类型指“函数之外、类型的大小不能被确定的类型”,只能以有限方式使用。在这里,你就理解为一个tag,我们永远不会实例化这个。
这个tag在构造类模板中起到了非常大的一个作用,比如我们可以为点云添加一个字段的名称(简化模式),这样在使用时候就可以通过name<_PointXYZIT, pcl::fields::x>::value
来得到指定字段对应的字符串名称(“x”),pcl::fields::x
对应的是前面展开的struct x;
,而且在注册fields
时候完全不用担心重定义问题,因为这个只是个声明,没有任何实质内容。真的是一个非常巧妙的一种设计模式,好喜欢!!!下面的template class仅是个示例,下节细说。
// 这里的value记录的就是字段对应的字符串形式,即
// name<_PointXYZIT, pcl::fields::x>::value = "x"
// name<_PointXYZIT, pcl::fields::intensity>::value = "intensity"
template<class PointT, typename Tag>
struct name
{
static const char value[];
};
到这里肯定有个疑问,这里用宏定义来定义一个唯一的struct是否可以,当然可以。但这种编程模式,属于C++编程技术中的萃取技术(Traits)。萃取技术(Traits)是模板元编程的一个核心概念,用于在编译时提供类型相关的信息。用template比用宏定义更加清晰,也更加容易维护。
2.2.3 [traits] 注册各种字段详细属性
Traits 是一种编程技术,特别适用于 C++ 这样的泛型编程语言。它的核心思想是在编译时收集和封装关于特定类型的各种属性或行为信息,这些信息对于编写泛型代码至关重要,但通常不会直接体现在类型本身的声明中。在编写泛型代码时(比如使用模板),经常会遇到这样的情况:
- 需要根据类型的不同特性(如是否为指针、是否支持某种运算符、是否有特定的成员函数等)来选择不同的实现路径。
- 想要提供一套通用的接口,使其能够适应不同类型的特殊要求,如容器类可能需要知道其元素类型的大小以便高效分配内存。
- 希望在编译阶段就能够发现某些类型不满足特定条件(如不支持某个操作)导致的错误,而不是等到运行时出错。
Traits 技术就是为了满足这些需求而诞生的,它提供了一种机制,使得编译器可以根据类型特质而非类型本身来做出编译时决策,生成最合适的代码。《C++ 的 traits 技术到底是什么? - 星河灿烂梦中寻的回答》

在理解前面的内容之后,我们开始正式理解PCL中的注册原理。注册主要需要记录以下信息:字段名称、字段大小、字段类型、添加列表。
(1) 点云字段名的注册
由BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_NAME, name, seq)
完成,其中关键宏定义POINT_CLOUD_REGISTER_FIELD_NAME
如下:
// templat中用了dummy,这个字段没有意义,只是为了保证这个class可以定义在hpp中,
// 否则认为没有参数,无法直接初始化value的值,编译期抛出错误
#define POINT_CLOUD_REGISTER_FIELD_NAME(r, point, elem) \
template<int dummy> \
struct name<point, pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem), dummy> \
{ \
static const char value[]; \
}; \
\
template<int dummy> \
const char name<point, \
pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem), \
dummy>::value[] = \
BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(3, 2, elem)); \ // 记录字段的名称
原理很简单,声明一个模板struct,其中Point和对应的字段fields作为个Tag,然后初始化value为字段的字符串形式。注意到这里用了一个template name
,并对这个name进行了class template 特化处理,原型如下。
// 这里的POD目前可以理解为POD<PointT>::type等价于PointT,具体用法后面说。
template<class PointT, typename Tag, int dummy = 0>
struct name : name<typename POD<PointT>::type, Tag, dummy>
{
// 特化时需要指定value
// static const char value[];
// 剔除编译器的无限循环,只要这个tag没有被特化,就直接抛出错误
BOOST_MPL_ASSERT_MSG((!std::is_same<PointT, typename POD<PointT>::type>::value),
POINT_TYPE_NOT_PROPERLY_REGISTERED, (PointT&));
};
从点云注册的角度去理解特化,就是每个点的字段都必须对name进行特化,即必须构造struct name<point, pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem), dummy>
,这样在调用时调用的是特化后的数据。否则调用的是原型的类模板,并在编译期抛出错误,以提示开发者需要处理这个字段。(更详细的特化技术可以参考博客:C++ 模板特化与偏特化)
(2) 点云字段偏移量的注册
由BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_OFFSET, name, seq)
完成,其中关键宏定义POINT_CLOUD_REGISTER_FIELD_OFFSET
如下,整体定义思路及方式与name
相似。
template<class PointT, typename Tag>
struct offset : offset<typename POD<PointT>::type, Tag>
{
BOOST_MPL_ASSERT_MSG((!std::is_same<PointT, typename POD<PointT>::type>::value),
POINT_TYPE_NOT_PROPERLY_REGISTERED, (PointT&));
};
// 记录字段偏移量
#define POINT_CLOUD_REGISTER_FIELD_OFFSET(r, name, elem) \
template<> struct offset<name, pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem)> \
{ \
static const std::size_t value = offsetof(name, BOOST_PP_TUPLE_ELEM(3, 1, elem)); \
};
其中offsetof
是个宏定义,定义在#include <cstddef>
中,传入Class名和变量名即可确定当前变量的偏移。原型很简单:
// 通过将结构体首地址虚拟为0来计算成员偏移量
// 该宏在预处理阶段展开,不产生任何运行时开销。偏移量计算结果由编译器根据结构体内存布局直接生成
// 这里仅限制常见的C语言类型(POD),包含数组。其他非POD,比如vector类型就不能这样用了
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
// (TYPE*)0:将0强制转换为TYPE类型的指针,虚拟一个起始地址为0的结构体实例
// &((TYPE*)0)->MEMBER:获取成员MEMBER的地址。由于结构体首地址为0,该地址的数值即等于成员的字节偏移量
// (size_t):将偏移量转换为无符号整型,确保跨平台兼容性
(3) 点云字段数据类型的注册
由BOOST_PP_SEQ_FOR_EACH(POINT_CLOUD_REGISTER_FIELD_DATATYPE, name, seq)
完成,其中关键宏定义POINT_CLOUD_REGISTER_FIELD_DATATYPE
如下:
template<class PointT, typename Tag>
struct datatype : datatype<typename POD<PointT>::type, Tag>
{
BOOST_MPL_ASSERT_MSG((!std::is_same<PointT, typename POD<PointT>::type>::value),
POINT_TYPE_NOT_PROPERLY_REGISTERED, (PointT&));
};
// 利用name确定数据类型,数据类型的enum,以及数据类型大小
// 注:这里pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem)我认为应该是BOOST_PP_TUPLE_ELEM(3, 1, elem)
// 但由于注册时变量和name一样,所以这种问题没有触发
#define POINT_CLOUD_REGISTER_FIELD_DATATYPE(r, name, elem) \
template<> struct datatype<name, pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem)> \
{ \
using type = boost::mpl::identity<BOOST_PP_TUPLE_ELEM(3, 0, elem)>::type; \
using decomposed = decomposeArray<type>; \
static const std::uint8_t value = asEnum<decomposed::type>::value; \
static const std::uint32_t size = decomposed::value; \
};
datatype
中有三个关键变量:
type
:数据类型,比如float, int,float[32]等。boost::mpl::identity
是个模板,输入是什么类型,输出是什么类型,主要用于延迟推导。value
:每个元素的数据类型,每种数据类型映射到了一个uint8上,这样可以在运行时动态的对点云进行拷贝赋值等无感操作。
template<typename T> struct asEnum {};
template<> struct asEnum<std::int8_t> { static const std::uint8_t value = detail::PointFieldTypes::INT8; };
template<> struct asEnum<std::uint8_t> { static const std::uint8_t value = detail::PointFieldTypes::UINT8; };
template<> struct asEnum<std::int16_t> { static const std::uint8_t value = detail::PointFieldTypes::INT16; };
template<> struct asEnum<std::uint16_t> { static const std::uint8_t value = detail::PointFieldTypes::UINT16; };
template<> struct asEnum<std::int32_t> { static const std::uint8_t value = detail::PointFieldTypes::INT32; };
template<> struct asEnum<std::uint32_t> { static const std::uint8_t value = detail::PointFieldTypes::UINT32; };
template<> struct asEnum<float> { static const std::uint8_t value = detail::PointFieldTypes::FLOAT32; };
template<> struct asEnum<double> { static const std::uint8_t value = detail::PointFieldTypes::FLOAT64; };
size
:返回元素个数,如果注册类型是数组,则这里是元素个数。否则就是1。
元素类型和元素个数利用下面这个template进行推断
template<typename T> struct decomposeArray
{
// 单数据:float->float; 数组:float[]->float
using type = std::remove_all_extents_t<T>;
// 单数据: sizeof(float) / sizeof(float) = 1;
// 数组:sizeof(float[8]) / sizeof(float) = 8;
static const std::uint32_t value = sizeof (T) / sizeof (type);
};
(4) 构造点云字段列表
相关代码为POINT_CLOUD_REGISTER_POINT_FIELD_LIST(name, POINT_CLOUD_EXTRACT_TAGS(seq))
。
先看展开seq的部分,这里的目的是提取字段的内容,比如((float, x, x))((float, y, y))
就变更为(pcl::fields::x)(pcl::fields::y)
// 我依然认为这里应该是BOOST_PP_TUPLE_ELEM(3, 1, elem)
#define POINT_CLOUD_TAG_OP(s, data, elem) pcl::fields::BOOST_PP_TUPLE_ELEM(3, 2, elem)
#define POINT_CLOUD_EXTRACT_TAGS(seq) BOOST_PP_SEQ_TRANSFORM(POINT_CLOUD_TAG_OP, _, seq)
注册这个点云的属性列表,就是增加个vector数据类型,承载所有的字段
template<typename PointT>
struct fieldList : fieldList<typename POD<PointT>::type>
{
BOOST_MPL_ASSERT_MSG((!std::is_same<PointT, typename POD<PointT>::type>::value),
POINT_TYPE_NOT_PROPERLY_REGISTERED, (PointT&));
};
#define POINT_CLOUD_REGISTER_POINT_FIELD_LIST(name, seq) \
template<> struct fieldList<name> \
{ \
using type = boost::mpl::vector<BOOST_PP_SEQ_ENUM(seq)>; \
};
// 展开后,_PointXYZIT对应内容为
template<> struct fieldList<_PointXYZIT>
{
using type = boost::mpl::vector<
pcl::fields::x,pcl::fields::y,pcl::fields::z,
pcl::fields::intensity,pcl::fields::time>
}
定义了这个之后,就可以利用for_each_type<typename traits::fieldList<PointT>::type> (mapper);
对每个字段进行遍历,或者利用boost::mpl::contains
判断是否包含指定字段了。
- 对于遍历:可以利用offset和name,直接动态memcpy, write, read对每个字段进行处理,无须关心字段具体内容。
- 对于判断:算法处理时,可以利用contains判断是否存在指定字段,可以在编译期直接报错。
2.3 注册自定义点类型包装器

注册自定义点类型包装器的宏,其核心用途是扩展或适配现有点类型的访问接口,使其与 PCL 内部算法兼容。其定义如下:
#define POINT_CLOUD_REGISTER_POINT_WRAPPER(wrapper, pod) \
BOOST_MPL_ASSERT_MSG(sizeof(wrapper) == sizeof(pod), POINT_WRAPPER_AND_POD_TYPES_HAVE_DIFFERENT_SIZES, (wrapper&, pod&)); \
namespace pcl { \
namespace traits { \
template<> struct POD<wrapper> { using type = pod; }; \
} \
}
我们在定义好点云时候,还会再派生一个class,增加构造方式,以及std::cout打印点云信息。从定义中,也可以发现,派生的class,不能再定义新的字段。

包装器template<> struct POD<wrapper> { using type = pod; };
的定义,就是说明派生class的字段操作方式同原始数据结构,即PointXYZL
的字段操作方式同_PointXYZL
。这里不理解可以先mark下,后面带着例子去理解。
三 点云无感操作函数解析
对于自定义点云来说,无感操作函数,也就是完全不关心有什么字段,只需要遍历已存在的字段列表,完成数据拷贝IO等操作,涉及到拷贝核心就是memcpy,size大小,offset,数据类型(type和实际内容互转)。经典常用的无感函数有3钟:
// 自定义点云保存
template <typename PointT> int
writeBinaryCompressed (const std::string &file_name, const pcl::PointCloud<PointT> &cloud)
{
PCDWriter w;
return (w.writeBinaryCompressed<PointT>(file_name, cloud));
}
// 自定义点云加载
template<typename PointT> inline int
loadPCDFile (const std::string &file_name, pcl::PointCloud<PointT> &cloud)
{
pcl::PCDReader p;
return (p.read (file_name, cloud));
}
// 点云A到点云B的拷贝
template <typename PointInT, typename PointOutT> void
copyPointCloud (const pcl::PointCloud<PointInT> &cloud_in, pcl::PointCloud<PointOutT> &cloud_out);
3.1 字段操作
在正式介绍相关内容之前,还需要理解一些前置函数,比如操作字段相关。
3.1.1 将任意点云的所有字段统一为同一个Struct
pcl中构造了一个结构体PCLPointField
来存放一个字段的所有内容,定义函数getFields
来将任意一种点云类型转换成std::vector<pcl::PCLPointField>
(有一种万象归一的感觉👀),每个字段的转换方式定义在FieldAdder
中。
先介绍可承载“万点类型”的数据结构:
// 定义在common\include\pcl\PCLPointField.h
struct PCLPointField
{
std::string name; // 字段的字符串名称
uindex_t offset = 0; // 字段偏移量
std::uint8_t datatype = 0; // 数据类型,
uindex_t count = 0; // 元素个数
// 还有一些无关函数,跟本节内容无关,省略掉了
}; // struct PCLPointField
PCL中定义了一个函数pcl::for_each_type
,通过调用转换器的operator()
函数来对每个字段完成数据转换,因此对应的转换器细节如下:
template<typename PointT>
struct FieldAdder
{
// 传入vector的引用,记录fields的指针。
FieldAdder (std::vector<pcl::PCLPointField>& fields) : fields_ (fields) {};
// 运算符重载,U表示字段对应的tag,就是在点云注册时定义的
template<typename U> void operator() ()
{
pcl::PCLPointField f;
// 这里传递的就是注册时确定的值
f.name = traits::name<PointT, U>::value;
f.offset = traits::offset<PointT, U>::value;
f.datatype = traits::datatype<PointT, U>::value;
f.count = traits::datatype<PointT, U>::size;
fields_.push_back (f);
}
std::vector<pcl::PCLPointField>& fields_;
};
最终定义了下述函数,实现所有字段的转换
template <typename PointT> std::vector<pcl::PCLPointField>
getFields ()
{
std::vector<pcl::PCLPointField> fields;
// Get the fields list
pcl::for_each_type<typename pcl::traits::fieldList<PointT>::type>(pcl::detail::FieldAdder<PointT>(fields));
return fields;
}
3.1.2 提取两个点云字段的交集
定义前要,每个字段列表的定义如下:
#define POINT_CLOUD_REGISTER_POINT_FIELD_LIST(name, seq) \
template<> struct fieldList<name> \
{ \
using type = boost::mpl::vector<BOOST_PP_SEQ_ENUM(seq)>; \
};
提取两个vector seq1, seq2
交集的思路如下:
- 判断seq1中的每个字段是否在seq2中。
boost::mpl::contains<Sequence2, boost::mpl::_1>
,这里boost::mpl::_1
就是个占位符,在后续操作中被调用时动态接收参数。 - 移除不包含的字段。
- 利用
boost::mpl::not_
将contains
结果取反,即若seq1
中的一个字段key1
不在seq2
中,则为true
。 - 移除字段,
boost::mpl::remove_if
移除seq1
中不在seq2
的字段。
- 利用
template <typename Sequence1, typename Sequence2>
struct intersect
{
using type = typename boost::mpl::remove_if<
Sequence1,
boost::mpl::not_<
boost::mpl::contains<Sequence2, boost::mpl::_1>
>
>::type;
};
3.1.3 判断某个自定义类型是否包含指定字段
在设计算法时,想在编译期预先检查是否包含所需字段,则可以参考common\include\pcl\impl\point_types.hpp
中的类模板has_all_fields
进行判断。
// 是否含有指定字段,如果有则has_field::value = true
template <typename PointT, typename Field>
struct has_field : boost::mpl::contains<
typename pcl::traits::fieldList<PointT>::type, // PointT的全量字段列表
Field // 待判断字段
>::type
{ };
// 指定的字段列表是否都在PointT中,如果都在则has_all_fields::value = true
template <typename PointT, typename Field>
struct has_all_fields : boost::mpl::fold<Field, // boost::mpl::vector
boost::mpl::bool_<true>,
boost::mpl::and_<boost::mpl::_1,
has_field<PointT, boost::mpl::_2> >
>::type
{ };
如果想判断是否存在xyz字段,则可以定义
template <typename PointT>
struct has_xyz : has_all_fields<PointT, boost::mpl::vector<pcl::fields::x,
pcl::fields::y,
pcl::fields::z> >
{ };
// 若值为true则表示xyz字段在PointT中
template <typename PointT>
constexpr auto has_xyz_v = has_xyz<PointT>::value;
3.2 保存点云
点云保存以PCD为例,保存后的PCD内容如下,包含数据头和编码数据两个部分。
关于数据头保存定义在函数generateHeader
中,详见io\include\pcl\io\impl\pcd_io.hpp
,这里仅介绍跟自定义点云相关内容。
template <typename PointT> std::string
pcl::PCDWriter::generateHeader (const pcl::PointCloud<PointT> &cloud, const int nr_points)
{
std::ostringstream oss;
oss.imbue (std::locale::classic ());
oss << "# .PCD v0.7 - Point Cloud Data file format"
"\nVERSION 0.7"
"\nFIELDS";
// 获取点云的所有字段信息
const std::vector<pcl::PCLPointField> fields = pcl::getFields<PointT>();
std::stringstream field_names, field_types, field_sizes, field_counts;
for (const auto &field : fields)
{
// PCLPointCloud2中定义的某种字段,跟本博客内容暂时无关,暂不考虑
if (field.name == "_")
continue;
// 保存字段名称
field_names << " " << field.name;
// 根据数据类型,获取保存字段对应的字节数,
field_sizes << " " << pcl::getFieldSize (field.datatype);
// 根据数据类型,获取保存时数据类型的名称,比如I/U/F等
if ("rgb" == field.name)
field_types << " " << "U";
else
field_types << " " << pcl::getFieldType (field.datatype);
// 记录当前字段的元素个数(字段类型为array情况)
int count = std::abs (static_cast<int> (field.count));
if (count == 0) count = 1; // check for 0 counts (coming from older converter code)
field_counts << " " << count;
}
oss << field_names.str ();
oss << "\nSIZE" << field_sizes.str ()
<< "\nTYPE" << field_types.str ()
<< "\nCOUNT" << field_counts.str ();
// 记录点云个数等信息(跟自定义点无关了,这里可以跳过)
if (nr_points != std::numeric_limits<int>::max ())
oss << "\nWIDTH " << nr_points << "\nHEIGHT " << 1 << "\n";
else
oss << "\nWIDTH " << cloud.width << "\nHEIGHT " << cloud.height << "\n";
oss << "VIEWPOINT " << cloud.sensor_origin_[0] << " " << cloud.sensor_origin_[1] << " " << cloud.sensor_origin_[2] << " " <<
cloud.sensor_orientation_.w () << " " <<
cloud.sensor_orientation_.x () << " " <<
cloud.sensor_orientation_.y () << " " <<
cloud.sensor_orientation_.z () << "\n";
if (nr_points != std::numeric_limits<int>::max ())
oss << "POINTS " << nr_points << "\n";
else
oss << "POINTS " << cloud.size () << "\n";
return (oss.str ());
}
上述介绍了怎么遍历字段来获取字节、名称等信息,下面这里正式介绍怎么保存点云各个字段的数据。
template <typename PointT> int
pcl::PCDWriter::writeBinaryCompressed (const std::string &file_name,
const pcl::PointCloud<PointT> &cloud)
{
// 生成PCD文件头
std::ostringstream oss;
oss << generateHeader<PointT> (cloud) << "DATA binary_compressed\n";
oss.flush ();
int data_idx = static_cast<int> (oss.tellp ());
// 打开文件,准备保存
int fd = io::raw_open (file_name.c_str (), O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
boost::interprocess::file_lock file_lock;
setLockingPermissions(file_name, file_lock);
//
auto fields = pcl::getFields<PointT> (); // 存放待处理的字段
std::size_t fsize = 0; // 记录一个点的字节数
std::size_t data_size = 0; // 记录整个点云字节数,即num * fsize
std::size_t nri = 0;
std::vector<int> fields_sizes (fields.size ()); // 每个字段,整个点云的字节数
for (const auto &field : fields)
{
if (field.name == "_")
continue;
fields_sizes[nri] = field.count * pcl::getFieldSize (field.datatype);
fsize += fields_sizes[nri];
fields[nri] = field;
++nri;
}
fields_sizes.resize (nri);
fields.resize (nri);
data_size = cloud.size () * fsize;
// 分配需要保存的数据内存,我这里选择的是有压缩的保存方式,因此需要将所有字节存放在一起进行无损压缩
char *only_valid_data = static_cast<char*> (malloc (data_size));
// 保存方式是将字段合并在一起保存,比如字段内容是xyz,则将点云的x放一起保存,再将y放一起保存。
std::vector<char*> pters (fields.size ()); // 各个字段的数据指针
std::size_t toff = 0;
for (std::size_t i = 0; i < pters.size (); ++i)
{
pters[i] = &only_valid_data[toff];
toff += static_cast<std::size_t>(fields_sizes[i]) * cloud.size();
}
// 遍历所有的点的所有字段,将数据存放在对应指针下
for (const auto& point: cloud)
{
for (std::size_t j = 0; j < fields.size (); ++j)
{
// 利用offset确定字段首地址,利用field_size确定需要拷贝的字节数
memcpy (pters[j], reinterpret_cast<const char*> (&point) + fields[j].offset, fields_sizes[j]);
// 将指针向后移,移动后即为下个点对应字段的保存地址
pters[j] += fields_sizes[j];
}
}
/*后面与字段操作无关,这里不进行详解了*/
char* temp_buf = static_cast<char*> (malloc (static_cast<std::size_t> (static_cast<float> (data_size) * 1.5f + 8.0f)));
unsigned int compressed_size = pcl::lzfCompress (only_valid_data,
static_cast<std::uint32_t> (data_size),
&temp_buf[8],
static_cast<std::uint32_t> (static_cast<float>(data_size) * 1.5f));
unsigned int compressed_final_size = 0;
// Was the compression successful?
if (compressed_size)
{
char *header = &temp_buf[0];
memcpy (&header[0], &compressed_size, sizeof (unsigned int));
memcpy (&header[4], &data_size, sizeof (unsigned int));
data_size = compressed_size + 8;
compressed_final_size = static_cast<std::uint32_t> (data_size) + data_idx;
}
else
{
resetLockingPermissions (file_name, file_lock);
throw pcl::IOException ("[pcl::PCDWriter::writeBinaryCompressed] Error during compression!");
return (-1);
}
// Prepare the map
// Allocate disk space for the entire file to prevent bus errors.
if (io::raw_fallocate (fd, compressed_final_size) != 0) { /*省略异常处理*/ }
char *map = static_cast<char*> (::mmap (nullptr, compressed_final_size, PROT_WRITE, MAP_SHARED, fd, 0));
if (map == reinterpret_cast<char*> (-1)) //MAP_FAILED) { /*省略异常处理*/ }
// Copy the header
memcpy (&map[0], oss.str ().c_str (), data_idx);
// Copy the compressed data
memcpy (&map[data_idx], temp_buf, data_size);
// Unmap the pages of memory
if (::munmap (map, (compressed_final_size)) == -1) { /*省略异常处理*/ }
// Close file
io::raw_close (fd);
resetLockingPermissions (file_name, file_lock);
free (only_valid_data);
free (temp_buf);
return (0);
}
3.2 加载点云
点云读取时,是先将数据读取到PCLPointCloud2
(全量点云),然后再通过fromPCLPointCloud2
将读取到的点云转换为目标点云,点云加载时允许存在字段的丢失,比如保存时点云类型为PointXYZI,加载时可以指定为PointXYZ。
template<typename PointT> int
read (const std::string &file_name, pcl::PointCloud<PointT> &cloud, const int offset = 0)
{
pcl::PCLPointCloud2 blob;
int pcd_version;
int res = read (file_name, blob, cloud.sensor_origin_, cloud.sensor_orientation_,
pcd_version, offset);
// If no error, convert the data
if (res == 0)
pcl::fromPCLPointCloud2 (blob, cloud);
return (res);
}
保存时按照每个点的每个字段进行保存,那么读取时同样是按照每个字段进行读取,相关代码在io\src\pcd_io.cpp
,这里仅摘出读取部分的代码以便理解:
/* 假设这里我们已经读取完文件头,并且完成相关字段信息的构建,相关变量
std::vector<pcl::PCLPointField> fields;
std::vector<int> fields_sizes; // 每个字段,整个点云的字节数
*/
// 构造每个字段的数据指针起始地址
std::vector<char*> pters (fields.size ());
std::size_t toff = 0;
for (std::size_t i = 0; i < pters.size (); ++i)
{
pters[i] = &buf[toff];
toff += fields_sizes[i] * cloud.width * cloud.height;
}
// Copy it to the cloud
for (uindex_t i = 0; i < cloud.width * cloud.height; ++i)
{
for (std::size_t j = 0; j < pters.size (); ++j)
{
// 利用offset确定字段首地址,利用field_size确定需要拷贝的字节数
// 利用memcpy将保存的数据地址,复制到点云对应位置上
memcpy (&cloud.data[i * fsize + fields[j].offset], pters[j], fields_sizes[j]);
// Increment the pointer
pters[j] += fields_sizes[j];
}
}
3.3 点云拷贝
点云拷贝,主要是将点云类型A,转换为点云数据类型B,核心的功能就是需要将有共同字段的数据拷贝过去,相关代码在common\include\pcl\common\impl\io.hpp
,这里对其中一个最简单的函数进行详解
template <typename PointInT, typename PointOutT> void
copyPointCloud (const pcl::PointCloud<PointInT> &cloud_in, pcl::PointCloud<PointOutT> &cloud_out)
{
// 拷贝点云基本信息,跟自定义点无关,并分配内存空间
cloud_out.header = cloud_in.header;
cloud_out.width = cloud_in.width;
cloud_out.height = cloud_in.height;
cloud_out.is_dense = cloud_in.is_dense;
cloud_out.sensor_orientation_ = cloud_in.sensor_orientation_;
cloud_out.sensor_origin_ = cloud_in.sensor_origin_;
cloud_out.resize (cloud_in.size ());
if (cloud_in.empty ())
return;
// 如果两种点云数据类型一样,直接对点云进行深拷贝
if (isSamePointType<PointInT, PointOutT> ())
memcpy (&cloud_out[0], &cloud_in[0], cloud_in.size () * sizeof (PointInT));
else
// 点云类型不一致,只能一个点一个点的处理了
for (std::size_t i = 0; i < cloud_in.size (); ++i)
copyPoint (cloud_in[i], cloud_out[i]);
}
自定义点的拷贝copyPoint
的主函数定义在common\include\pcl\common\impl\copy_point.hpp
中。
template <typename PointInT, typename PointOutT> void
copyPoint (const PointInT& point_in, PointOutT& point_out)
{
detail::CopyPointHelper<PointInT, PointOutT> copy;
copy (point_in, point_out);
}
值得注意的是,CopyPointHelper
也是个类模板,跟3.1中介绍的FieldAdder
的使用方式相似,PCL中针对有无rgb/rgba字段的点进行可不同的拷贝处理,这里介绍大多数字段的拷贝方式。
// 计算两个点云的字段交集,intersect原理参考3.1
using FieldListInT = typename pcl::traits::fieldList<PointInT>::type;
using FieldListOutT = typename pcl::traits::fieldList<PointOutT>::type;
using FieldList = typename pcl::intersect<FieldListInT, FieldListOutT>::type;
// 遍历每个字段进行拷贝
pcl::for_each_type <FieldList> (pcl::NdConcatenateFunctor <PointInT, PointOutT> (point_in, point_out));
字段拷贝器NdConcatenateFunctor
,定义在common\include\pcl\common\concatenate.h
:
template<typename PointInT, typename PointOutT>
struct NdConcatenateFunctor
{
using PodIn = typename traits::POD<PointInT>::type;
using PodOut = typename traits::POD<PointOutT>::type;
NdConcatenateFunctor (const PointInT &p1, PointOutT &p2)
: p1_ (reinterpret_cast<const PodIn&> (p1))
, p2_ (reinterpret_cast<PodOut&> (p2)) { }
template<typename Key> inline void
operator () ()
{
// 提取字段的数据类型,如果数据类型不同直接抛出错误
using InT = typename pcl::traits::datatype<PointInT, Key>::type;
using OutT = typename pcl::traits::datatype<PointOutT, Key>::type;
BOOST_MPL_ASSERT_MSG ((std::is_same<InT, OutT>::value),
POINT_IN_AND_POINT_OUT_HAVE_DIFFERENT_TYPES_FOR_FIELD,
(Key, PointInT&, InT, PointOutT&, OutT));
// 利用offset,和字段的sizeof,利用memcpy直接拷贝
memcpy (reinterpret_cast<std::uint8_t*>(&p2_) + pcl::traits::offset<PointOutT, Key>::value,
reinterpret_cast<const std::uint8_t*>(&p1_) + pcl::traits::offset<PointInT, Key>::value,
sizeof (InT));
}
private:
const PodIn &p1_;
PodOut &p2_;
};
四 小结
从24年9月份就准备开始彻底理解这个注册原理,用业余时间断断续续花了7个月才把这个吃透(真的太烧脑了😭)。点云注册机制是PCL的核心,吃透了这个,后面去理解点云处理算法会轻松很多。
PCL中的注册器以及各种traits都很深奥,需要花费较多时间去理解。PCL中默认的注册方式有些潜在的约束,用户是无法感知的,而且注册器中有些小小的bug,比如注册字段时,字符串和name都是一致的,因此没有触发。还有对于特殊的类别,如果想在点云中记录label等信息,在进行点云的基本运算时,很容易出现问题。
PCL注册机制对个人的项目开发有很大的启发意义,我在自己的工作中模仿PCL的方式实现了挺多功能,解决新类型注册的封装、代码复用等等。当然,模仿的也仅仅是其冰山一角,代码结构设计还需要不断地在实际应用中不断尝试。