【特徵匹配】SIFT原理之KD樹+BBF算法解析

   轉載請註明出處: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上的大牛們!



發佈了57 篇原創文章 · 獲贊 268 · 訪問量 60萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章