人工智能 | 计算机视觉 | 深度学习

0%

PCL系列——如何逐渐地配准一对点云

PCL系列

说明

通过本教程,我们将会学会:

  • 如何配准多个点云图。
  • 配准的方法是:点云图两两配准,计算它们的变换矩阵,然后计算总的变换矩阵。
  • 两个点云配准使用的是非线性ICP算法,它是ICP的算法的变体,使用Levenberg-Marquardt最优化。

操作

  • 在VS2010 中新建一个文件 pairwise_incremental_registration.cpp,然后将下面的代码复制到文件中。
  • 参照之前的文章,配置项目的属性。设置包含目录和库目录和附加依赖项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#include <boost/make_shared.hpp> //共享指针
//点/点云
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/point_representation.h>
//pcd文件输入/输出
#include <pcl/io/pcd_io.h>
//滤波
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/filter.h>
//特征
#include <pcl/features/normal_3d.h>
//配准
#include <pcl/registration/icp.h> //ICP方法
#include <pcl/registration/icp_nl.h>
#include <pcl/registration/transforms.h>
//可视化
#include <pcl/visualization/pcl_visualizer.h>

//命名空间
using pcl::visualization::PointCloudColorHandlerGenericField;
using pcl::visualization::PointCloudColorHandlerCustom;

//定义类型的别名
typedef pcl::PointXYZ PointT;
typedef pcl::PointCloud<PointT> PointCloud;
typedef pcl::PointNormal PointNormalT;
typedef pcl::PointCloud<PointNormalT> PointCloudWithNormals;

//全局变量
//可视化对象
pcl::visualization::PCLVisualizer *p;
//左视区和右视区,可视化窗口分成左右两部分
int vp_1, vp_2;

//定义结构体,用于处理点云
struct PCD
{
PointCloud::Ptr cloud; //点云指针
std::string f_name; //文件名
//构造函数
PCD() : cloud (new PointCloud) {}; //初始化
};


// 定义新的点表达方式< x, y, z, curvature > 坐标+曲率
class MyPointRepresentation : public pcl::PointRepresentation <PointNormalT> //继承关系
{
using pcl::PointRepresentation<PointNormalT>::nr_dimensions_;
public:
MyPointRepresentation ()
{
//指定维数
nr_dimensions_ = 4;
}

//重载函数copyToFloatArray,以定义自己的特征向量
virtual void copyToFloatArray (const PointNormalT &p, float * out) const
{
//< x, y, z, curvature > 坐标xyz和曲率
out[0] = p.x;
out[1] = p.y;
out[2] = p.z;
out[3] = p.curvature;
}
};



//在窗口的左视区,简单的显示源点云和目标点云
void showCloudsLeft(const PointCloud::Ptr cloud_target, const PointCloud::Ptr cloud_source)
{
p->removePointCloud ("vp1_target"); //根据给定的ID,从屏幕中去除一个点云。参数是ID
p->removePointCloud ("vp1_source"); //
PointCloudColorHandlerCustom<PointT> tgt_h (cloud_target, 0, 255, 0); //目标点云绿色
PointCloudColorHandlerCustom<PointT> src_h (cloud_source, 255, 0, 0); //源点云红色
p->addPointCloud (cloud_target, tgt_h, "vp1_target", vp_1); //加载点云
p->addPointCloud (cloud_source, src_h, "vp1_source", vp_1);
PCL_INFO ("Press q to begin the registration.\n"); //在命令行中显示提示信息
p-> spin();
}


//在窗口的右视区,简单的显示源点云和目标点云
void showCloudsRight(const PointCloudWithNormals::Ptr cloud_target, const PointCloudWithNormals::Ptr cloud_source)
{
p->removePointCloud ("source"); //根据给定的ID,从屏幕中去除一个点云。参数是ID
p->removePointCloud ("target");
PointCloudColorHandlerGenericField<PointNormalT> tgt_color_handler (cloud_target, "curvature"); //目标点云彩色句柄
if (!tgt_color_handler.isCapable ())
PCL_WARN ("Cannot create curvature color handler!");
PointCloudColorHandlerGenericField<PointNormalT> src_color_handler (cloud_source, "curvature"); //源点云彩色句柄
if (!src_color_handler.isCapable ())
PCL_WARN ("Cannot create curvature color handler!");
p->addPointCloud (cloud_target, tgt_color_handler, "target", vp_2); //加载点云
p->addPointCloud (cloud_source, src_color_handler, "source", vp_2);
p->spinOnce();
}


// 读取一系列的PCD文件(希望配准的点云文件)
// 参数argc 参数的数量(来自main())
// 参数argv 参数的列表(来自main())
// 参数models 点云数据集的结果向量
void loadData (int argc, char **argv, std::vector<PCD, Eigen::aligned_allocator<PCD> > &models)
{
std::string extension (".pcd"); //声明并初始化string类型变量extension,表示文件后缀名
// 通过遍历文件名,读取pcd文件
for (int i = 1; i < argc; i++) //遍历所有的文件名(略过程序名)
{
std::string fname = std::string (argv[i]);
if (fname.size () <= extension.size ()) //文件名的长度是否符合要求
continue;

std::transform (fname.begin (), fname.end (), fname.begin (), (int(*)(int))tolower); //将某操作(小写字母化)应用于指定范围的每个元素
//检查文件是否是pcd文件
if (fname.compare (fname.size () - extension.size (), extension.size (), extension) == 0)
{
// 读取点云,并保存到models
PCD m;
m.f_name = argv[i];
pcl::io::loadPCDFile (argv[i], *m.cloud); //读取点云数据
//去除点云中的NaN点(xyz都是NaN)
std::vector<int> indices; //保存去除的点的索引
pcl::removeNaNFromPointCloud(*m.cloud,*m.cloud, indices); //去除点云中的NaN点

models.push_back (m);
}
}
}


//简单地配准一对点云数据,并返回结果
//参数cloud_src 源点云
//参数cloud_tgt 目标点云
//参数output 输出点云
//参数final_transform 成对变换矩阵
//参数downsample 是否下采样
void pairAlign (const PointCloud::Ptr cloud_src, const PointCloud::Ptr cloud_tgt, PointCloud::Ptr output, Eigen::Matrix4f &final_transform, bool downsample = false)
{
//
//为了一致性和速度,下采样
// \note enable this for large datasets
PointCloud::Ptr src (new PointCloud); //创建点云指针
PointCloud::Ptr tgt (new PointCloud);
pcl::VoxelGrid<PointT> grid; //VoxelGrid 把一个给定的点云,聚集在一个局部的3D网格上,并下采样和滤波点云数据
if (downsample) //下采样
{
grid.setLeafSize (0.05, 0.05, 0.05); //设置体元网格的叶子大小
//下采样 源点云
grid.setInputCloud (cloud_src); //设置输入点云
grid.filter (*src); //下采样和滤波,并存储在src中
//下采样 目标点云
grid.setInputCloud (cloud_tgt);
grid.filter (*tgt);
}
else //不下采样
{
src = cloud_src; //直接复制
tgt = cloud_tgt;
}

//计算曲面的法向量和曲率
PointCloudWithNormals::Ptr points_with_normals_src (new PointCloudWithNormals); //创建源点云指针(注意点的类型包含坐标和法向量)
PointCloudWithNormals::Ptr points_with_normals_tgt (new PointCloudWithNormals); //创建目标点云指针(注意点的类型包含坐标和法向量)
pcl::NormalEstimation<PointT, PointNormalT> norm_est; //该对象用于计算法向量
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree (new pcl::search::KdTree<pcl::PointXYZ> ()); //创建kd树,用于计算法向量的搜索方法
norm_est.setSearchMethod (tree); //设置搜索方法
norm_est.setKSearch (30); //设置最近邻的数量
norm_est.setInputCloud (src); //设置输入云
norm_est.compute (*points_with_normals_src); //计算法向量,并存储在points_with_normals_src
pcl::copyPointCloud (*src, *points_with_normals_src); //复制点云(坐标)到points_with_normals_src(包含坐标和法向量)
norm_est.setInputCloud (tgt); //这3行计算目标点云的法向量,同上
norm_est.compute (*points_with_normals_tgt);
pcl::copyPointCloud (*tgt, *points_with_normals_tgt);

//创建一个 自定义点表达方式的 实例
MyPointRepresentation point_representation;
//加权曲率维度,以和坐标xyz保持平衡
float alpha[4] = {1.0, 1.0, 1.0, 1.0};
point_representation.setRescaleValues (alpha); //设置缩放值(向量化点时使用)

//创建非线性ICP对象 并设置参数
pcl::IterativeClosestPointNonLinear<PointNormalT, PointNormalT> reg; //创建非线性ICP对象(ICP变体,使用Levenberg-Marquardt最优化)
reg.setTransformationEpsilon (1e-6); //设置容许的最大误差(迭代最优化)
//***** 注意:根据自己数据库的大小调节该参数
reg.setMaxCorrespondenceDistance (0.1); //设置对应点之间的最大距离(0.1m),在配准过程中,忽略大于该阈值的点
reg.setPointRepresentation (boost::make_shared<const MyPointRepresentation> (point_representation)); //设置点表达
//设置源点云和目标点云
//reg.setInputSource (points_with_normals_src); //版本不符合,使用下面的语句
reg.setInputCloud (points_with_normals_src); //设置输入点云(待变换的点云)
reg.setInputTarget (points_with_normals_tgt); //设置目标点云
reg.setMaximumIterations (2); //设置内部优化的迭代次数

// Run the same optimization in a loop and visualize the results
Eigen::Matrix4f Ti = Eigen::Matrix4f::Identity (), prev, targetToSource;
PointCloudWithNormals::Ptr reg_result = points_with_normals_src; //用于存储结果(坐标+法向量)

for (int i = 0; i < 30; ++i) //迭代
{
PCL_INFO ("Iteration Nr. %d.\n", i); //命令行显示迭代的次数
//保存点云,用于可视化
points_with_normals_src = reg_result; //
//估计
//reg.setInputSource (points_with_normals_src);
reg.setInputCloud (points_with_normals_src); //重新设置输入点云(待变换的点云),因为经过上一次迭代,已经发生变换了
reg.align (*reg_result); //对齐(配准)两个点云

Ti = reg.getFinalTransformation () * Ti; //累积(每次迭代的)变换矩阵
//如果这次变换和上次变换的误差比阈值小,通过减小最大的对应点距离的方法来进一步细化
if (fabs ((reg.getLastIncrementalTransformation () - prev).sum ()) < reg.getTransformationEpsilon ())
reg.setMaxCorrespondenceDistance (reg.getMaxCorrespondenceDistance () - 0.001); //减小对应点之间的最大距离(上面设置过)
prev = reg.getLastIncrementalTransformation (); //上一次变换的误差
//显示当前配准状态,在窗口的右视区,简单的显示源点云和目标点云
showCloudsRight(points_with_normals_tgt, points_with_normals_src);
}

targetToSource = Ti.inverse(); //计算从目标点云到源点云的变换矩阵
pcl::transformPointCloud (*cloud_tgt, *output, targetToSource); //将目标点云 变换回到 源点云帧

p->removePointCloud ("source"); //根据给定的ID,从屏幕中去除一个点云。参数是ID
p->removePointCloud ("target");
PointCloudColorHandlerCustom<PointT> cloud_tgt_h (output, 0, 255, 0); //设置点云显示颜色,下同
PointCloudColorHandlerCustom<PointT> cloud_src_h (cloud_src, 255, 0, 0);
p->addPointCloud (output, cloud_tgt_h, "target", vp_2); //添加点云数据,下同
p->addPointCloud (cloud_src, cloud_src_h, "source", vp_2);

PCL_INFO ("Press q to continue the registration.\n");
p->spin ();

p->removePointCloud ("source");
p->removePointCloud ("target");

//add the source to the transformed target
*output += *cloud_src; // 拼接点云图(的点)点数数目是两个点云的点数和

final_transform = targetToSource; //最终的变换。目标点云到源点云的变换矩阵
}


//**************** 入口函数 ************************
//主函数
int main (int argc, char** argv)
{
//读取数据
std::vector<PCD, Eigen::aligned_allocator<PCD> > data; //模型
loadData (argc, argv, data); //读取pcd文件数据,定义见上面

//检查用户数据
if (data.empty ())
{
PCL_ERROR ("Syntax is: %s <source.pcd> <target.pcd> [*]", argv[0]); //语法
PCL_ERROR ("[*] - multiple files can be added. The registration results of (i, i+1) will be registered against (i+2), etc"); //可以使用多个文件
return (-1);
}
PCL_INFO ("Loaded %d datasets.", (int)data.size ()); //显示读取了多少个点云文件

//创建一个 PCLVisualizer 对象
p = new pcl::visualization::PCLVisualizer (argc, argv, "Pairwise Incremental Registration example"); //p是全局变量
p->createViewPort (0.0, 0, 0.5, 1.0, vp_1); //创建左视区
p->createViewPort (0.5, 0, 1.0, 1.0, vp_2); //创建右视区

//创建点云指针和变换矩阵
PointCloud::Ptr result (new PointCloud), source, target; //创建3个点云指针,分别用于结果,源点云和目标点云
//全局变换矩阵,单位矩阵,成对变换
//逗号表达式,先创建一个单位矩阵,然后将成对变换 赋给 全局变换
Eigen::Matrix4f GlobalTransform = Eigen::Matrix4f::Identity (), pairTransform;


//遍历所有的点云文件
for (size_t i = 1; i < data.size (); ++i)
{
source = data[i-1].cloud; //源点云
target = data[i].cloud; //目标点云
showCloudsLeft(source, target); //在左视区,简单的显示源点云和目标点云
PointCloud::Ptr temp (new PointCloud); //创建临时点云指针
//显示正在配准的点云文件名和各自的点数
PCL_INFO ("Aligning %s (%d points) with %s (%d points).\n", data[i-1].f_name.c_str (), source->points.size (), data[i].f_name.c_str (), target->points.size ());

//********************************************************
//配准2个点云,函数定义见上面
pairAlign (source, target, temp, pairTransform, true);
//将当前的一对点云数据,变换到全局变换中。
pcl::transformPointCloud (*temp, *result, GlobalTransform);
//更新全局变换
GlobalTransform = GlobalTransform * pairTransform;
//********************************************************

// 保存成对的配准结果,变换到第一个点云帧
std::stringstream ss; //这两句是生成文件名
ss << i << ".pcd";
pcl::io::savePCDFile (ss.str (), *result, true); //保存成对的配准结果

}
}
  • 重新生成项目。
  • 到改项目的Debug目录下,按住Shift,同时点击鼠标右键,在当前窗口打开CMD窗口。
  • 在命令行中输入pairwise_incremental_registration.exe capture0001.pcd capture0002.pcd capture0003.pcd capture0004.pcd capture0005.pcd,执行程序。得到如下图所示的结果。

结果图

参考