By RaySaint 2011/10/12

动机

先前写了一篇文章《SIFT算法研究》讲了讲SIFT特征具体是如何检测和描述的,其中也提到了SIFT常见的一个用途就是物体识别,物体识别的过程如下图所示:

如上图(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,先讲一讲最近邻搜索。

最近邻搜索

先给出一个最近邻的数学形式的定义。给定一个多维空间,把中的一个向量成为一个样本点或数据点。中样本点的有限集合称为样本集。给定样本集E,和一个样本点d,d的最近邻就是任何样本点d’∈E满足None-nearer(E,d,d’)。

None-nearer如下定义:

上面的公式中距离度量是欧式距离,当然也可以是任何其他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的构建是一个逐级展开的递归过程。

其算法的伪代码如下:

  1. 算法:createKDTree 构建一棵k-d tree
  2. 输入:exm_set 样本集
  3. 输出 : Kd, 类型为kd-tree
  4. 1. 如果exm_set是空的,则返回空的kd-tree
  5. 2.调用分裂结点选择程序(输入是exm_set),返回两个值
  6. dom_elt:= exm_set中的一个样本点
  7. split := 分裂维的序号
  8. 3.exm_set_left = {exm∈exm_set – dom_elt && exm[split] <= dom_elt[split]}
  9. exm_set_right = {exm∈exm_set – dom_elt && exm[split] > dom_elt[split]}
  10. 4.left = createKDTree(exm_set_left)
  11. 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最近邻搜索的伪代码:

  1. 算法:kdtreeFindNearest /* k-d tree的最近邻搜索 */
  2. 输入:Kd /* k-d tree类型*/
  3. target /* 待查询数据点 */
  4. 输出 : nearest /* 最近邻数据结点 */
  5. dist /* 最近邻和查询点的距离 */
  6. 1. 如果Kd是空的,则设dist为无穷大返回
  7. 2. 向下搜索直到叶子结点
  8. pSearch = &Kd
  9. while(pSearch != NULL)
  10. {
  11. pSearch加入到search_path中;
  12. if(target[pSearch->split] <= pSearch->dom_elt[pSearch->split]) /* 如果小于就进入左子树 */
  13. {
  14. pSearch = pSearch->left;
  15. }
  16. else
  17. {
  18. pSearch = pSearch->right;
  19. }
  20. }
  21. 取出search_path最后一个赋给nearest
  22. dist = Distance(nearest, target);
  23. 3. 回溯搜索路径
  24. while(search_path不为空)
  25. {
  26. 取出search_path最后一个结点赋给pBack
  27. if(pBack->left为空 && pBack->right为空) /* 如果pBack为叶子结点 */
  28. {
  29. if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )
  30. {
  31. nearest = pBack->dom_elt;
  32. dist = Distance(pBack->dom_elt, target);
  33. }
  34. }
  35. else
  36. {
  37. s = pBack->split;
  38. if( abs(pBack->dom_elt[s] - target[s]) < dist) /* 如果以target为中心的圆(球或超球),半径为dist的圆与分割超平面相交, 那么就要跳到另一边的子空间去搜索 */
  39. {
  40. if( Distance(nearest, target) > Distance(pBack->dom_elt, target) )
  41. {
  42. nearest = pBack->dom_elt;
  43. dist = Distance(pBack->dom_elt, target);
  44. }
  45. if(target[s] <= pBack->dom_elt[s]) /* 如果target位于pBack的左子空间,那么就要跳到右子空间去搜索 */
  46. pSearch = pBack->right;
  47. else
  48. pSearch = pBack->left; /* 如果target位于pBack的右子空间,那么就要跳到左子空间去搜索 */
  49. if(pSearch != NULL)
  50. pSearch加入到search_path中
  51. }
  52. }
  53. }

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

转载于:https://blog.51cto.com/underthehood/687160

k-d tree算法的研究相关推荐

  1. K单体型重建算法的研究

    K单体型重建算法的研究 王兆灿   [摘要]:随着新一代基因测序技术的飞速发展,以及单体型数据在人类遗传学等领域研究和应用的不断深入,对单体型数据的研究开始转向其他生物物种.由于测序技术的限制,通过生 ...

  2. 碰撞检测中的K_DOPS算法的研究

    摘  要  K_DOPS碰撞检测算法是一类重要的碰撞检测算法,本文从K_DOPS的定义.包围盒的选择与计算.包围盒树的构造等几个方面对K_DOPS算法进行研究,并给出一种快速碰撞检测算法并对该算法进行 ...

  3. K-D Tree 算法详解及Python实现

    K-D Tree 算法 k−d treek−dtree\mathrm{k-d\ tree}即k−dimensional treek−dimensionaltree\mathrm{k-dimension ...

  4. K-d tree 算法

    转载的原文链接:http://www.cnblogs.com/eyeszjwang/articles/2429382.html 已经写得非常好了,非常清晰.只是在原文的基础上增加了一点解释,方便大家理 ...

  5. 全基因组关联分析中上位性检测算法的研究

    全基因组关联分析中上位性检测算法的研究 前言 这个项目主要是分享一些全基因组关联分析中上位性检测算法的研究经验,算是,怎么入门,写这么个东西,一是做总结,二是咱实验室估计以后还会有做这个方向的,备着吧 ...

  6. ML之kNN:k最近邻kNN算法的简介、应用、经典案例之详细攻略

    ML之kNN:k最近邻kNN算法的简介.应用.经典案例之详细攻略 目录 kNN算法的简介 1.kNN思路过程 1.1.k的意义 1.2.kNN求最近距离案例解释原理-通过实际案例,探究kNN思路过程 ...

  7. FP Tree算法原理总结(转)

    FP Tree算法原理总结 转自: https://www.cnblogs.com/zhengxingpeng/p/6679280.html 总结得太好了. FP Tree算法原理总结 在Aprior ...

  8. R-Tree空间索引算法的研究历程和最新进展分析

    摘要:本文介绍了空间索引的概念.R-Tree数据结构和R-Tree空间索引的算法描述,并从R-Tree索引技术的优缺点对R-Tree的改进结构--变种R-Tree进行了论述.最后,对R-Tree的最新 ...

  9. k-d tree算法原理及实现

    k-d tree即k-dimensional tree,常用来作空间划分及近邻搜索,是二叉空间划分树的一个特例.通常,对于维度为k,数据点数为N的数据集,k-d tree适用于N≫2k的情形. 1)k ...

最新文章

  1. 【学习笔记】线性规划与对偶问题和LP对偶费用流([ZJOI2013]防守战线题解)
  2. Linux如何通过命令查看日志文件的某几行(中间几行或最后几行)
  3. numpy维度交换_如何将2个不同维度的numpy数组相乘
  4. python网络爬虫开发从入门到精通_Python突击-从入门到精通到项目实战
  5. java课程设计学生信息管理_JAVA课程设计---学生基本信息管理系统
  6. 也说 Jquery+ASP.NET 实现开心网上传头像剪裁功能
  7. 判别性的低秩字典学习代码matlab,基于分类的判别性字典学习的稀疏编码算法研究...
  8. Java程序员必读——领悟Java编程思想
  9. paip.环境配置整合 ibatis mybatis proxool
  10. 袖珍计算器c语言设计源码,VB程序题:编一模拟袖珍计算器的完整程序,界面如下图所示。要求:输入两个操作数和一个操作符,根据操作符决定所做的运算。 VB源码 龚沛曾...
  11. 机器学习:matlab和python实现PCA降维算法
  12. 网盘多线程提速下载利器:JDownloader 2 for Mac
  13. 【STM32】可变焦、聚焦摄像头驱动电路与驱动程序——两相四线步进电机驱动
  14. baidu 快递查询API
  15. python怎么使用int四舍五入_python浮点数舍入(ROUND)方式总结
  16. 谷歌AI人工智能:我们的原则
  17. 关于element-plus的Dropdown 下拉菜单属性的修改
  18. 为什么同时需要IP地址和MAC地址
  19. PostgreSQL 中的单引号与双引号
  20. Win10 发布UWD图形驱动程序 有助于小幅提高性能

热门文章

  1. linux下有关phy的命令,linux – 如何为Debian安装b43-lpphy-installer?
  2. Java项目:个人博客系统(前后端分离+java+vue+Springboot+ssm+mysql+maven+redis)
  3. 手机号码输入历史记录匹配
  4. 通知提示SCPromptView
  5. [译] iOS 开发之新版 APNs 搭建必备知识
  6. Linux下的redis的持久化,主从同步及哨兵
  7. Ubuntu 上创建常用磁盘阵列
  8. python包引用问题
  9. 使用hql动态创建对象问题
  10. 软件工程实践2017 个人技术博客