PCL中的KDTree

k-d樹(k-dimensional樹的簡稱),是一種分割k維數據空間的數據結構。主要應用於多維空間關鍵數據的搜索(如:範圍搜索和最近鄰搜索)。K-D樹是二進制空間分割樹的特殊的情況。

 

一個KDTree的例子

 

 

上圖的樹就是一棵KDTree,形似二叉搜索樹,其實KDTree就是二叉搜索樹的變種。這裏的K = 3.

首先來看下樹的組織原則。將每一個元組按0排序(第一項序號爲0,第二項序號爲1,第三項序號爲2),在樹的第n層,第 n%3 項被用粗體顯示,而這些被粗體顯示的樹就是作爲二叉搜索樹的key值,比如,根節點的左子樹中的每一個節點的第一個項均小於根節點的的第一項,右子樹的節點中第一項均大於根節點的第一項,子樹依次類推。

 

對於這樣的一棵樹,對其進行搜索節點會非常容易,給定一個元組,首先和根節點比較第一項,小於往左,大於往右,第二層比較第二項,依次類推。

 

 

分割的概念

 

看了上面的例子,確實比較簡單,但不知道爲何要這樣做,這裏從幾何意義出發,引出分割的概念。

先看一個標準的BSTree,每個節點只有一個key值。

 

將key值對應到一維的座標軸上。

 

根節點對應的就是2,左子樹都在2的左邊,右子樹都在2的右邊,整個一維空間就被根節點分割成了兩個部分,當要查找結點0的時候,由於是在2的左邊,所以可以放心的只搜索左子樹的部分。整個搜索的過程可以看成不斷分割搜索區間的過程,直到找到目標節點。

 

這樣的分割可以擴展到二維甚至更多維的情況。

但是問題來了,二維的節點怎麼比較大小?

在BSTree中,節點分割的是一維數軸,那麼在二維中,就應當是分割平面了,就像這樣:

 

黃色的點作爲根節點,上面的點歸左子樹,下面的點歸右子樹,接下來再不斷地劃分,最後得到一棵樹就是赫赫有名的BSPTree(binary space partitioning tree). 分割的那條線叫做分割超平面(splitting hyperplane),在一維中是一個點,二維中是線,三維的是面。

 

KDTree就是超平面都垂直於軸的BSPTree。同樣的數據集,用KDTree劃分之後就是這樣:

 

黃色節點就是Root節點,下一層是紅色,再下一層是綠色,再下一層是藍色。爲了更好的理解KDTree的分割,我們在圖形中來形象地看一下搜索的過程,假設現在需要搜尋右下角的一個點,首先要做的就是比較這個點的x座標和root點的x座標值,由於x座標值大於root節點的x座標,所以只需要在右邊搜尋,接下來,要比較該節點和右邊紅色節點y值得大小...後面依此類推。整個過程如下圖:

   ->  ->

 

 

理解完KDTree之後,下面要說的就是關於KDTree的兩個最重要的問題:

1.樹的建立;

2.最近鄰域搜索(Nearest-Neighbor Lookup)。

樹的建立

先定義一下節點的數據結構。每個節點應當有下面幾個域:

Node-data -  數據矢量, 數據集中某個數據點,是n維矢量(這裏也就是k維)
Range  - 空間矢量, 該節點所代表的空間範圍
split  - 整數, 垂直於分割超平面的方向軸序號
Left  - k-d樹, 由位於該節點分割超平面左子空間內所有數據點所構成的k-d樹
Right  - k-d樹, 由位於該節點分割超平面右子空間內所有數據點所構成的k-d樹
parent  - k-d樹, 父節點

 

建立樹最大的問題在於軸點(pivot)的選擇,選擇好軸點之後,樹的建立就和BSTree差不多了。

建樹必須遵循兩個準則:

1.建立的樹應當儘量平衡,樹越平衡代表着分割得越平均,搜索的時間也就是越少。

2.最大化鄰域搜索的剪枝機會。

 

第一種選取軸點的策略是median of the most spread dimension pivoting strategy,對於所有描述子數據(特徵矢量),統計他們在每個維度上的數據方差,挑選出方差中最大值,對應的維就是split域的值。數據方差大說明沿該座標軸方向上數據點分散的比較開。這個方向上,進行數據分割可以獲得最好的平衡。數據點集Data-Set按照第split維的值排序,位於正中間的那個數據點 被選爲軸點。

但是問題來了,理論上空間均勻分佈的點,在一個方向上分割只有,通過計算方差,下一次分割就不會出現在這個方向上了,但是一些特殊的情況中,還是會出現問題,比如

這樣就會出現很多長條的分割,對於KDTree來說是很不利的。

 

爲了避免這種情況,需要修改一下算法,緯度的選擇的依據爲數據範圍最大的那一維作爲分割緯度,之後也是選中這個緯度的中間節點作爲軸點,然後進行分割,分割出來的結果是:

 

這樣的結果對於最鄰近搜索是非常友好的。

但是這樣做還是有一些不好,就是在樹上很可能有一些空的節點,當要限制樹的高度的時候,這種方法就不合適了。

 

鄰近搜索

給定一個KDTree和一個節點,求KDTree中離這個節點最近的節點.(這個節點就是最臨近點)

這裏距離的求法用的是歐式距離。

 

基本的思路很簡單:首先通過二叉樹搜索(比較待查詢節點和分裂節點的分裂維的值,小於等於就進入左子樹分支,等於就進入右子樹分支直到葉子結點),順着“搜索路徑”很快能找到最近鄰的近似點,也就是與待查詢點處於同一個子空間的葉子結點;然後再回溯搜索路徑,並判斷搜索路徑上的結點的其他子結點空間中是否可能有距離查詢點更近的數據點,如果有可能,則需要跳到其他子結點空間中去搜索(將其他子結點加入到搜索路徑)。重複這個過程直到搜索路徑爲空。

這裏還有幾個細節需要注意一下,如下圖,假設標記爲星星的點是 test point, 綠色的點是找到的近似點,在回溯過程中,需要用到一個隊列,存儲需要回溯的點,在判斷其他子節點空間中是否有可能有距離查詢點更近的數據點時,做法是以查詢點爲圓心,以當前的最近距離爲半徑畫圓,這個圓稱爲候選超球(candidate hypersphere),如果圓與回溯點的軸相交,則需要將軸另一邊的節點都放到回溯隊列裏面來。

判斷軸是否與候選超球相交的方法可以參考下圖:

 

下面再用一個例子來具體說一下查詢的過程。

假設我們的k-d tree就是上面通過樣本集{(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)}創建的。
我們來查找點(2.1,3.1),在(7,2)點測試到達(5,4),在(5,4)點測試到達(2,3),然後search_path中的結點爲<(7,2), (5,4), (2,3)>,從search_path中取出(2,3)作爲當前最佳結點nearest, dist爲0.141;
然後回溯至(5,4),以(2.1,3.1)爲圓心,以dist=0.141爲半徑畫一個圓,並不和超平面y=4相交,如下圖,所以不必跳到結點(5,4)的右子空間去搜索,因爲右子空間中不可能有更近樣本點了。
於是在回溯至(7,2),同理,以(2.1,3.1)爲圓心,以dist=0.141爲半徑畫一個圓並不和超平面x=7相交,所以也不用跳到結點(7,2)的右子空間去搜索。
至此,search_path爲空,結束整個搜索,返回nearest(2,3)作爲(2.1,3.1)的最近鄰點,最近距離爲0.141。

 

 

再舉一個稍微複雜的例子,我們來查找點(2,4.5),在(7,2)處測試到達(5,4),在(5,4)處測試到達(4,7),然後search_path中的結點爲<(7,2), (5,4), (4,7)>,從search_path中取出(4,7)作爲當前最佳結點nearest, dist爲3.202;
然後回溯至(5,4),以(2,4.5)爲圓心,以dist=3.202爲半徑畫一個圓與超平面y=4相交,如下圖,所以需要跳到(5,4)的左子空間去搜索。所以要將(2,3)加入到search_path中,現在search_path中的結點爲<(7,2), (2, 3)>;另外,(5,4)與(2,4.5)的距離爲3.04 < dist = 3.202,所以將(5,4)賦給nearest,並且dist=3.04。
回溯至(2,3),(2,3)是葉子節點,直接平判斷(2,3)是否離(2,4.5)更近,計算得到距離爲1.5,所以nearest更新爲(2,3),dist更新爲(1.5)
回溯至(7,2),同理,以(2,4.5)爲圓心,以dist=1.5爲半徑畫一個圓並不和超平面x=7相交, 所以不用跳到結點(7,2)的右子空間去搜索。

至此,search_path爲空,結束整個搜索,返回nearest(2,3)作爲(2,4.5)的最近鄰點,最近距離爲1.5。

 

所以在搜索中可能會出現不同的情況,比如下面的兩張圖就是比較極端的兩個例子。

接下來就是PCL中的實現過程:


#include <pcl/point_cloud.h>
#include <pcl/kdtree/kdtree_flann.h>
#include <iostream>
#include <vector>
#include <ctime>
 
int main (int argc, char**argv)
{
srand (time (NULL));
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud (new pcl::PointCloud<pcl::PointXYZ>);
//點雲生成
cloud->width =1000;
cloud->height =1;
cloud->points.resize (cloud->width * cloud->height);
for (size_t i=0; i< cloud->points.size (); ++i)
  {
cloud->points[i].x =1024.0f* rand () / (RAND_MAX +1.0f);
cloud->points[i].y =1024.0f* rand () / (RAND_MAX +1.0f);
cloud->points[i].z =1024.0f* rand () / (RAND_MAX +1.0f);
  }
pcl::KdTreeFLANN<pcl::PointXYZ>kdtree;
kdtree.setInputCloud (cloud);
pcl::PointXYZ searchPoint;
searchPoint.x=1024.0f* rand () / (RAND_MAX +1.0f);
searchPoint.y=1024.0f* rand () / (RAND_MAX +1.0f);
searchPoint.z=1024.0f* rand () / (RAND_MAX +1.0f);
// k近鄰搜索
int K =10;
std::vector<int>pointIdxNKNSearch(K);
std::vector<float>pointNKNSquaredDistance(K);
std::cout<<"K nearest neighbor search at ("<<searchPoint.x
<<" "<<searchPoint.y
<<" "<<searchPoint.z
<<") with K="<< K <<std::endl;
if ( kdtree.nearestKSearch (searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance) >0 )
  {
for (size_t i=0; i<pointIdxNKNSearch.size (); ++i)
std::cout<<"    "<<   cloud->points[ pointIdxNKNSearch[i] ].x 
<<" "<< cloud->points[pointIdxNKNSearch[i] ].y 
<<" "<< cloud->points[pointIdxNKNSearch[i] ].z 
<<" (squared distance: "<<pointNKNSquaredDistance[i] <<")"<<std::endl;
  }
// 在半徑r內搜索近鄰
std::vector<int> pointIdxRadiusSearch;
std::vector<float> pointRadiusSquaredDistance;
float radius =256.0f* rand () / (RAND_MAX +1.0f);
std::cout<<"Neighbors within radius search at ("<<searchPoint.x
<<" "<<searchPoint.y
<<" "<<searchPoint.z
<<") with radius="<< radius <<std::endl;
if ( kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance) >0 )
  {
for (size_t i=0; i<pointIdxRadiusSearch.size (); ++i)
std::cout<<"    "<<   cloud->points[ pointIdxRadiusSearch[i] ].x 
<<" "<< cloud->points[pointIdxRadiusSearch[i] ].y 
<<" "<< cloud->points[pointIdxRadiusSearch[i] ].z 
<<" (squared distance: "<<pointRadiusSquaredDistance[i] <<")"<<std::endl;
  }
return 0;
}

 

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