By RaySaint 2011/10/12算法
動機sql
先前寫了一篇文章《SIFT算法研究》講了講SIFT特徵具體是如何檢測和描述的,其中也提到了SIFT常見的一個用途就是物體識別,物體識別的過程以下圖所示:數據結構
如上圖(a),咱們先對待識別的物體的圖像進行SIFT特徵點的檢測和特徵點的描述,而後獲得了SIFT特徵點集合。接下來生成物體目標描述要作的就是對特徵點集合進行數據組織,造成一種特殊的表示,其做用是爲了加速特徵點匹配的過程。所謂的特徵點匹配本質上是一個經過距離函數(例如歐式距離)在高維矢量之間進行類似性檢索的問題,簡單來說就是範圍查詢或者K近鄰查詢的問題。dom
範圍查詢就是給定查詢點和查詢距離閾值,從數據集中找出全部與查詢點距離小於查詢距離閾值的數據;K近鄰查詢就是給定查詢點和正整數K,從數據集中找到距離查詢點最近的K個數據,當K=1時,它就是最近鄰查詢。ide
如上圖(b)咱們從輸入圖像中進行SIFT特徵點的檢測和特徵點的描述後,獲得了一個待查詢點的集合,接下來就是要找出集合中的每個待查詢點在(a)過程獲得的目標物體的特徵點集合中進行2近鄰查詢(即獲得最近鄰和次近鄰),獲得一組特徵點的匹配對<待查詢點,待查詢點的最近鄰>;獲得全部匹配對後,而後經過閾值法(與最近鄰的距離要小於一個常數)和比值法(與最近鄰的距離比次近鄰的距離要小於一個常數)進行提純,濾去較差的匹配對。獲得最終的匹配對集合。最後在計算單應性矩陣時,使用RANSAC算法再進行一次提純,剔除錯誤的匹配對。關於RANSAC算法,我還會再寫一篇文章講一講。David G.Lowe的論文說3個或以上的特徵點匹配對能夠確認一個正確的識別。(由於單應性矩陣的計算最少得使用4個點,而且可能會有錯誤匹配的狀況存在,因此最好須要多一點的特徵點匹配對)函數
本文的主要目的是講一下如何建立k-d tree對目標物體的特徵點集合進行數據組織和使用k-d tree最近鄰搜索來加速特徵點匹配。上面已經講了特徵點匹配的問題其實上是一個最近鄰(K近鄰)搜索的問題。因此爲了更好的引出k-d tree,先講一講最近鄰搜索。性能
最近鄰搜索測試
先給出一個最近鄰的數學形式的定義。給定一個多維空間,把中的一個向量成爲一個樣本點或數據點。中樣本點的有限集合稱爲樣本集。給定樣本集E,和一個樣本點d,d的最近鄰就是任何樣本點d’∈E知足None-nearer(E,d,d’)。google
None-nearer以下定義:spa
上面的公式中距離度量是歐式距離,固然也能夠是任何其餘Lp-norm。
其中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),以下圖所示:
(紅線表明分割超平面)
因而其餘數據點的x維(第split=0維)若是小於7,則被分配到左子空間;若大於7,則被分配到右子空間。例如,(5,4)被分配到左子空間,(9,6)被分配到右子空間。以下圖所示:
從上面的表也能夠看出k-d tree本質上是一種二叉樹,所以k-d tree的構建是一個逐級展開的遞歸過程。
其算法的僞代碼以下:
- 算法:createKDTree 構建一棵k-d tree
- 輸入:exm_set 樣本集
- 輸出 : Kd, 類型爲kd-tree
- 1. 若是exm_set是空的,則返回空的kd-tree
- 2.調用分裂結點選擇程序(輸入是exm_set),返回兩個值
- dom_elt:= exm_set中的一個樣本點
- split := 分裂維的序號
- 3.exm_set_left = {exm∈exm_set – dom_elt && exm[split] <= dom_elt[split]}
- exm_set_right = {exm∈exm_set – dom_elt && exm[split] > dom_elt[split]}
- 4.left = createKDTree(exm_set_left)
- 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)分別對左子空間中的數據點和右子空間中的數據點重複上面的步驟構建左子樹和右子樹直到通過劃分的子樣本集爲空。下面的圖從左至右從上至下顯示了構建這棵二叉樹的全部步驟:
k-d tree的最近鄰搜索算法
如前所述,在k-d tree樹中進行數據的k近鄰搜索是特徵匹配的重要環節,其目的是檢索在k-d tree中與待查詢點距離最近的k個數據點。
最近鄰搜索是k近鄰的特例,也就是1近鄰。將1近鄰改擴展到k近鄰很是容易。下面介紹最簡單的k-d tree最近鄰搜索算法。
基本的思路很簡單:首先經過二叉樹搜索(比較待查詢節點和分裂節點的分裂維的值,小於等於就進入左子樹分支,等於就進入右子樹分支直到葉子結點),順着「搜索路徑」很快能找到最近鄰的近似點,也就是與待查詢點處於同一個子空間的葉子結點;而後再回溯搜索路徑,並判斷搜索路徑上的結點的其餘子結點空間中是否可能有距離查詢點更近的數據點,若是有可能,則須要跳到其餘子結點空間中去搜索(將其餘子結點加入到搜索路徑)。重複這個過程直到搜索路徑爲空。下面給出k-d tree最近鄰搜索的僞代碼:
- 算法:kdtreeFindNearest /* k-d tree的最近鄰搜索 */
- 輸入:Kd /* k-d tree類型*/
- target /* 待查詢數據點 */
- 輸出 : nearest /* 最近鄰數據結點 */
- dist /* 最近鄰和查詢點的距離 */
- 1. 若是Kd是空的,則設dist爲無窮大返回
- 2. 向下搜索直到葉子結點
- pSearch = &Kd
- while(pSearch != NULL)
- {
- pSearch加入到search_path中;
- if(target[pSearch->split] <= pSearch->dom_elt[pSearch->split]) /* 若是小於就進入左子樹 */
- {
- pSearch = pSearch->left;
- }
- else
- {
- pSearch = pSearch->right;
- }
- }
- 取出search_path最後一個賦給nearest
- dist = Distance(nearest, target);
- 3. 回溯搜索路徑
- while(search_path不爲空)
- {
- 取出search_path最後一個結點賦給pBack
- if(pBack->left爲空 && pBack->right爲空) /* 若是pBack爲葉子結點 */
- {
- if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )
- {
- nearest = pBack->dom_elt;
- dist = Distance(pBack->dom_elt, target);
- }
- }
- else
- {
- s = pBack->split;
- if( abs(pBack->dom_elt[s] - target[s]) < dist) /* 若是以target爲中心的圓(球或超球),半徑爲dist的圓與分割超平面相交, 那麼就要跳到另外一邊的子空間去搜索 */
- {
- if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )
- {
- nearest = pBack->dom_elt;
- dist = Distance(pBack->dom_elt, target);
- }
- if(target[s] <= pBack->dom_elt[s]) /* 若是target位於pBack的左子空間,那麼就要跳到右子空間去搜索 */
- pSearch = pBack->right;
- else
- pSearch = pBack->left; /* 若是target位於pBack的右子空間,那麼就要跳到左子空間去搜索 */
- if(pSearch != NULL)
- pSearch加入到search_path中
- }
- }
- }
OK,如今舉一些例子來講明上面的最近鄰搜索算法會比較直觀。
假設咱們的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。
兩次搜索的返回的最近鄰點雖然是同樣的,可是搜索(2, 4.5)的過程要複雜一些,由於(2, 4.5)更接近超平面。研究代表,當查詢點的鄰域與分割超平面兩側的空間都產生交集時,回溯的次數大大增長。最壞的狀況下搜索N個結點的k維kd-tree所花費的時間爲:
後記
到此爲止,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