使用深度學習進行點雲匹配(一)

前言:使用深度學習進行點雲匹配研究是我的畢設題目。因爲之前只學習過深度學習在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中代碼的分析,如果有錯誤的話請指正,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章