k-d tree算法的研究

By RaySaint 2011/10/12

動機

先前寫了一篇文章《SIFT算法研究》講了講SIFT特徵具體是如何檢測和描述的,其中也提到了SIFT常見的一個用途就是物體識別,物體識別的過程如下圖所示:

p_w_picpath

如上圖(a),我們先對待識別的物體的圖像進行SIFT特徵點的檢測和特徵點的描述,然後得到了SIFT特徵點集合。接下來生成物體目標描述要做的就是對特徵點集合進行數據組織,形成一種特殊的表示,其作用是爲了加速特徵點匹配的過程。所謂的特徵點匹配本質上是一個通過距離函數(例如歐式距離)在高維矢量之間進行相似性檢索的問題,簡單來講就是範圍查詢或者K近鄰查詢的問題。

 

範圍查詢就是給定查詢點和查詢距離閾值,從數據集中找出所有與查詢點距離小於查詢距離閾值的數據;K近鄰查詢就是給定查詢點和正整數K,從數據集中找到距離查詢點最近的K個數據,當K=1時,它就是最近鄰查詢

 

如上圖(b)我們從輸入圖像中進行SIFT特徵點的檢測和特徵點的描述後,得到了一個待查詢點的集合,接下來就是要找出集合中的每一個待查詢點在(a)過程得到的目標物體的特徵點集合中進行2近鄰查詢(即得到最近鄰和次近鄰),得到一組特徵點的匹配對<待查詢點,待查詢點的最近鄰>;得到所有匹配對後,然後通過閾值法(與最近鄰的距離要小於一個常數)和比值法(與最近鄰的距離比次近鄰的距離要小於一個常數)進行提純,濾去較差的匹配對。得到最終的匹配對集合。最後在計算單應性矩陣時,使用RANSAC算法再進行一次提純,剔除錯誤的匹配對。關於RANSAC算法,我還會再寫一篇文章講一講。David G.Lowe的論文說3個或以上的特徵點匹配對可以確認一個正確的識別。(因爲單應性矩陣的計算最少得使用4個點,並且可能會有錯誤匹配的情況存在,所以最好需要多一點的特徵點匹配對)

 

本文的主要目的是講一下如何創建k-d tree對目標物體的特徵點集合進行數據組織使用k-d tree最近鄰搜索來加速特徵點匹配。上面已經講了特徵點匹配的問題其實上是一個最近鄰(K近鄰)搜索的問題。所以爲了更好的引出k-d tree,先講一講最近鄰搜索。

 

最近鄰搜索

先給出一個最近鄰的數學形式的定義。給定一個多維空間p_w_picpath,把p_w_picpath中的一個向量成爲一個樣本點或數據點。p_w_picpath中樣本點的有限集合稱爲樣本集。給定樣本集E,和一個樣本點d,d的最近鄰就是任何樣本點d’∈E滿足None-nearer(E,d,d’)。

None-nearer如下定義:

p_w_picpath

上面的公式中距離度量是歐式距離,當然也可以是任何其他Lp-norm。

p_w_picpath

其中di是向量d的第i個分量。

現在再來說最近鄰搜索,如何找到一個這樣的d’,它離d的距離在E中是最近的。

很容易想到的一個方法就是線性掃描,也稱爲窮舉搜索,依次計算樣本集E中每個樣本點到d的距離,然後取最小距離的那個點。這個方法又稱爲樸素最近鄰搜索。當樣本集E較大時(在物體識別的問題中,可能有數千個甚至數萬個SIFT特徵點),顯然這種策略是非常耗時的。

因爲實際數據一般都會呈現簇狀的聚類形態,因此我們想到建立數據索引,然後再進行快速匹配。索引樹是一種樹結構索引方法,其基本思想是對搜索空間進行層次劃分。k-d tree是索引樹中的一種典型的方法。

k-d tree的簡介及表示

k-d tree是英文K-dimension tree的縮寫,是對數據點在k維空間中劃分的一種數據結構。k-d tree實際上是一種二叉樹。每個結點的內容如下:

域名 類型 描述
dom_elt kd維的向量 kd維空間中的一個樣本點
split 整數 分裂維的序號,也是垂直於分割超面的方向軸序號
left kd-tree 由位於該結點分割超面左子空間內所有數據點構成的kd-tree
right kd-tree 由位於該結點分割超面右子空間內所有數據點構成的kd-tree

樣本集E由k-d tree的結點的集合表示,每個結點表示一個樣本點,dom_elt就是表示該樣本點的向量。該樣本點根據結點的分割超平面將樣本空間分爲兩個子空間。左子空間中的樣本點集合由左子樹left表示,右子空間中的樣本點集合由右子樹right表示。分割超平面是一個通過點dom_elt並且垂直於split所指示的方向軸的平面。舉個簡單的例子,在二維的情況下,一個樣本點可以由二維向量(x,y)表示,其中令x維的序號爲0,y維的序號爲1。假設一個結點的dom_elt爲(7,2) ,split的取值爲0,那麼分割超面就是x=dom_elt(0)=7,它垂直與x軸且過點(7,2),如下圖所示:

p_w_picpath

(紅線代表分割超平面)

於是其他數據點的x維(第split=0維)如果小於7,則被分配到左子空間;若大於7,則被分配到右子空間。例如,(5,4)被分配到左子空間,(9,6)被分配到右子空間。如下圖所示:

p_w_picpath

 

從上面的表也可以看出k-d tree本質上是一種二叉樹,因此k-d tree的構建是一個逐級展開的遞歸過程。

其算法的僞代碼如下:

  1. 算法:createKDTree 構建一棵k-d tree 
  2.  
  3. 輸入:exm_set 樣本集 
  4.  
  5. 輸出 : Kd, 類型爲kd-tree 
  6.  
  7. 1. 如果exm_set是空的,則返回空的kd-tree 
  8.  
  9. 2.調用分裂結點選擇程序(輸入是exm_set),返回兩個值 
  10.  
  11.        dom_elt:= exm_set中的一個樣本點 
  12.  
  13.        split := 分裂維的序號 
  14.  
  15. 3.exm_set_left = {exm∈exm_set – dom_elt && exm[split] <= dom_elt[split]} 
  16.  
  17.    exm_set_right = {exm∈exm_set – dom_elt && exm[split] > dom_elt[split]} 
  18.  
  19. 4.left = createKDTree(exm_set_left) 
  20.  
  21. right = createKDTree(exm_set_right) 

現在來解釋一下分裂結點選擇程序。分裂結點的選擇通常有多種方法,最常用的是一種方法是:對於所有的樣本點,統計它們在每個維上的方差,挑選出方差中的最大值,對應的維就是split域的值。數據方差最大表明沿該維度數據點分散得比較開,這個方向上進行數據分割可以獲得最好的分辨率;然後再將所有樣本點按其第split維的值進行排序,位於正中間的那個數據點選爲分裂結點的dom_elt域。

下面以一個簡單的例子來解釋上述k-d tree的構建過程。假設樣本集爲:{(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)}。構建過程如下:

(1)確定split域,6個數據點在x,y維度上的數據方差分別爲39, 28.63。在x軸上方差最大,所以split域值爲0(x維的序號爲0)

(2)確定分裂節點,根據x維上的值將數據排序,則6個數據點再排序後位於中間的那個數據點爲(7,2),該結點就是分割超平面就是通過(7,2)並垂直於split=0(x)軸的直線x=7

(3)左子空間和右子空間,分割超面x=7將整個空間氛圍兩部分,x<=7的部分爲左子空間,包含3個數據點{(2,3), (5,4), (4,7)};另一部分爲右子空間,包含2個數據點{(9,6), (8,1)}。如下圖所示

(4)分別對左子空間中的數據點和右子空間中的數據點重複上面的步驟構建左子樹和右子樹直到經過劃分的子樣本集爲空。下面的圖從左至右從上至下顯示了構建這棵二叉樹的所有步驟:

p_w_picpathp_w_picpathp_w_picpathp_w_picpath

k-d tree的最近鄰搜索算法

如前所述,在k-d tree樹中進行數據的k近鄰搜索是特徵匹配的重要環節,其目的是檢索在k-d tree中與待查詢點距離最近的k個數據點。

最近鄰搜索是k近鄰的特例,也就是1近鄰。將1近鄰改擴展到k近鄰非常容易。下面介紹最簡單的k-d tree最近鄰搜索算法。

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

  1. 算法:kdtreeFindNearest /* k-d tree的最近鄰搜索 */ 
  2.  
  3. 輸入:Kd /* k-d tree類型*/ 
  4.  
  5. target /* 待查詢數據點 */ 
  6.  
  7. 輸出 : nearest /* 最近鄰數據結點 */ 
  8.  
  9. dist /* 最近鄰和查詢點的距離 */ 
  10.  
  11. 1. 如果Kd是空的,則設dist爲無窮大返回 
  12.  
  13. 2. 向下搜索直到葉子結點 
  14.  
  15. pSearch = &Kd 
  16.  
  17. while(pSearch != NULL)  
  18. {  
  19. pSearch加入到search_path中;  
  20. if(target[pSearch->split] <= pSearch->dom_elt[pSearch->split]) /* 如果小於就進入左子樹 */  
  21. {  
  22. pSearch = pSearch->left;  
  23. }  
  24. else  
  25. {  
  26. pSearch = pSearch->right;  
  27. }  
  28. }  
  29. 取出search_path最後一個賦給nearest 
  30.  
  31. dist = Distance(nearest, target);  
  32. 3. 回溯搜索路徑 
  33.  
  34. while(search_path不爲空)  
  35. {  
  36. 取出search_path最後一個結點賦給pBack 
  37.  
  38. if(pBack->left爲空 && pBack->right爲空) /* 如果pBack爲葉子結點 */ 
  39.  
  40.  
  41. if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )  
  42. {  
  43. nearest = pBack->dom_elt;  
  44. dist = Distance(pBack->dom_elt, target);  
  45.  
  46.  
  47. else 
  48.  
  49.  
  50. s = pBack->split;  
  51. if( abs(pBack->dom_elt[s] - target[s]) < dist) /* 如果以target爲中心的圓(球或超球),半徑爲dist的圓與分割超平面相交, 那麼就要跳到另一邊的子空間去搜索 */  
  52. {  
  53. if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )  
  54. {  
  55. nearest = pBack->dom_elt;  
  56. dist = Distance(pBack->dom_elt, target);  
  57. }  
  58. if(target[s] <= pBack->dom_elt[s]) /* 如果target位於pBack的左子空間,那麼就要跳到右子空間去搜索 */  
  59. pSearch = pBack->right;  
  60. else  
  61. pSearch = pBack->left; /* 如果target位於pBack的右子空間,那麼就要跳到左子空間去搜索 */  
  62. if(pSearch != NULL)  
  63. pSearch加入到search_path中  
  64.  
  65. }  

OK,現在舉一些例子來說明上面的最近鄰搜索算法會比較直觀。

假設我們的k-d tree就是上面通過樣本集{(2,3), (5,4), (9,6), (4,7), (8,1), (7,2)}創建的。將上面的圖轉化爲樹形圖的樣子如下:

p_w_picpath

我們來查找點(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)的右子空間去搜索,因爲右子空間中不可能有更近樣本點了。

p_w_picpath

於是在回溯至(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。

p_w_picpath

回溯至(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。

 

兩次搜索的返回的最近鄰點雖然是一樣的,但是搜索(2, 4.5)的過程要複雜一些,因爲(2, 4.5)更接近超平面。研究表明,當查詢點的鄰域與分割超平面兩側的空間都產生交集時,回溯的次數大大增加。最壞的情況下搜索N個結點的k維kd-tree所花費的時間爲:

p_w_picpath

後記

到此爲止,k-d tree相關的基本知識就說完了。關於k-d tree還有很多擴展。由於大量回溯會導致kd-tree最近鄰搜索的性能大大下降,因此研究人員也提出了改進的k-d tree近鄰搜索,其中一個比較著名的就是 Best-Bin-First,它通過設置優先級隊列和運行超時限定來獲取近似的最近鄰,有效地減少回溯的次數。這裏就不詳細講了,如果想知道可以查詢後面的參考資料。

 

參考資料

1.An intoductory tutorial on kd-trees Andrew W.Moore

2.《圖像局部不變特性特徵與描述》王永明 王貴錦 編著 國防工業出版社

3.kdtree A simple C library for working with KD-Trees

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