轉載請註明出處:http://blog.csdn.net/luoshixian099/article/details/47606159
繼上一篇中已經介紹了SIFT原理與C源碼剖析,最後得到了一系列特徵點,每個特徵點對應一個128維向量。假如現在有兩副圖片都已經提取到特徵點,現在要做的就是匹配上相似的特徵點。
相似性查詢有兩種基本方式:1.範圍查詢:即給點查詢點和查詢閾值,從數據集中找出所有與查詢點距離小於閾值的點。
2.K近鄰查詢:給點查詢點及正整數K,從數據集中找到與查詢點最近的K個數據,當K=1時,就是最近鄰查詢。
特徵匹配算子可以分爲兩類:1.窮舉法:即將數據集中的點與查詢點逐一計算距離,如果圖1提取到N1個特徵點,圖2中提取到N2個特徵點,用窮舉法匹配,要做N1×N2運算,這種方法顯然效率低下。
2.建立數據索引:對數據進行分析,對搜索數據空間進行劃分,按劃分時是否有重疊,分爲KD樹和R樹。KD樹是對空間劃分時沒有重疊的一種。
一個三維k-d樹。第一次劃分(紅色)把根節點(白色)劃分成兩個節點,然後它們分別再次被劃分(綠色)爲兩個子節點。最後這四個子節點的每一個都被劃分(藍色)爲兩個子節點。因爲沒有更進一步的劃分,最後得到的八個節點稱爲葉子節點。
KD樹的構建:KD樹是一個二叉樹,對數據空間空間進行劃分,每一個結點對應一個空間範圍。如上圖所示,三維空間的劃分方式。首先確定在數據集上對應方差最大的維度ki,並找到在ki維度上的數據集的中值kv(並作爲根節點),即第一步把空間劃分成兩部分,在第ki維上小於kv的爲一部分稱爲左子節點,大於kv的爲另外一部分對應右子節點,,然後再利用同樣的方法,對左子結點和右子節點繼續構建二叉樹,直所剩數據集爲空。
舉個例子:有5個數據,每個數據都是5維,建立KD樹,A<7,5,7,3,8>;B<3,4,1,2,7>;C<5,2,6,6,9>;D<9,3,2,4,1>,E<2,1,5,1,4>,首先在計算在5個維度上的方差爲6.56;2;5.36;2.96;8.56;可見在第5維度上方差最大,繼續在第5個維度上找到中值爲7,即B點,在第5維度上值小於7的作爲左子樹數據(A,C),大於7的作爲右子樹(D,E),然後繼續在A,C,兩點上計算方差最大的維度,繼續劃分。D,E也是如此。如下圖,ki表示維度,kv表示該維度上的值。
KD樹的查詢:從根節點開始沿二叉樹搜索,直到葉子結點爲止,此時該葉節點並不一定是最近的點,但是一定是葉子結點附近的點。所以一定要有回溯的過程,回溯到根節點爲止。例如:查詢與M<5,4,1,3,6>點的最近鄰點,查詢路徑爲B,A,C,計算完MC的距離後,逆序向上,查詢A及A的右子樹,再次回溯B及B左子樹,最後得到最近的距離,MB點最近。
假如數據集是維數是D,一般來說要求數據的規模N需要滿足N>>2^D條件,才能達到高效的搜索,一般來說用標準的KD樹時數據集的維數不超過20,但是像SIFT特徵描述子128爲,SURF描述子爲64維,所以要對現有的KD樹進行改進。
BBF:上述回溯的過程,完全是按照查詢時路徑決定的,沒有考慮查詢路徑上的數據性質,BBF(Best-Bin-First)查詢機制能確保優先包含最近鄰點的空間,即BBF維護了一個優先隊列,每一次查詢到左子樹或右子樹的過程中,同時計算查詢點在該維度的中值的距離差保存在優先隊列裏,同時另一個孩子節點地址也存入隊列裏,回溯的過程即從優先隊列按(差值)從小到大的順序依次回溯。如上一個例子,首先把B保存在優先隊列裏,然後開始從優先隊列裏取數據,取出B,發現要到左孩子A節點裏繼續查詢,這時,要把右孩子節點D保存在優先隊列裏,同時加上距離屬性ki=5,kv=7,所以d=7-6=1,這時優先隊列裏簡記爲D(1);同理,如果A有右孩子,也要存入優先隊列,加上屬性ki=2,kv=5,d=5-4=1;(例子不太恰當,o(╯□╰)o),回溯的過程是按照優先隊列的距離逐個回溯,直到優先隊列爲空,或者超時,停止;BBF設置了超時機制,爲了在高維數據上,滿足檢索速度的需要以精度換取時間,獲得快速查詢。這樣可知,BBF機制找到的最近鄰是近似的,並非是最近的,只能說是離最近點比較近而已。超時機制在算法的實現上,限定了從優先隊列中提取數據的次數。
下面從算法上解析:
構建KD樹:
struct kd_node* kdtree_build( struct feature* features, int n )//features爲特徵帶你,n爲個數
{
struct kd_node* kd_root;
if( ! features || n <= 0 )
{
fprintf( stderr, "Warning: kdtree_build(): no features, %s, line %d\n",
__FILE__, __LINE__ );
return NULL;
}
kd_root = kd_node_init( features, n ); //建立根節點,每次建立一個節點存入一個特徵點
expand_kd_node_subtree( kd_root );//以根節點開始擴展KD樹
return kd_root;
}
static struct kd_node* kd_node_init( struct feature* features, int n )
{
struct kd_node* kd_node;
kd_node = malloc( sizeof( struct kd_node ) );
memset( kd_node, 0, sizeof( struct kd_node ) );
kd_node->ki = -1; //屬性ki初始化爲1
kd_node->features = features;//指向特徵點
kd_node->n = n; //節點屬性n保存以kd_node爲根的樹上總節點數
return kd_node;
}
擴展KD樹:以當前結點的最大方差的維數爲對應的中值爲基準,把所有數據分成左右子樹的結點數據,並以此遞歸下去,直到葉子結點的創建即返回。
static void expand_kd_node_subtree( struct kd_node* kd_node ) //遞歸法建立KD樹
{
/* base case: leaf node */
if( kd_node->n == 1 || kd_node->n == 0 ) //如果剩下一個節點,成爲葉子節點
{
kd_node->leaf = 1;
return;
}
assign_part_key( kd_node ); //計算最大方差的對應的維數,ki和kv
partition_features( kd_node );//按第ki維的數據大小分成左子樹數據和右子樹的數據
if( kd_node->kd_left ) //繼續構建左子樹
expand_kd_node_subtree( kd_node->kd_left );
if( kd_node->kd_right )//繼續構建右子樹
expand_kd_node_subtree( kd_node->kd_right );
}
計算最大方差對應的維數ki,與中值kv,取中值時,採用了最壞情況也是線性時間的選擇算法,我的博客之前寫過,這裏不再分析<中位數排序>點擊打開鏈接static void assign_part_key( struct kd_node* kd_node ) //計算節點數據的最大方差對應的維數ki,和中值kv
{
struct feature* features;
double kv, x, mean, var, var_max = 0;
double* tmp;
int d, n, i, j, ki = 0;
features = kd_node->features;
n = kd_node->n;
d = features[0].d;
/* partition key index is that along which descriptors have most variance */
for( j = 0; j < d; j++ ) //計算d維數據上,所有維數上的方差。
{
mean = var = 0;
for( i = 0; i < n; i++ )
mean += features[i].descr[j];
mean /= n;
for( i = 0; i < n; i++ )
{
x = features[i].descr[j] - mean;
var += x * x;
}
var /= n; //計算第j維的數據的方差
if( var > var_max )
{
ki = j;
var_max = var;
}
}
/* partition key value is median of descriptor values at ki */
tmp = calloc( n, sizeof( double ) );
for( i = 0; i < n; i++ ) //取得所有數據上第ki維上的數據
tmp[i] = features[i].descr[ki];
kv = median_select( tmp, n ); //找到第ki維度上中間的值,這裏採用了最壞情況運行時間O(n)的選擇算法
free( tmp );
kd_node->ki = ki; //維度
kd_node->kv = kv; //中間值
}
按ki維上kv值,把特徵點排序,小於等於kv爲作爲左子樹數據,大於kv作爲右子樹數據static void partition_features( struct kd_node* kd_node )
{
struct feature* features, tmp;
double kv;
int n, ki, p, i, j = -1;
features = kd_node->features;
n = kd_node->n;
ki = kd_node->ki;
kv = kd_node->kv;
for( i = 0; i < n; i++ ) //對特徵點按第ki維數據大小排序
if( features[i].descr[ki] <= kv )
{
tmp = features[++j];
features[j] = features[i];
features[i] = tmp;
if( features[j].descr[ki] == kv )
p = j;
}
tmp = features[p];
features[p] = features[j];
features[j] = tmp;
/* if all records fall on same side of partition, make node a leaf */
if( j == n - 1 ) //說明只剩一個節點,標記爲葉子節點
{
kd_node->leaf = 1;
return;
}
kd_node->kd_left = kd_node_init( features, j + 1 );//創建左子樹,裏面有j+1個結點
kd_node->kd_right = kd_node_init( features + ( j + 1 ), ( n - j - 1 ) );//創建右子樹,裏面有n-j-1個結點
}
☆KD樹已經創建完畢,現在要做的是查詢,查詢與特徵點最近鄰的K個特徵點,首先把根節點插入到優先隊列,然後開始從有優先隊列中取元素,遍歷到葉節點,同時路徑過程中,未查詢的另一個結點的加入優先隊列(按ki維上的數值與kv的差值的絕對值大小),然後再次從優先隊列中取結點,再次遍歷到葉節點,如此反覆...直到遇到超時限制,或者遍歷完所有節點爲止。
/*
kd_root爲創建好的KD樹,feat爲要查詢的特徵點
k爲要找到的近鄰節點數,SIFT中選取2
nbrs存儲查詢到的k個近鄰數據
max_nn_chkes爲最大提取隊列次數,即超時限制
成功返回找到的近鄰數據個數,否則返回-1
*/
int kdtree_bbf_knn( struct kd_node* kd_root, struct feature* feat, int k,
struct feature*** nbrs, int max_nn_chks )
{
struct kd_node* expl;
struct min_pq* min_pq;
struct feature* tree_feat, ** _nbrs;
struct bbf_data* bbf_data;
int i, t = 0, n = 0;
if( ! nbrs || ! feat || ! kd_root )
{
fprintf( stderr, "Warning: NULL pointer error, %s, line %d\n",
__FILE__, __LINE__ );
return -1;
}
_nbrs = calloc( k, sizeof( struct feature* ) );
min_pq = minpq_init(); //創建一個最小優先隊列
minpq_insert( min_pq, kd_root, 0 ); //在優先隊列在插入第一個根元素
while( min_pq->n > 0 && t < max_nn_chks ) //如果隊列不爲空且在超時次數內
{
expl = (struct kd_node*)minpq_extract_min( min_pq );//在優先隊列中取出一個元素
if( ! expl )
{
fprintf( stderr, "Warning: PQ unexpectedly empty, %s line %d\n",
__FILE__, __LINE__ );
goto fail;
}
expl = explore_to_leaf( expl, feat, min_pq );// 找到特徵點在KD樹葉子節點位置,過程中未查詢的加入優先隊列
if( ! expl )
{
fprintf( stderr, "Warning: PQ unexpectedly empty, %s line %d\n",
__FILE__, __LINE__ );
goto fail;
}
for( i = 0; i < expl->n; i++ ) //遍歷以expl爲根的子樹所有節點
{
//printf("%x",expl->features[i].feature_data);
tree_feat = &expl->features[i];
bbf_data = malloc( sizeof( struct bbf_data ) );
if( ! bbf_data )
{
fprintf( stderr, "Warning: unable to allocate memory,"
" %s line %d\n", __FILE__, __LINE__ );
goto fail;
}
//bbf_data->old_data 這個數據沒有用途,因爲特徵點屬性中沒有使用到feature_data這個自定義類型
bbf_data->old_data = tree_feat->feature_data;
printf("%x",bbf_data->old_data);
bbf_data->d = descr_dist_sq(feat, tree_feat); //計算兩特徵點的歐式距離
tree_feat->feature_data = bbf_data;
n += insert_into_nbr_array( tree_feat, _nbrs, n, k ); //找到K個近鄰的特徵點,存入數組_nbrs中,從小到大的距離;
}
t++;
}
minpq_release( &min_pq );
for( i = 0; i < n; i++ )
{
bbf_data = _nbrs[i]->feature_data;
_nbrs[i]->feature_data = bbf_data->old_data;
free( bbf_data );
}
*nbrs = _nbrs;
return n;
fail:
minpq_release( &min_pq );
for( i = 0; i < n; i++ )
{
bbf_data = _nbrs[i]->feature_data;
_nbrs[i]->feature_data = bbf_data->old_data;
free( bbf_data );
}
free( _nbrs );
*nbrs = NULL;
return -1;
}
這裏創建優先隊列採用了堆排序的思想,堆排序一個重要的應用就是優先隊列,在一個包含n個元素的堆中,所有優先隊列的操作都可以在lgn時間內完成。優先隊列也有兩種形式,最大優先隊列和最小優先隊列,這裏使用的是最小優先隊列,即key值越小優先級越高,關於堆排序的原理,可以看之前的堆排序的算法<堆排序>點擊打開鏈接。struct min_pq* minpq_init() //隊列初始化
{
struct min_pq* min_pq;
min_pq = malloc( sizeof( struct min_pq ) );
min_pq->pq_array = calloc( MINPQ_INIT_NALLOCD, sizeof( struct pq_node ) );//分配隊列的空間
min_pq->nallocd = MINPQ_INIT_NALLOCD;
min_pq->n = 0;//隊列中元素的個數
return min_pq;
}
int minpq_insert( struct min_pq* min_pq, void* data, int key )//向優先隊列中插入元素
{
int n = min_pq->n;
/* double array allocation if necessary */
if( min_pq->nallocd == n )
{
min_pq->nallocd = array_double( (void**)&min_pq->pq_array,
min_pq->nallocd,
sizeof( struct pq_node ) );
if( ! min_pq->nallocd )
{
fprintf( stderr, "Warning: unable to allocate memory, %s, line %d\n",
__FILE__, __LINE__ );
return 1;
}
}
min_pq->pq_array[n].data = data;
min_pq->pq_array[n].key = INT_MAX;
decrease_pq_node_key( min_pq->pq_array, min_pq->n, key ); //插入元素到優先隊列中,堆排序算法
min_pq->n++;
return 0;
}
從隊列中取出一個節點,沿着結點遍歷到葉節點爲止,同時未查詢的加入優先隊列。
static struct kd_node* explore_to_leaf( struct kd_node* kd_node, //從kd_node開始開始查詢直到葉節點爲止
struct feature* feat,
struct min_pq* min_pq )
{
struct kd_node* unexpl, * expl = kd_node;
double kv;
int ki;
while( expl && ! expl->leaf )
{
ki = expl->ki;
kv = expl->kv;
if( ki >= feat->d )
{
fprintf( stderr, "Warning: comparing imcompatible descriptors, %s" \
" line %d\n", __FILE__, __LINE__ );
return NULL;
}
if( feat->descr[ki] <= kv )
{
unexpl = expl->kd_right;
expl = expl->kd_left;
}
else
{
unexpl = expl->kd_left;
expl = expl->kd_right;
}
if( minpq_insert( min_pq, unexpl, ABS( kv - feat->descr[ki] ) ) ) //未查詢到的結點,按差值大小加入優先隊列
{
fprintf( stderr, "Warning: unable to insert into PQ, %s, line %d\n",
__FILE__, __LINE__ );
return NULL;
}
}
return expl;
}
計算兩特徵點的歐式距離:
double descr_dist_sq( struct feature* f1, struct feature* f2 )
{
double diff, dsq = 0;
double* descr1, * descr2;
int i, d;
d = f1->d;
if( f2->d != d )
return DBL_MAX;
descr1 = f1->descr;
descr2 = f2->descr;
for( i = 0; i < d; i++ )
{
diff = descr1[i] - descr2[i];
dsq += diff*diff;
}
return dsq;
}
找到的特徵點距離插入nbrs隊列,作爲輸出。nbrs也是按照從小到大的順序存儲距離,
假如要插入的新的距離爲D,隊列此時最後一個元素d
1.如果隊列未滿的情況(即要取得K近鄰還沒有找到K個)(1) D>=d,直接插入到隊列後面;(2) D<d,則找到要插入的位置,然後比D大的元素都往後移動一位;
2.如果隊列已經滿了:(1)D>=d,直接丟棄當前的距離;(2)D<d,則找到要插入的位置,然後比D大的元素都往後移動一位;最後一個元素丟棄。
static int insert_into_nbr_array( struct feature* feat, struct feature** nbrs,
int n, int k )
{
struct bbf_data* fdata, * ndata;
double dn, df;
int i, ret = 0;
if( n == 0 )
{
nbrs[0] = feat;
return 1;
}
/* check at end of array */
fdata = (struct bbf_data*)feat->feature_data; //判斷要插入的位置
df = fdata->d;
ndata = (struct bbf_data*)nbrs[n-1]->feature_data;
dn = ndata->d;
if( df >= dn ) //準備插入到最後
{
if( n == k ) //但K近鄰隊列已滿,捨棄
{
feat->feature_data = fdata->old_data; //捨棄掉前,再次保留之前自定義的數據
free( fdata );
return 0;
}
nbrs[n] = feat;
return 1;
}
/* find the right place in the array插入到隊列中間,分爲隊列滿或不滿的情況 */
if( n < k ) //K近鄰隊列沒滿,元素向後平移
{
nbrs[n] = nbrs[n-1];
ret = 1;
}
else
{
nbrs[n-1]->feature_data = ndata->old_data;//隊列已滿,最後一個要捨棄,恢復之前的數據
free( ndata );
}
i = n-2;
while( i >= 0 ) //元素逐次向後平移找到,隊列中適當的位置;
{
ndata = (struct bbf_data*)nbrs[i]->feature_data;
dn = ndata->d;
if( dn <= df )
break;
nbrs[i+1] = nbrs[i];
i--;
}
i++;
nbrs[i] = feat; //插入元素
return ret;
}
至此,關於SIFT原理以及特徵點匹配的算法已介紹完畢,後續文章將陸續更新surf,brife,fast,ORB等一系列關於特徵匹配的文章,再次感謝CSDN上的大牛們!