KD tree and Bbf

k-d tree算法

   k-d树(k-dimensional树的简称),是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索(如:范围搜索和最近邻搜索)。

应用背景

  SIFT算法中做特征点匹配的时候就会利用到k-d树。而特征点匹配实际上就是一个通过距离函数在高维矢量之间进行相似性检索的问题。针对如何快速而准确地找到查询点的近邻,现在提出了很多高维空间索引结构和近似查询的算法,k-d树就是其中一种。

  索引结构中相似性查询有两种基本的方式:一种是范围查询(range searches),另一种是K近邻查询(K-neighbor searches)。范围查询就是给定查询点和查询距离的阈值,从数据集中找出所有与查询点距离小于阈值的数据;K近邻查询是给定查询点及正整数K,从数据集中找到距离查询点最近的K个数据,当K=1时,就是最近邻查询(nearest neighbor searches)。

  特征匹配算子大致可以分为两类。一类是线性扫描法,即将数据集中的点与查询点逐一进行距离比较,也就是穷举,缺点很明显,就是没有利用数据集本身蕴含的任何结构信息,搜索效率较低,第二类是建立数据索引,然后再进行快速匹配。因为实际数据一般都会呈现出簇状的聚类形态,通过设计有效的索引结构可以大大加快检索的速度。索引树属于第二类,其基本思想就是对搜索空间进行层次划分。根据划分的空间是否有混叠可以分为Clipping和Overlapping两种。前者划分空间没有重叠,其代表就是k-d树;后者划分空间相互有交叠,其代表为R树。(这里只介绍k-d树)

实例

  先以一个简单直观的实例来介绍k-d树算法。假设有6个二维数据点{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)},数据点位于二维空间内(如图1中黑点所示)。k-d树算法就是要确定图1中这些分割空间的分割线(多维空间即为分割平面,一般为超平面)。下面就要通过一步步展示k-d树是如何确定这些分割线的。

图1  二维数据k-d树空间划分示意图

  k-d树算法可以分为两大部分,一部分是有关k-d树本身这种数据结构建立的算法,另一部分是在建立的k-d树上如何进行最邻近查找的算法。

k-d树构建算法

  k-d树是一个二叉树,每个节点表示一个空间范围。表1给出的是k-d树每个节点中主要包含的数据结构。

表1  k-d树中每个节点的数据类型

域名 数据类型 描述
Node-data 数据矢量 数据集中某个数据点,是n维矢量(这里也就是k维)
Range 空间矢量 该节点所代表的空间范围
split 整数 垂直于分割超平面的方向轴序号
Left k-d树 由位于该节点分割超平面左子空间内所有数据点所构成的k-d树
Right k-d树 由位于该节点分割超平面右子空间内所有数据点所构成的k-d树
parent k-d树 父节点

  从上面对k-d树节点的数据类型的描述可以看出构建k-d树是一个逐级展开的递归过程。表2给出的是构建k-d树的伪码。

表2  构建k-d树的伪码

算法:构建k-d树(createKDTree)
输入:数据点集Data-set和其所在的空间Range
输出:Kd,类型为k-d tree
1.If Data-set为空,则返回空的k-d tree

2.调用节点生成程序:

  (1)确定split域:对于所有描述子数据(特征矢量),统计它们在每个维上的数据方差。以SURF特征为例,描述子为64维,可计算64个方差。挑选出最大值,对应的维就是split域的值。数据方差大表明沿该坐标轴方向上的数据分散得比较开,在这个方向上进行数据分割有较好的分辨率;

  (2)确定Node-data域:数据点集Data-set按其第split域的值排序。位于正中间的那个数据点被选为Node-data。此时新的Data-set' = Data-set\Node-data(除去其中Node-data这一点)。

3.dataleft = {d属于Data-set' && d[split] ≤ Node-data[split]}

   Left_Range = {Range && dataleft}

   dataright = {d属于Data-set' && d[split] > Node-data[split]}

   Right_Range = {Range && dataright}

4.left = 由(dataleft,Left_Range)建立的k-d tree,即递归调用createKDTree(dataleft,Left_

   Range。并设置left的parent域为Kd;

   right = 由(dataright,Right_Range)建立的k-d tree,即调用createKDTree(dataleft,Left_

   Range)。并设置right的parent域为Kd

  以上述举的实例来看,过程如下:

  由于此例简单,数据维度只有2维,所以可以简单地给x,y两个方向轴编号为0,1,也即split={0,1}。

  (1)确定split域的首先该取的值。分别计算x,y方向上数据的方差得知x方向上的方差最大,所以split域值首先取0,也就是x轴方向;

  (2)确定Node-data的域值。根据x轴方向的值2,5,9,4,8,7排序选出中值为7,所以Node-data = (7,2)。这样,该节点的分割超平面就是通过(7,2)并垂直于split = 0(x轴)的直线x = 7;

  (3)确定左子空间和右子空间。分割超平面x = 7将整个空间分为两部分,如图2所示。x < =  7的部分为左子空间,包含3个节点{(2,3),(5,4),(4,7)};另一部分为右子空间,包含2个节点{(9,6),(8,1)}。

图2  x=7将整个空间分为两部分

  如算法所述,k-d树的构建是一个递归的过程。然后对左子空间和右子空间内的数据重复根节点的过程就可以得到下一级子节点(5,4)和(9,6)(也就是左右子空间的'根'节点),同时将空间和数据集进一步细分。如此反复直到空间中只包含一个数据点,如图1所示。最后生成的k-d树如图3所示。

图3  上述实例生成的k-d树

  注意:每一级节点旁边的'x'和'y'表示以该节点分割左右子空间时split所取的值。

k-d树上的最邻近查找算法

  在k-d树中进行数据的查找也是特征匹配的重要环节,其目的是检索在k-d树中与查询点距离最近的数据点。这里先以一个简单的实例来描述最邻近查找的基本思路。

  星号表示要查询的点(2.1,3.1)。通过二叉搜索,顺着搜索路径很快就能找到最邻近的近似点,也就是叶子节点(2,3)。而找到的叶子节点并不一定就是最邻近的,最邻近肯定距离查询点更近,应该位于以查询点为圆心且通过叶子节点的圆域内。为了找到真正的最近邻,还需要进行'回溯'操作:算法沿搜索路径反向查找是否有距离查询点更近的数据点。此例中先从(7,2)点开始进行二叉查找,然后到达(5,4),最后到达(2,3),此时搜索路径中的节点为<(7,2),(5,4),(2,3)>,首先以(2,3)作为当前最近邻点,计算其到查询点(2.1,3.1)的距离为0.1414,然后回溯到其父节点(5,4),并判断在该父节点的其他子节点空间中是否有距离查询点更近的数据点。以(2.1,3.1)为圆心,以0.1414为半径画圆,如图4所示。发现该圆并不和超平面y = 4交割,因此不用进入(5,4)节点右子空间中去搜索。

图4  查找(2.1,3.1)点的两次回溯判断

  再回溯到(7,2),以(2.1,3.1)为圆心,以0.1414为半径的圆更不会与x = 7超平面交割,因此不用进入(7,2)右子空间进行查找。至此,搜索路径中的节点已经全部回溯完,结束整个搜索,返回最近邻点(2,3),最近距离为0.1414。

  一个复杂点了例子如查找点为(2,4.5)。同样先进行二叉查找,先从(7,2)查找到(5,4)节点,在进行查找时是由y = 4为分割超平面的,由于查找点为y值为4.5,因此进入右子空间查找到(4,7),形成搜索路径<(7,2),(5,4),(4,7)>,取(4,7)为当前最近邻点,计算其与目标查找点的距离为3.202。然后回溯到(5,4),计算其与查找点之间的距离为3.041。以(2,4.5)为圆心,以3.041为半径作圆,如图5所示。可见该圆和y = 4超平面交割,所以需要进入(5,4)左子空间进行查找。此时需将(2,3)节点加入搜索路径中得<(7,2),(2,3)>。回溯至(2,3)叶子节点,(2,3)距离(2,4.5)比(5,4)要近,所以最近邻点更新为(2,3),最近距离更新为1.5。回溯至(7,2),以(2,4.5)为圆心1.5为半径作圆,并不和x = 7分割超平面交割,如图6所示。至此,搜索路径回溯完。返回最近邻点(2,3),最近距离1.5。k-d树查询算法的伪代码如表3所示。

图5  查找(2,4.5)点的第一次回溯判断

图6  查找(2,4.5)点的第二次回溯判断

 

表3  标准k-d树查询算法

算法:k-d树最邻近查找

输入:Kd,    //k-d tree类型

     target  //查询数据点

输出:nearest, //最邻近数据点

     dist      //最邻近数据点和查询点间的距离

1. If Kd为NULL,则设dist为infinite并返回

2. //进行二叉查找,生成搜索路径

   Kd_point = &Kd;                   //Kd-point中保存k-d tree根节点地址

   nearest = Kd_point -> Node-data;  //初始化最近邻点

   while(Kd_point)

     push(Kd_point)到search_path中; //search_path是一个堆栈结构,存储着搜索路径节点指针

 /*** If Dist(nearest,target) > Dist(Kd_point -> Node-data,target)

       nearest  = Kd_point -> Node-data;    //更新最近邻点

       Max_dist = Dist(Kd_point,target);  //更新最近邻点与查询点间的距离  ***/

     s = Kd_point -> split;                       //确定待分割的方向

     If target[s] <= Kd_point -> Node-data[s]     //进行二叉查找

       Kd_point = Kd_point -> left;

     else

       Kd_point = Kd_point ->right;

   nearest = search_path中最后一个叶子节点; //注意:二叉搜索时不比计算选择搜索路径中的最邻近点,这部分已被注释

   Max_dist = Dist(nearest,target);    //直接取最后叶子节点作为回溯前的初始最近邻点

3. //回溯查找

   while(search_path != NULL)

     back_point = 从search_path取出一个节点指针;   //从search_path堆栈弹栈

     s = back_point -> split;                   //确定分割方向

     If Dist(target[s],back_point -> Node-data[s]) < Max_dist   //判断还需进入的子空间

       If target[s] <= back_point -> Node-data[s]

         Kd_point = back_point -> right;  //如果target位于左子空间,就应进入右子空间

       else

         Kd_point = back_point -> left;    //如果target位于右子空间,就应进入左子空间

       将Kd_point压入search_path堆栈;

     If Dist(nearest,target) > Dist(Kd_Point -> Node-data,target)

       nearest  = Kd_point -> Node-data;                 //更新最近邻点

       Min_dist = Dist(Kd_point -> Node-data,target);  //更新最近邻点与查询点间的距离

  上述两次实例表明,当查询点的邻域与分割超平面两侧空间交割时,需要查找另一侧子空间,导致检索过程复杂,效率下降。研究表明N个节点的K维k-d树搜索过程时间复杂度为:tworst=O(kN1-1/k)。

后记

  以上为了介绍方便,讨论的是二维情形。像实际的应用中,如SIFT特征矢量128维,SURF特征矢量64维,维度都比较大,直接利用k-d树快速检索(维数不超过20)的性能急剧下降。假设数据集的维数为D,一般来说要求数据的规模N满足N»2D,才能达到高效的搜索。所以这就引出了一系列对k-d树算法的改进。有待进一步研究学习。

参考

1.《图像局部不变特性特征与描述》王永明 王贵锦 编著 国防工业出版社

2.http://underthehood.blog.51cto.com/2531780/687160

转载请注明:http://www.cnblogs.com/eyeszjwang/articles/2429382.html 

分类:  算法

k-d tree代码解析

  上一篇较详细地介绍了k-d树算法。本文来讲解具体的实现代码。

  首先是一些数据结构的定义。我们先来定义单个数据,代码如下:

复制代码
//单个数据向量结构定义
struct _Examplar
{
public:

_Examplar():dom_dims(0){} //数据维度初始化为0
  //带有完整的两个参数的constructor,这里const是为了保护原数据不被修改
_Examplar(const std::vector<double> elt, int dims)
{
if(dims > 0)
{
dom_elt = elt;
dom_dims = dims;
}
else
{
dom_dims = 0;
}
}
复制代码
        _Examplar(int dims)    //只含有维度信息的constructor
{
if(dims > 0)
{
dom_elt.resize(dims);
dom_dims = dims;
}
else
{
dom_dims = 0;
}
}
_Examplar(const _Examplar& rhs) //copy-constructor
{
if(rhs.dom_dims > 0)
{
dom_elt = rhs.dom_elt;
dom_dims = rhs.dom_dims;
}
else
{
dom_dims = 0;
}
}
_Examplar& operator=(const _Examplar& rhs) //重载"="运算符
{
if(this == &rhs)
return *this;

releaseExamplarMem();

if(rhs.dom_dims > 0)
{
dom_elt = rhs.dom_elt;
dom_dims = rhs.dom_dims;
}

return *this;
}
~_Examplar()
{
}
double& dataAt(int dim) //定义访问控制函数
{
assert(dim < dom_dims);
return dom_elt[dim];
}
double& operator[](int dim) //重载"[]"运算符,实现下标访问
{
return dataAt(dim);
}
const double& dataAt(int dim) const //定义只读访问函数
{
assert(dim < dom_dims);
return dom_elt[dim];
}
const double& operator[](int dim) const //重载"[]"运算符,实现下标只读访问
{
return dataAt(dim);
}
void create(int dims) //创建数据向量
{
releaseExamplarMem();
if(dims > 0)
{
dom_elt.resize(dims); //控制数据向量维度
dom_dims = dims;
}
}
int getDomDims() const //获得数据向量维度信息
{
return dom_dims;
}
void setTo(double val) //数据向量初始化设置
{
if(dom_dims > 0)
{
for(int i=0;i<dom_dims;i++)
{
dom_elt[i] = val;
}
}
}
private:
void releaseExamplarMem() //清除现有数据向量
{
dom_elt.clear();
dom_dims = 0;
}
复制代码
private:
std::vector<double> dom_elt; //每个数据定义为一个double类型的向量
int dom_dims; //数据向量的维度
};
复制代码

  结构_Examplar定义了单个数据节点的结构,主要包含的信息有:1.数据向量本身;2.数据向量的维度。接下来定义一整个数据集的结构,代码如下:

复制代码
//数据集结构定义
class ExamplarSet : public TrainData //整个数据集类,由一个抽象类TrainData派生
{
private:
//_Examplar *_ex_set;
std::vector<_Examplar> _ex_set; //定义含有若干个_Examplar类数据向量的数据集
int _size; //数据集大小
int _dims; //数据集中每个数据向量的维度
public:
复制代码
        ExamplarSet():_size(0), _dims(0){}
ExamplarSet(std::vector<_Examplar> ex_set, int size, int dims);
ExamplarSet(int size, int dims);
ExamplarSet(const ExamplarSet& rhs);
ExamplarSet& operator=(const ExamplarSet& rhs);
~ExamplarSet(){}

_Examplar& examplarAt(int idx)
{
assert(idx < _size);
return _ex_set[idx];
}
_Examplar& operator[](int idx)
{
return examplarAt(idx);
}
const _Examplar& examplarAt(int idx) const
{
assert(idx < _size);
return _ex_set[idx];
}
void create(int size, int dims);
int getDims() const { return _dims;}
int getSize() const { return _size;}
_HyperRectangle calculateRange();
bool empty() const
{
return (_size == 0);
}
复制代码
    void sortByDim(int dim);     //按某个方向维的排序函数
bool remove(int idx); //去除数据集中排序后指定位置的数据向量
void push_back(const _Examplar& ex) //添加某个数据向量至数据集末尾
{
_ex_set.push_back(ex);
_size++;
}
int readData(char *strFilePath); //从文件读取数据集
private:
void releaseExamplarSetMem() //清除现有数据集
{
_ex_set.clear();
_size = 0;
}
};
复制代码

  类ExamplarSet定义了整个数据集的结构,其包含的主要信息有:1.含有若干个_Examplar类数据向量的数据集;2.数据集的大小;3.每个数据向量的维度。以上两个结构是整个算法两个基本的数据结构,这里的代码只是展示其主要包含的结构信息,详细的定义及函数实现代码请参看附件。

  接下来就要定义k-d tree的结构。同样采用上述由点定义到集定义的思路,我们先来定义k-d tree中一个节点结构,代码如下:

复制代码
//k-d tree节点结构定义
class KDTreeNode
{
private:
int _split_dim; //该节点的最大区分度方向维
_Examplar _dom_elt; //该节点的数据向量
_HyperRectangle _range_hr; //表示数据范围的超矩形结构
public:
KDTreeNode *_left_child, *_right_child, *_parent; //该节点的左右子树和父节点
复制代码
public:
KDTreeNode():_left_child(0), _right_child(0), _parent(0),
_split_dim(0){}
KDTreeNode(KDTreeNode *left_child, KDTreeNode *right_child,
KDTreeNode *parent, int split_dim, _Examplar dom_elt, _HyperRectangle range_hr):
_left_child(left_child), _right_child(right_child), _parent(parent),
_split_dim(split_dim), _dom_elt(dom_elt), _range_hr(range_hr){}
KDTreeNode(const KDTreeNode &rhs);
KDTreeNode& operator=(const KDTreeNode &rhs);
_Examplar& getDomElt() { return _dom_elt; }
_HyperRectangle& getHyperRectangle(){ return _range_hr; }
int& splitDim(){ return _split_dim; }
void create(KDTreeNode *left_child, KDTreeNode *right_child,
KDTreeNode *parent, int split_dim, _Examplar dom_elt, _HyperRectangle range_hr);
复制代码
};
复制代码

  类KDTreeNode就是按照前一篇表1所述定义的。需要注意的是_HyperRectangle这一结构,它表示的就是这一节点所代表的空间范围Range,其定义如下:

复制代码
struct _HyperRectangle    //定义表示数据范围的超矩形结构
{
_Examplar min; //统计数据集中所有数据向量每个维度上最小值组成的一个数据向量
_Examplar max; //统计数据集中所有数据向量每个维度上最大值组成的一个数据向量
复制代码
        _HyperRectangle() {}
_HyperRectangle(_Examplar mx, _Examplar mn)
{
assert (mx.getDomDims() == mn.getDomDims());
min = mn;
max = mx;
}
_HyperRectangle(const _HyperRectangle& rhs)
{
min = rhs.min;
max = rhs.max;
}
_HyperRectangle& operator= (const _HyperRectangle& rhs)
{
if(this == &rhs)
return *this;
min = rhs.min;
max = rhs.max;
return *this;
}
void create(_Examplar mx, _Examplar mn)
{
assert (mx.getDomDims() == mn.getDomDims());
min = mn;
max = mx;
}
复制代码
};
复制代码

  对于整个数据集来说_HyperRectangle表示的就是对全体的统计范围信息,对部分数据集来说其表示的就是对部分数据的统计范围信息。还是以上篇中实例中的数据{(2,3),(5,4),(9,6),(4,7),(8,1),(7,2)}为例,_HyperRectangle表示的统计范围如图1所示:

图1  _HyperRectangle表示的统计范围

  • 对于根节点(7,2),其所对应的空间范围是整个数据集,所以根节点(7,2)的_range_hr就是对整个数据集所有维度方向(此例即x,y方向)的数据范围统计得min = {dom_elt = (2,1),dom_dims = 2},max = {dom_elt = (9,7),dom_dims = 2};
  • 对于中间节点(5,4),其所对应的空间范围是根节点的左子树,所以节点(5,4)的_range_hr就是对整个数据集所有维度方向(此例即x,y方向)的数据范围统计得min = {dom_elt = (2,3),dom_dims = 2},max = {dom_elt = (5,7),dom_dims = 2};
  • 对于叶子节点(4,7),其所对应的空间范围是节点本身,所以节点(4,7)的_range_hr就是对整个数据集所有维度方向(此例即x,y方向)的 数据范围统计得min = {dom_elt = (4,7),dom_dims = 2},max = {dom_elt = (4,7),dom_dims = 2};

  最后再进行整个k-d tree结构的定义。代码如下:

复制代码
class KDTree    //k-d tree结构定义
{
public:
KDTreeNode *_root; //k-d tree的根节点
public:
KDTree():_root(NULL){}
void create(const ExamplarSet &exm_set); //创建k-d tree,实际上调用createKDTree
void destroy(); //销毁k-d tree,实际上调用destroyKDTree
~KDTree(){ destroyKDTree(_root); }
std::pair<_Examplar, double> findNearest(_Examplar target); //查找最近邻点函数,返回值是pair类型
//实际是调用findNearest_i
//查找距离在range范围内的近邻点,返回这样近邻点的个数,实际是调用findNearest_range
int findNearest(_Examplar target, double range, std::vector<std::pair<_Examplar, double>> &res_nearest);
private:
KDTreeNode* createKDTree(const ExamplarSet &exm_set);
void destroyKDTree(KDTreeNode *root);
std::pair<_Examplar, double> findNearest_i(KDTreeNode *root, _Examplar target);
int findNearest_range(KDTreeNode *root, _Examplar target, double range,
std::vector<std::pair<_Examplar, double>> &res_nearest);
复制代码

  可见,整个k-d tree结构是由一系列KDTreeNode类的节点构成。整个k-d树的构建算法和基于k-d树的最邻近查找算法主要就是由createKDTree,findNearest_i以及findNearest_range这三个函数完成。代码分别如下:

  • createKDTree
复制代码
//KDTree::是由于定义了KDTree的namespace
KDTree::KDTreeNode* KDTree::KDTree::createKDTree( const ExamplarSet &exm_set )
{
if(exm_set.empty())
return NULL;

ExamplarSet exm_set_copy(exm_set);

int dims = exm_set_copy.getDims();
int size = exm_set_copy.getSize();

//计算每个维的方差,选出方差值最大的维
double var_max = -0.1;
double avg, var;
int dim_max_var = -1;
for(int i=0;i<dims;i++)
{
avg = 0;
var = 0;
//求某一维的总和
for(int j=0;j<size;j++)
{
avg += exm_set_copy[j][i];
}
//求平均
avg /= size;
//求方差
for(int j=0;j<size;j++)
{
var += ( exm_set_copy[j][i] - avg ) *
( exm_set_copy[j][i] - avg );
}
var /= size;
if(var > var_max)
{
var_max = var;
dim_max_var = i;
}
}

//确定节点的数据矢量
_HyperRectangle hr = exm_set_copy.calculateRange(); //统计节点空间范围
exm_set_copy.sortByDim(dim_max_var); //将所有数据向量按最大区分度方向排序
int mid = size / 2;
_Examplar exm_split = exm_set_copy.examplarAt(mid); //取出排序结果的中间节点
exm_set_copy.remove(mid); //将中间节点作为父(根)节点,所有将其从数据集中去除

//确定左右节点
ExamplarSet exm_set_left(0, exm_set_copy.getDims());
ExamplarSet exm_set_right(0, exm_set_copy.getDims());
exm_set_right.remove(0);

int size_new = exm_set_copy.getSize(); //获得子数据空间大小
for(int i=0;i<size_new;i++) //生成左右子节点
{
_Examplar temp = exm_set_copy[i];
if( temp.dataAt(dim_max_var) <
exm_split.dataAt(dim_max_var) )
exm_set_left.push_back(temp);
else
exm_set_right.push_back(temp);
}

KDTreeNode *pNewNode = new KDTreeNode(0, 0, 0, dim_max_var, exm_split, hr);
pNewNode->_left_child = createKDTree(exm_set_left); //递归调用生成左子树
if(pNewNode->_left_child != NULL) //确认左子树父节点
pNewNode->_left_child->_parent = pNewNode;
pNewNode->_right_child = createKDTree(exm_set_right); //递归调用生成右子树
if(pNewNode->_right_child != NULL) //确认右子树父节点
pNewNode->_right_child->_parent = pNewNode;

return pNewNode; //最终返回k-d tree的根节点
}
复制代码

  整个createKDTree函数完全符合上篇中表2所述。注意其中统计节点空间范围calculateRange这一函数,其定义如下:

复制代码
KDTree::_HyperRectangle KDTree::ExamplarSet::calculateRange()
{
assert(_size > 0);
assert(_dims > 0);
_Examplar mn(_dims);
_Examplar mx(_dims);

for(int j=0;j<_dims;j++)
{
mn.dataAt(j) = (*this)[0][j]; //初始化最小范围向量
mx.dataAt(j) = (*this)[0][j]; //初始化最大范围向量
}

for(int i=1;i<_size;i++) //统计数据集中每一个数据向量
{
for(int j=0;j<_dims;j++)
{
if( (*this)[i][j] < mn[j] ) //比较每一维,寻找最小值
mn[j] = (*this)[i][j];
if( (*this)[i][j] > mx[j] ) //比较每一维,寻找最大值
mx[j] = (*this)[i][j];
}
}
_HyperRectangle hr(mx, mn);

return hr; //返回一个_HyperRectangle结构
}
复制代码
  • findNearest_i
复制代码
std::pair<KDTree::_Examplar, double> KDTree::KDTree::findNearest_i( KDTreeNode *root, _Examplar target )
{
KDTreeNode *pSearch = root;

//堆栈用于保存搜索路径
std::vector<KDTreeNode*> search_path;

_Examplar nearest;

double max_dist;

while(pSearch != NULL) //首先通过二叉查找得到搜索路径
{
search_path.push_back(pSearch);
int s = pSearch->splitDim();
if(target[s] <= pSearch->getDomElt()[s])
{
pSearch = pSearch->_left_child;
}
else
{
pSearch = pSearch->_right_child;
}
}

nearest = search_path.back()->getDomElt(); //取路径中最后的叶子节点为回溯前的最邻近点
max_dist = Distance_exm(nearest, target);

search_path.pop_back();

//回溯搜索路径
while(!search_path.empty())
{
KDTreeNode *pBack = search_path.back();
search_path.pop_back();

if( pBack->_left_child == NULL && pBack->_right_child == NULL) //如果是叶子节点,就直接比较距离的大小
{
if( Distance_exm(nearest, target) > Distance_exm(pBack->getDomElt(), target) )
{
nearest = pBack->getDomElt();
max_dist = Distance_exm(pBack->getDomElt(), target);
}
}
else
{
int s = pBack->splitDim();
if( abs(pBack->getDomElt()[s] - target[s]) < max_dist) //以target为圆心,max_dist为半径的圆和分割面如果
{ //有交割,则需要进入另一边子空间搜索
if( Distance_exm(nearest, target) > Distance_exm(pBack->getDomElt(), target) )
{
nearest = pBack->getDomElt();
max_dist = Distance_exm(pBack->getDomElt(), target);
}
if(target[s] <= pBack->getDomElt()[s]) //如果target位于左子空间,就应进入右子空间
pSearch = pBack->_right_child;
else
pSearch = pBack->_left_child; //如果target位于右子空间,就应进入左子空间
if(pSearch != NULL)
search_path.push_back(pSearch); //将新的节点加入search_path中
}
}
}

std::pair<_Examplar, double> res(nearest, max_dist);

return res; //返回包含最邻近点和最近距离的pair
}
复制代码
  • findNearest_range
复制代码
int KDTree::KDTree::findNearest_range( KDTreeNode *root, _Examplar target, double range, 
std::vector<std::pair<_Examplar, double>> &res_nearest )
{
if(root == NULL)
return 0;
double dist_sq, dx;
int ret, added_res = 0;
dist_sq = 0;
dist_sq = Distance_exm(root->getDomElt(), target); //计算搜索路径中每个节点和target的距离

if(dist_sq <= range) {                   //将范围内的近邻添加到结果向量res_nearest中
std::pair<_Examplar,double> temp(root->getDomElt(), dist_sq);
res_nearest.push_back(temp);
//结果个数+1
added_res = 1;
}

dx = target[root->splitDim()] - root->getDomElt()[root->splitDim()];
//左子树或右子树递归的查找
ret = findNearest_range(dx <= 0.0 ? root->_left_child : root->_right_child, target, range, res_nearest);
//当另外一边可能存在范围内的近邻
if(ret >= 0 && fabs(dx) < range) {
added_res += ret;
ret = findNearest_range(dx <= 0.0 ? root->_right_child : root->_left_child, target, range, res_nearest);
}

added_res += ret;
return added_res; //最终返回范围内的近邻个数
}
复制代码

  依然利用前述实例的数据来做测试,查找(2.1,3.1)和(2,4.5)两点的最近邻,并查找距离在4以内的所有近邻。程序运行结果如下:
                     

           图2  查找(2.1,3.1)的结果                                                       图3  查找(2,4.5)的结果

 

附件:http://files.cnblogs.com/eyeszjwang/kdtree.rar

转载请注明:http://www.cnblogs.com/eyeszjwang/articles/2432465.html

分类:  算法


k-d tree的优化查找算法BBF

  BBF(Best Bin First)是一种改进的k-d树最近邻查询算法。从前两篇标准的k-d树查询过程可以看出其搜索过程中的“回溯”是由“查询路径”来决定的,并没有考虑查询路径上数据点本身的一些性质。BBF的查询思路就是将“查询路径”上的节点进行排序,如按各自分割超平面(称为Bin)与查询点的距离排序。回溯检查总是从优先级最高的(Best Bin)的树节点开始。另外BBF还设置了一个运行超时限制,当优先级队列中的所有节点都经过检查或者超出时间限制时,算法返回当前找到的最好结果作为近似的最近邻。采用了best-bin-first search方法就可以将k-d树扩展到高维数据集上。

  下面我们通过大牛Rob Hess基于OpenCV的SIFT实现中的相关代码来具体学习下BBF算法。

复制代码
/*
Finds an image feature's approximate k nearest neighbors in a kd tree using
Best Bin First search.

@param kd_root root of an image feature kd tree
@param feat image feature for whose neighbors to search
@param k number of neighbors to find
@param nbrs pointer to an array in which to store pointers to neighbors
in order of increasing descriptor distance
@param max_nn_chks search is cut off after examining this many tree entries

@return Returns the number of neighbors found and stored in nbrs, or
-1 on error.
*/
//参数和返回值参看以上注释
//基于k-d tree + bbf的k近邻查找函数
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;      //expl是特征k-d tree中的一个节点
struct min_pq* min_pq; //min_pq是优先级队列
struct feature* tree_feat, ** _nbrs;//tree_feat是一个SIFT特征,_nbrs中存放着查找出来的近邻特征节点
struct bbf_data* bbf_data;   //bbf_data是一个用来存放临时特征数据和特征间距离的缓存结构
int i, t = 0, n = 0;       //t是运行时限,n是查找出来的近邻个数
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();                 //min_pq队列初始化
minpq_insert( min_pq, kd_root, 0 );         //将根节点先插入到min_pq优先级队列中
while( min_pq->n > 0 && t < max_nn_chks ) //min_pq队列没有回溯完且未达到时限
{
expl = (struct kd_node*)minpq_extract_min( min_pq );//从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 );    //从expl节点开始查找到叶子节点(下详) 
if( ! expl )
{
fprintf( stderr, "Warning: PQ unexpectedly empty, %s line %d\n",
__FILE__, __LINE__ );
goto fail;
}

for( i = 0; i < expl->n; i++ )  //开始比较查找最近邻
{
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 = tree_feat->feature_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 );//判断并插入符合条件的近邻到_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;
}
复制代码

   整个kdtree_bbf_knn函数包括了优先级队列的建立和k邻近查找两个过程。其中最关键的两个数据结构就是min_pq优先级队列和_nbrs存放k邻近结果的队列。min_pq优先级队列是按照各节点的分割超平面和目标查询特征点之间的距离升序排列的,第一个节点就是最小距离(优先级最高)的节点。另外_nbrs中也是按照与目标特征的距离升序排列,直接取结果的前k个特征就是对应的k近邻。注意:上述代码中的一些数据结构的定义以及一些对应的函数如:minpq_insert,minpq_extract_min, insert_into_nbr_array, descr_dist_sq等在这里就不贴了,详细代码可参看Rob Hess主页(http://blogs.oregonstate.edu/hess/)中代码参考文档。

  下面来详细看看函数explore_to_leaf是如何实现的。

复制代码
/*
Explores a kd tree from a given node to a leaf. Branching decisions are
made at each node based on the descriptor of a given feature. Each node
examined but not explored is put into a priority queue to be explored
later, keyed based on the distance from its partition key value to the
given feature's desctiptor.

@param kd_node root of the subtree to be explored
@param feat feature upon which branching decisions are based
@param min_pq a minimizing priority queue into which tree nodes are placed
as described above

@return Returns a pointer to the leaf node at which exploration ends or
NULL on error.
*/
//参数和返回值参看以上注释
//搜索路径和优先级队列的生成函数
static struct kd_node* explore_to_leaf( struct kd_node* kd_node, struct feature* feat,
struct min_pq* min_pq )
{
struct kd_node* unexpl, * expl = kd_node;  //unexpl中存放着优先级队列的候选特征点
                             //expl为开始搜索节点
double kv;                     //kv是分割维度的数据
int ki;                     //ki是分割维度序号
while( expl && ! expl->leaf )
{
ki = expl->ki;                //获得分割节点的ki,kv数据
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;        //并进入右子树搜索
}
     //将候选节点unexpl根据目标与分割超平面的距离插入到优先级队列中
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;  //返回搜索路径中最后的叶子节点
}
复制代码

  explore_to_leaf函数的实现中可以看到,优先级队列和搜索路径是同时生成的,这也是BBF算法的精髓所在:在二叉搜索的时候将搜索路径另一侧的分支加入到优先级队列中,供回溯时查找。而优先级队列的排序就是根据目标特征与分割超平面的距离ABS( kv - feat->descr[ki] )

注意:是目标特征和分割超平面间的距离,不是候选节点和分割超平面的距离。如还是上两篇例子中的数据,查找(2,4.5)的k近邻,当搜索到(5,4)节点时,应将(4,7)节点加入搜索路径而将(2,3)节点选为优先级队列的候选节点,优先级的计算是:abs(4 - 4.5) = 0.5。

 

转载请注明出处:http://www.cnblogs.com/eyeszjwang/articles/2437706.html

 


你可能感兴趣的:(KD tree and Bbf)