使用深度学习进行点云匹配(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xckkcxxck/article/details/84671024

前言:使用深度学习进行点云匹配研究是我的毕设题目。因为之前只学习过深度学习在2D CV上的一些知识,对于三维点云这种东西根本没有听说过,因此也是感觉头大。好在老师给了我一篇paper,让我先去研究里面的方法,这篇论文是CVPR2017年的一篇口头报告,《3DMatch: Learning Local Geometric Descriptors from RGB-D Reconstructions》,论文主要是提出了3DMatch这么一种基于深度学习方法训练出来的描述子,作者认为他们提出的方法不仅在性能上有提升,在扩展性,鲁棒性等都有提升。同时作者也专门建立了一个网站,http://3dmatch.cs.princeton.edu/ 并开源自己所有的代码和数据,以及评测基准,号召大家可以去提出别的方法进行对比啥的。这也是我第一次阅读长篇英文论文(美赛除外)以及研究matlab代码,因为之前都是用python。不过这几日我发觉matlab与python的相似性简直是太高,而且这门语言实在是容易上手。

正题:接下来说我是怎么研究这篇论文的,首先我把论文就着谷歌翻译翻译了一遍,因为我的英语功底有点差,而且有的地方我也不知道翻译对不对,因为涉及到专业词汇。接下来我去看了官网,官网也翻译了一下,差不多理解,我又看了github,在github上作者给出了代码的运行顺序,所以今天我主要记录一下第一个Demo代码。声明以下都是根据我的理解写成,有不对的地方请友善指出,谢谢。

介绍:Demo程序是作者给出了两幅点云图使用已经训练好的3DMatch描述子进行匹配的代码。这两幅点云都是同一个室内,只不过角度不同。

此图就是Demo的示例图,可以看到将两个点云图align,最后完成匹配。

如果你想运行这个程序,需要按照要求完成以下两步:

可以看到第一步是需要编译C++/CUDA demo代码和Marvin。第二步是下载训练好的权重文件,第三步是加载两个示例3D点云,计算其TDF体素网格体积,并计算随机表面关键点及其3DMatch描述子(保存到磁盘上的二进制文件)。 警告:此演示仅读取以简单二进制格式保存的3D点云。 如果您想以自己的点云格式运行3DMatch演示代码,请相应地修改demo.cu。关于这些内容我暂时不讲解是什么,会在接下来用到的时候再进行解释。(应该是本系列第二篇)。可以看出自然是需要Ubuntu系统+CUDA+CUDNN才可以,不过我拜托学长帮忙运行了一下,然后把得到的core直接替换了原来的core文件夹,然后直接在Windows下的Matlab运行,也是没问题的。好,接下来说一下代码。

代码如下:

% Add dependencies (for RANSAC-based rigid transform estimation) 添加依赖项(用于基于RANSAC的刚性变换估计)
addpath(genpath('external'));    %genpath('external')当前文件夹下的所有文件夹

fragment1PointCloudFile = '../data/sample/3dmatch-demo/single-depth-1.ply';
fragment1KeypointsFile = 'fragment-1.keypts.bin';
fragment1DescriptorsFile = 'fragment-1.desc.3dmatch.bin';

fragment2PointCloudFile = '../data/sample/3dmatch-demo/single-depth-2.ply';
fragment2KeypointsFile = 'fragment-2.keypts.bin';
fragment2DescriptorsFile = 'fragment-2.desc.3dmatch.bin';

首先是加载运行需要的一些函数,都在external文件夹下,下面是加载程序运行需要的文件,其中第一个和第四个是自带的点云文件,剩下的都是刚才生成的。2,3是点云1的关键点及其对应的描述子,5,6是点云2的关键点及其描述子。

% Load fragment point clouds         加载点云片段               
fragment1PointCloud = pcread(fragment1PointCloudFile);
fragment2PointCloud = pcread(fragment2PointCloudFile);

% Load keypoints of fragment 1      加载点云1的关键点
fid = fopen(fragment1KeypointsFile,'rb');
numFragment1Keypoints = fread(fid,1,'single');    %500    %fread函数主要用法读取二进制文件  1表示输出数组的维度是1维 ‘single'表示读出的是单精度浮点数
fragment1Keypoints = fread(fid,'single');       %关键点  1500*1 double
fragment1Keypoints = reshape(fragment1Keypoints,3,numFragment1Keypoints)';    %reshape为(500,3)
fclose(fid);

这是加载点云片段和加载点云1的关键点,可以看到每个点云选择了500个关键点,每个点有x,y,z三个坐标,因此是(500,3)

% Load 3DMatch feature descriptors for keypoints of fragment 1
% 加载点云1的3DMatch描述子
fid = fopen(fragment1DescriptorsFile,'rb');
fragment1DescriptorData = fread(fid,'single');     %256002*1 double
fragment1NumDescriptors = fragment1DescriptorData(1);   %500    描述子数量是500
fragment1DescriptorSize = fragment1DescriptorData(2);    %512   描述子大小是512
fragment1Descriptors = reshape(fragment1DescriptorData(3:end),fragment1DescriptorSize,fragment1NumDescriptors)'; %500,512
fclose(fid);

这是加载点云1的3DMatch描述子,描述子文件第一个代表描述子数目,与关键点数目对应是500,然后是大小,每个描述子是一维向量(不知道理解对不对),大小是512.最后是一个(500,512)的向量。

% Load keypoints of fragment 2             %加载点云12片段
fid = fopen(fragment2KeypointsFile,'rb');
numFragment2Keypoints = fread(fid,1,'single');    %500
fragment2Keypoints = fread(fid,'single');         %1500*1
fragment2Keypoints = reshape(fragment2Keypoints,3,numFragment2Keypoints)';   %(1500*1)->(500*3)
fclose(fid);

% Load 3DMatch feature descriptors for keypoints of fragment 2
% %导入点云2的关键点匹配
fid = fopen(fragment2DescriptorsFile,'rb');
fragment2DescriptorData = fread(fid,'single');  %256002*1
fragment2NumDescriptors = fragment2DescriptorData(1);     %500
fragment2DescriptorSize = fragment2DescriptorData(2);      %512
fragment2Descriptors = reshape(fragment2DescriptorData(3:end),fragment2DescriptorSize,fragment2NumDescriptors)';  %(500*512)
fclose(fid);

同理,这是加载点云2的关键点和对应的描述子,也是500个关键点和(500,512)的描述子。从以上程序可以看出,我们已经得到了两幅点云图的关键点和对应的描述子,自然没有说是怎么得到的,因为那不是Demo程序要做的事,接下来我们可以想,肯定是基于描述子,进行关键点的匹配,从而将两幅点云进行匹配。

% Find mutually closest keypoints in 3DMatch descriptor space  在3DMatch描述子空间中查找相互最接近的关键点
fprintf('Finding mutually closest points in 3DMatch descriptor space...\n');
fragment2KDT = KDTreeSearcher(fragment2Descriptors);                   %使用K近邻算法  训练点云2的描述子
fragment1KDT = KDTreeSearcher(fragment1Descriptors);                   %使用K近邻算法  训练点云1的描述子
fragment1NNIdx = knnsearch(fragment2KDT,fragment1Descriptors);         %K近邻查找,对每一个点云1描述子,查找距离最近的点云2描述子,注意返回的试试索引值
fragment2NNIdx = knnsearch(fragment1KDT,fragment2Descriptors);         %K近邻查找,对每一个点云2描述子,查找距离最近的点云1描述子,注意返回的是索引值
fragment2MatchIdx = find((1:size(fragment2NNIdx,1))' == fragment1NNIdx(fragment2NNIdx));% 96个相等  按照fragment2NNIdx中的索引顺序对fragment2NNIdx重新排序后的结果  与 1-500直接匹配,观察哪些相等
fragment2MatchKeypoints = fragment2Keypoints(fragment2MatchIdx,:);           %从fragment2MatchKeypoints取出符合的96个关键点
fragment1MatchKeypoints = fragment1Keypoints(fragment2NNIdx(fragment2MatchIdx),:);  %从fragment2MatchKeypoints取出符合的96个关键点

如何基于描述子匹配关键点,这里直接使用KD树,它就是KNN基于树的一个实现方式,但是它是比较优的解决方式,省内存,减少搜索的计算量,具体怎么实现可以查阅https://www.cnblogs.com/21207-iHome/p/6084670.html。代码中使用KDTreeSearcher分别将点云1关键点描述子进行训练,然后计算点云2的关键点描述子最近的是哪个,再反过来,将点云2关键点描述子进行训练,然后计算点云1的关键点描述子是哪个。最后我们可以看到在500个关键点中有96个关键点的描述子是相同的,也就是说他们的关键点是匹配的。最后两句代码是从所有关键点中取出对应的96个关键点。

% Estimate rigid transformation with RANSAC to align fragment 2 to fragment 1使用RANSAC估计刚性转化以将片段2与片段1比对
fprintf('Running RANSAC to estimate rigid transformation...\n');
[estimateRt,inlierIdx] = ransacfitRt([fragment1MatchKeypoints';fragment2MatchKeypoints'], 0.05, 0);
estimateRt = [estimateRt;[0,0,0,1]];
fprintf('Estimated rigid transformation to align fragment 2 to fragment 1:\n');
fprintf('\t %15.8e\t %15.8e\t %15.8e\t %15.8e\t\n',estimateRt');

接下来应该是从关键点的匹配完成点云的匹配,在这里使用RANSAC算法来估计两个点云之间的刚性变换。啥是刚性变换(Rigid Transformation),又称刚体变换,只改变物体的方向和位置,不改变形状,详细解释可以参阅:https://blog.csdn.net/mjie_liang/article/details/21403937,我们需要知道从刚体从一个位置变换另一个位置需要计算两个东西,R(旋转矩阵)和T(平移矩阵),而我们代码便是为了求出这部分。关于RANSAC算法,这是一个从含有较多噪音数据中建立模型的方法,主要应用便是寻找两幅图的对应位置,通过它可以计算出R和T。参阅https://www.cnblogs.com/weizc/p/5257496.html可以了解此算法。返回值estimateRt包含两部分,前三列是R旋转矩阵的参数,后一列是T平移矩阵的参数。inlierIdx代表选出最佳模型所需要的内点(关于什么是内点,请看RANSAN算法介绍)。

fragment2Points = estimateRt(1:3,1:3) * fragment2PointCloud.Location' + repmat(estimateRt(1:3,4),1,size(fragment2PointCloud.Location',2));
fragment1Points = fragment1PointCloud.Location';

接下来两行意思就很明显了,对点云2使用求得的参数R和T进行刚体变换,是fragment2Points,而fragment1Points直接取点云1的信息就可以了。

% Compute alignment percentage (for loop closure detection)  计算对齐百分比(用于回环检测)
fprintf('Computing surface alignment overlap...\n');
[nnIdx,sqrDists] = multiQueryKNNSearchImpl(pointCloud(fragment2Points'),fragment1Points',1);
dists = sqrt(sqrDists);
ratioAligned = sum(dists < 0.05)/size(fragment1Points,2);
fprintf('Estimated surface overlap: %.1f%%\n',ratioAligned*100);

接下来我们就要评估匹配的好不好了,也就是计算一个对齐的准确率。这里提到了一个名词:回环检测,这个经常在slam中使用,在3D点云中其主要是检测配准的好坏,可以参阅https://www.jianshu.com/p/023e5006499d。总之这是作者构建的评判基准之一。可以看出他设置的标准是dists < 0.05的数目与所有数目的比例,最后结果是19.8%。因为了解太少,我也不知道这个结果是不是很厉害。

% Visualize alignment results  可视化对齐结果
pcwrite(pointCloud([fragment2Points';fragment1Points'],'Color',[repmat(uint8([0,0,255]),size(fragment2Points,2),1);repmat(uint8([255,0,0]),size(fragment1Points,2),1)]),'result','PLYformat','binary');
figure(); pcshow(pointCloud([fragment2Points';fragment1Points'],'Color',[repmat(uint8([0,0,255]),size(fragment2Points,2),1);repmat(uint8([255,0,0]),size(fragment1Points,2),1)]));
fprintf('Generated visualization of alignment! See results in result.ply\n');

最后这部分是可视化最后的匹配结果,将“生成的点云2”(其实是将原点云2进行刚性变换之后的)与原点云1进行匹配。,并把结果保存。在这里我贴一个运行的效果图:

可能和人家的结果差距有点大,不过这里面也有一些随机性,每次运行的结果会有差异。

以上就是我对Demo中代码的分析,如果有错误的话请指正,谢谢。

猜你喜欢

转载自blog.csdn.net/xckkcxxck/article/details/84671024