【转载】OpenCV中的Haar+Adaboost(四):利用并查集合并检测窗口(NMS)

http://lib.csdn.net/article/opencv/29324(原文地址)


前一篇文章分析了OpenCV级联分类器结构,即“强分类器串联,弱分类器并联”,这一节我们来聊聊一些非常必要但是又容易人忽略的细节:如何利用并查集合并检测结果窗口。
  在上一篇文章中,我曾提到:级联分类器通过移动检测窗口寻找图像中不同位置的目标,同时通过放大检测窗口寻找图像中大小不同的目标,最终寻找到图像中不同位置、不同的大小的所有目标。那么必然存在这样的情况:一个被检测器检测为目标的窗口,其附近窗口也应该被检测到。例如在图像中的[x, y, w, h]窗口内检测到了人脸,那么[x-1, y-1, w+2, h+2]窗口也有极大的可能性被检测到,毕竟这2个窗口中的图像并没有明显差别(只是多了一个边而已)。
  图1展示了使用haarcascade_frontalface_alt2.xml检测一副含有人脸图像的结果,左边为合并检测结果窗口之前的结果,右边为合并之后的结果。从图1左边可以看出,每个目标(人脸)附近都有一组重叠检测结果窗口,除此之外还有零散分布的误检结果窗口。看到这里你应该明白了有必要对重叠的检测结果窗口进行合并,同时剔除零散分布的错误检测窗口。今天写Faster RCNN突然想到,如果非要给这玩意起个高大上的名字,应该叫做NMS(non-maximum suppression)。
   
图1 检测结果合并窗口前后对比图

1 并查集(Union-Set)

  在了解如何合并窗口前,先来了解一种数据结构——并查集。
  为了形象的说明并查集,首先来了解一个例子。江湖上存在各种各样的大侠,他们没什么正当职业,整天背着剑四处游荡,碰到其他大侠就大打出手。俗话说“双拳难敌四手”,这些大侠都会拉帮结派壮大实力。那么为了辨识每个大侠属于哪个帮派,就需要每个帮派都推举一个“老大”。这些大侠只需要知道自己和其他大侠的老大是不是同一个人,就能明白自己和对方是不是一个帮派,从而决定是否动手过招。

图2 江湖大侠关系图(箭头表示上一级)

  如图2,现在武当和明教分别推举张三丰和张无忌为老大。当帮派很大时,每个大侠记不住帮派所有人,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门遇到,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩是同门。反之宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,就不是同门。

  除此之外,在武林中还需要组建联盟扩大帮派势力。既然杨不悔嫁给了殷梨亭,不妨直接设张无忌的上级为张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线。需要说明,我们只关心数据的代表,而忽略数据内部结构。从此以后当宋青书再和杨不悔相遇,一级一级查询后可以确定是同伙了。但是如果大侠们相遇都像这样一级一级往上问,查询路径很长。所以这种直接连接最上级的方法不是最优。

  为了解决这个问题,需要压缩路径——每个人记住自己最终的老大就行(如宋青书记住自己老大是张三丰,不在去问宋远桥),基本思路如下:

1.以武当为例,张三丰创建门派(明教也类似)

2.宋远桥和殷梨亭加入武当派,上级设置为张三丰

3.宋青书通过与宋远桥的关系加入武当派,压缩路径后设置上级为张三丰,同时也设置其所有原上级的上级为张三丰(由于原上级宋远桥的上级就是张三丰,没有变化)

  压缩完路径后的武当与明教状态图如下,其中红色代表压缩路径:

4.杨不悔通过与殷梨亭的关系也加入武当派别,压缩路径后设置上级为张三丰,同时设置原上级张无忌的上级是张三丰。绿色代表此次压缩路径。

  以后每次在合并中关系到了谁,就压缩谁的路径,同时压缩谁的所有上级的路径。此后宋青书和杨不悔的查询路径就短了很多。

5.假如某天范右使收徒了,徒弟也要加入联盟。在加入的时候,也需要压缩路径,设置徒弟的上级为张三丰;同时设置徒弟的原上级(范右使和张无忌)的上级为张三丰,如蓝色箭头。由于张无忌的上级就是张三丰,所以没有改变。这样,范右使的路径也得到压缩。

  看完例子之后,一起来看看并查集定义。并查集保持一组不相交的动态集合S={S1,S2,…,Sk},每个动态集合Si通过一个代表ai来识别,代表是集合中的某个元素(ai∈Si)。在某些应用中,哪一个元素被选为代表是无所谓的,我们只关心在不修改动态集合的前提下分别寻找某一集合的代表2次获得的结果相同;在另外一些应用中,如何选择集合的代表可能存在预先说明的规则,如选择集合的最大or最小值作为代表。总之,在并查集中,不改变动态集合S则每个集合Si的代表ai不变。

  不妨设x表示每个结点,p[x]表示x的父结点(即上一级,如图2中p[宋远桥]==张三丰),rank[x]表示x节点的秩(即该节点最长路径中结点个数,如图2中最长路径为:张三丰-张无忌-杨左使-杨不悔,所以rank[张三丰]==4)。并查集伪代码如下:
[cpp]  view plain  copy
  1. //创建Union-set  
  2. MAKE-SET(x)  
  3. 1 p[x] ← x //←号表示赋值  
  4. 2 rank[x] ← 0  
  5.   
  6. //合并x和y,底层压缩路径  
  7. UNION(x, y)  
  8. 1 LINK(FIND-SET(x), FIND-SET(y))  
  9.   
  10. LINK(x, y)  
  11. if rank[x] < rank[y]  
  12. 2     p[x] ← y  
  13. else  
  14. 4     p[y] ← x  
  15. 5     if rank[x]==rank[y]  
  16. 6         rank[x] = rank[x] + 1  
  17.   
  18. FIND-SET(x)  
  19. if x ≠ p[x]  
  20. 2     p[x] ← FIND-SET(p[x])  
  21. return p[x]  
其中,MAKE-SET函数用于在无序数据中初始化并查集数据结构,将每个结点父结点设为其本身;UNION函数通过调用LINK和FIND-SET实现带压缩路径的并查集合并;LINK函数通过秩进行并查集合并;FIND-SET是带压缩路径的寻找结点代表的函数。如果还有不明白的地方,建议查阅《算法导论》中的第21章:《用于不相交的数据结构》。

2 利用并查集合并检测结果窗口

  为了将并查集利用到合并窗口中,首先要定义窗口相似函数,即当前的两个窗口是不是“一伙人”。在OpenCV中,图像中的矩形窗口一般用Rect结构体表示,其包含x,y,width,height共4个成员变量,分别代表窗口的左上角点x坐标、y坐标、宽度和高度。下面代码定义了窗口相似函数SimilarRects::operator(),当2个窗口r1和r2位置很接近时返回TRUE,通过SimilarRects::operator()就可以将图1那些重叠的窗口合并在“一伙人”中。
[cpp]  view plain  copy
  1. class CV_EXPORTS SimilarRects  
  2. {  
  3. public:  
  4.     SimilarRects(double _eps) : eps(_eps) {}  
  5.     inline bool operator()(const Rect& r1, const Rect& r2) const  
  6.     {  
  7.         double delta = eps*(std::min(r1.width, r2.width) + std::min(r1.height, r2.height))*0.5;  
  8.         return std::abs(r1.x - r2.x) <= delta &&  
  9.             std::abs(r1.y - r2.y) <= delta &&  
  10.             std::abs(r1.x + r1.width - r2.x - r2.width) <= delta &&  
  11.             std::abs(r1.y + r1.height - r2.y - r2.height) <= delta;  
  12.     }  
  13.     double eps;  
  14. }  

定义好窗口相似性函数后,就可以利用并查集合并窗口函数了,大致过程如下:
  1. 首先利用MAKE-SET函数建立Rect对象的并查集初始结构
  2. 然后遍历整个并查集,用SimilarRects::operator()判断每2个窗口相似性,若相似则将这2个窗口合并为“一伙人”;
  3. 运行完步骤2后应该出现几个相互间不相似的窗口“团伙”,当“团伙”中的窗口数量小于阈值minNeighbors时,丢弃该“团伙”(认为这是零散分布的误检);
  4. 之后剩下若干组由大量重叠窗口组成的大“团伙”,分别求每个“团伙”中的所有窗口位置的平均值作为最终检测结果。
下面的展示了OpenCV实现步骤1-2并查集归类窗口的代码。在之前算法描述中为了清晰简洁,使用递归实现了整个并查集;但在实际中递归需要保存现场并进行压栈,开销极大,所以OpenCV使用循环替代了递归。
[cpp]  view plain  copy
  1. // This function splits the input sequence or set into one or more equivalence classes and  
  2. // returns the vector of labels - 0-based class indexes for each element.  
  3. // predicate(a,b) returns true if the two sequence elements certainly belong to the same class.  
  4. // The algorithm is described in “Introduction to Algorithms”                                                                     
  5. // by Cormen, Leiserson and Rivest, the chapter “Data structures for disjoint sets”                                               
  6.                                                                                                                                 
  7. // _vec输入存储原始检测结果窗口vector<Rect>,labels输出与_vec对应的每个窗口Rect的类别                                               
  8. // predicate参数传入Rect同一类的判别回调用函数  
  9.                                                                                                                                        
  10. // 调用实例nclasses = partition(rectList, labels, SimilarRects(eps));  
  11. template<typename _Tp, class _EqPredicate> int  
  12. partition( const vector<_Tp>& _vec, vector<int>& labels, _EqPredicate predicate=_EqPredicate())                                   
  13. {  
  14.     int i, j, N = (int)_vec.size();  
  15.     const _Tp* vec = &_vec[0];  
  16.   
  17.     const int PARENT=0;  
  18.     const int RANK=1;  
  19.   
  20.     vector<int> _nodes(N*2);  
  21.     int (*nodes)[2] = (int(*)[2])&_nodes[0];  
  22.   
  23.     // The first O(N) pass: create N single-vertex trees 开始执行MAKE-SET,建立初始并查集结构  
  24.     for(i = 0; i < N; i++)  
  25.     {  
  26.         nodes[i][PARENT]=-1; // 初始化每个窗口的上一级为-1,即上一级不存在  
  27.         nodes[i][RANK] = 0; // 初始化每个窗口的秩为0  
  28.     }  
  29.   
  30.     // The main O(N^2) pass: merge connected components 开始执行UNION,合并相似窗口  
  31.     for( i = 0; i < N; i++ )  
  32.     {  
  33.         int root = i;  
  34.   
  35.         // find root  
  36.         while( nodes[root][PARENT] >= 0 ) // 即FIND-SET(root),寻找当前节点root的老大  
  37.             root = nodes[root][PARENT];  
  38.   
  39.         for( j = 0; j < N; j++ )  
  40.         {  
  41.             if( i == j || !predicate(vec[i], vec[j]))  
  42.                 continue;  
  43.             int root2 = j;  
  44.   
  45.             while( nodes[root2][PARENT] >= 0 ) // 即FIND-SET(root2),寻找root2的老大  
  46.                 root2 = nodes[root2][PARENT];  
  47.   
  48.             if( root2 != root ) // 当root和root2的老大不同时,LINK(root的老大, root2的老大),即LINK(FIND-SET(root),FIND-SET(root2))  
  49.             {  
  50.                 // unite both trees  
  51.                 int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];  
  52.                 if( rank > rank2 ) // 秩不相等时  
  53.                     nodes[root2][PARENT] = root;  
  54.                 else // rank = rank2,秩相等时按  
  55.                 {  
  56.                     nodes[root][PARENT] = root2;  
  57.                     nodes[root2][RANK] += rank == rank2;  
  58.                     root = root2;  
  59.                 }  
  60.                 assert( nodes[root][PARENT] < 0 );  
  61.   
  62.                 int k = j, parent;  
  63.   
  64.                 // compress the path from node2 to root 通过循环完成压缩路径  
  65.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  66.                 {  
  67.                     nodes[k][PARENT] = root;  
  68.                     k = parent;  
  69.                 }  
  70.   
  71.                 // compress the path from node to root 通过循环完成压缩路径  
  72.                 k = i;  
  73.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  74.                 {  
  75.                     nodes[k][PARENT] = root;  
  76.                     k = parent;  
  77.                 }  
  78.             }  
  79.         }  
  80.     }  
  81.   
  82.     // Final O(N) pass: enumerate classes  
  83.     labels.resize(N);  
  84.     int nclasses = 0;  
  85.   
  86.     for( i = 0; i < N; i++ )  
  87.     {  
  88.         int root = i;  
  89.         while( nodes[root][PARENT] >= 0 )  
  90.             root = nodes[root][PARENT];  
  91.         // re-use the rank as the class label  
  92.         if( nodes[root][RANK] >= 0 )  
  93.             nodes[root][RANK] = ~nclasses++;  
  94.         labels[i] = ~nodes[root][RANK];  
  95.     }  
  96.   
  97.     return nclasses;  
  98. }  

——————————————-

参考文献:

[1] Thomas H.Cormen、Charles E.Leiserson等.《算法导论》

[2] http://blog.csdn.net/dellaserss/article/details/7724401

——————————————-

欢迎在下面的评论区留言提问or交流,私信不回。

下一篇,我会逐步开始介绍opencv_traincasacde.exe的训练原理。

http://lib.csdn.net/article/opencv/29324(原文地址)


前一篇文章分析了OpenCV级联分类器结构,即“强分类器串联,弱分类器并联”,这一节我们来聊聊一些非常必要但是又容易人忽略的细节:如何利用并查集合并检测结果窗口。
  在上一篇文章中,我曾提到:级联分类器通过移动检测窗口寻找图像中不同位置的目标,同时通过放大检测窗口寻找图像中大小不同的目标,最终寻找到图像中不同位置、不同的大小的所有目标。那么必然存在这样的情况:一个被检测器检测为目标的窗口,其附近窗口也应该被检测到。例如在图像中的[x, y, w, h]窗口内检测到了人脸,那么[x-1, y-1, w+2, h+2]窗口也有极大的可能性被检测到,毕竟这2个窗口中的图像并没有明显差别(只是多了一个边而已)。
  图1展示了使用haarcascade_frontalface_alt2.xml检测一副含有人脸图像的结果,左边为合并检测结果窗口之前的结果,右边为合并之后的结果。从图1左边可以看出,每个目标(人脸)附近都有一组重叠检测结果窗口,除此之外还有零散分布的误检结果窗口。看到这里你应该明白了有必要对重叠的检测结果窗口进行合并,同时剔除零散分布的错误检测窗口。今天写Faster RCNN突然想到,如果非要给这玩意起个高大上的名字,应该叫做NMS(non-maximum suppression)。
   
图1 检测结果合并窗口前后对比图

1 并查集(Union-Set)

  在了解如何合并窗口前,先来了解一种数据结构——并查集。
  为了形象的说明并查集,首先来了解一个例子。江湖上存在各种各样的大侠,他们没什么正当职业,整天背着剑四处游荡,碰到其他大侠就大打出手。俗话说“双拳难敌四手”,这些大侠都会拉帮结派壮大实力。那么为了辨识每个大侠属于哪个帮派,就需要每个帮派都推举一个“老大”。这些大侠只需要知道自己和其他大侠的老大是不是同一个人,就能明白自己和对方是不是一个帮派,从而决定是否动手过招。

图2 江湖大侠关系图(箭头表示上一级)

  如图2,现在武当和明教分别推举张三丰和张无忌为老大。当帮派很大时,每个大侠记不住帮派所有人,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门遇到,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩是同门。反之宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,就不是同门。

  除此之外,在武林中还需要组建联盟扩大帮派势力。既然杨不悔嫁给了殷梨亭,不妨直接设张无忌的上级为张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线。需要说明,我们只关心数据的代表,而忽略数据内部结构。从此以后当宋青书再和杨不悔相遇,一级一级查询后可以确定是同伙了。但是如果大侠们相遇都像这样一级一级往上问,查询路径很长。所以这种直接连接最上级的方法不是最优。

  为了解决这个问题,需要压缩路径——每个人记住自己最终的老大就行(如宋青书记住自己老大是张三丰,不在去问宋远桥),基本思路如下:

1.以武当为例,张三丰创建门派(明教也类似)

2.宋远桥和殷梨亭加入武当派,上级设置为张三丰

3.宋青书通过与宋远桥的关系加入武当派,压缩路径后设置上级为张三丰,同时也设置其所有原上级的上级为张三丰(由于原上级宋远桥的上级就是张三丰,没有变化)

  压缩完路径后的武当与明教状态图如下,其中红色代表压缩路径:

4.杨不悔通过与殷梨亭的关系也加入武当派别,压缩路径后设置上级为张三丰,同时设置原上级张无忌的上级是张三丰。绿色代表此次压缩路径。

  以后每次在合并中关系到了谁,就压缩谁的路径,同时压缩谁的所有上级的路径。此后宋青书和杨不悔的查询路径就短了很多。

5.假如某天范右使收徒了,徒弟也要加入联盟。在加入的时候,也需要压缩路径,设置徒弟的上级为张三丰;同时设置徒弟的原上级(范右使和张无忌)的上级为张三丰,如蓝色箭头。由于张无忌的上级就是张三丰,所以没有改变。这样,范右使的路径也得到压缩。

  看完例子之后,一起来看看并查集定义。并查集保持一组不相交的动态集合S={S1,S2,…,Sk},每个动态集合Si通过一个代表ai来识别,代表是集合中的某个元素(ai∈Si)。在某些应用中,哪一个元素被选为代表是无所谓的,我们只关心在不修改动态集合的前提下分别寻找某一集合的代表2次获得的结果相同;在另外一些应用中,如何选择集合的代表可能存在预先说明的规则,如选择集合的最大or最小值作为代表。总之,在并查集中,不改变动态集合S则每个集合Si的代表ai不变。

  不妨设x表示每个结点,p[x]表示x的父结点(即上一级,如图2中p[宋远桥]==张三丰),rank[x]表示x节点的秩(即该节点最长路径中结点个数,如图2中最长路径为:张三丰-张无忌-杨左使-杨不悔,所以rank[张三丰]==4)。并查集伪代码如下:
[cpp]  view plain  copy
  1. //创建Union-set  
  2. MAKE-SET(x)  
  3. 1 p[x] ← x //←号表示赋值  
  4. 2 rank[x] ← 0  
  5.   
  6. //合并x和y,底层压缩路径  
  7. UNION(x, y)  
  8. 1 LINK(FIND-SET(x), FIND-SET(y))  
  9.   
  10. LINK(x, y)  
  11. if rank[x] < rank[y]  
  12. 2     p[x] ← y  
  13. else  
  14. 4     p[y] ← x  
  15. 5     if rank[x]==rank[y]  
  16. 6         rank[x] = rank[x] + 1  
  17.   
  18. FIND-SET(x)  
  19. if x ≠ p[x]  
  20. 2     p[x] ← FIND-SET(p[x])  
  21. return p[x]  
其中,MAKE-SET函数用于在无序数据中初始化并查集数据结构,将每个结点父结点设为其本身;UNION函数通过调用LINK和FIND-SET实现带压缩路径的并查集合并;LINK函数通过秩进行并查集合并;FIND-SET是带压缩路径的寻找结点代表的函数。如果还有不明白的地方,建议查阅《算法导论》中的第21章:《用于不相交的数据结构》。

2 利用并查集合并检测结果窗口

  为了将并查集利用到合并窗口中,首先要定义窗口相似函数,即当前的两个窗口是不是“一伙人”。在OpenCV中,图像中的矩形窗口一般用Rect结构体表示,其包含x,y,width,height共4个成员变量,分别代表窗口的左上角点x坐标、y坐标、宽度和高度。下面代码定义了窗口相似函数SimilarRects::operator(),当2个窗口r1和r2位置很接近时返回TRUE,通过SimilarRects::operator()就可以将图1那些重叠的窗口合并在“一伙人”中。
[cpp]  view plain  copy
  1. class CV_EXPORTS SimilarRects  
  2. {  
  3. public:  
  4.     SimilarRects(double _eps) : eps(_eps) {}  
  5.     inline bool operator()(const Rect& r1, const Rect& r2) const  
  6.     {  
  7.         double delta = eps*(std::min(r1.width, r2.width) + std::min(r1.height, r2.height))*0.5;  
  8.         return std::abs(r1.x - r2.x) <= delta &&  
  9.             std::abs(r1.y - r2.y) <= delta &&  
  10.             std::abs(r1.x + r1.width - r2.x - r2.width) <= delta &&  
  11.             std::abs(r1.y + r1.height - r2.y - r2.height) <= delta;  
  12.     }  
  13.     double eps;  
  14. }  

定义好窗口相似性函数后,就可以利用并查集合并窗口函数了,大致过程如下:
  1. 首先利用MAKE-SET函数建立Rect对象的并查集初始结构
  2. 然后遍历整个并查集,用SimilarRects::operator()判断每2个窗口相似性,若相似则将这2个窗口合并为“一伙人”;
  3. 运行完步骤2后应该出现几个相互间不相似的窗口“团伙”,当“团伙”中的窗口数量小于阈值minNeighbors时,丢弃该“团伙”(认为这是零散分布的误检);
  4. 之后剩下若干组由大量重叠窗口组成的大“团伙”,分别求每个“团伙”中的所有窗口位置的平均值作为最终检测结果。
下面的展示了OpenCV实现步骤1-2并查集归类窗口的代码。在之前算法描述中为了清晰简洁,使用递归实现了整个并查集;但在实际中递归需要保存现场并进行压栈,开销极大,所以OpenCV使用循环替代了递归。
[cpp]  view plain  copy
  1. // This function splits the input sequence or set into one or more equivalence classes and  
  2. // returns the vector of labels - 0-based class indexes for each element.  
  3. // predicate(a,b) returns true if the two sequence elements certainly belong to the same class.  
  4. // The algorithm is described in “Introduction to Algorithms”                                                                     
  5. // by Cormen, Leiserson and Rivest, the chapter “Data structures for disjoint sets”                                               
  6.                                                                                                                                 
  7. // _vec输入存储原始检测结果窗口vector<Rect>,labels输出与_vec对应的每个窗口Rect的类别                                               
  8. // predicate参数传入Rect同一类的判别回调用函数  
  9.                                                                                                                                        
  10. // 调用实例nclasses = partition(rectList, labels, SimilarRects(eps));  
  11. template<typename _Tp, class _EqPredicate> int  
  12. partition( const vector<_Tp>& _vec, vector<int>& labels, _EqPredicate predicate=_EqPredicate())                                   
  13. {  
  14.     int i, j, N = (int)_vec.size();  
  15.     const _Tp* vec = &_vec[0];  
  16.   
  17.     const int PARENT=0;  
  18.     const int RANK=1;  
  19.   
  20.     vector<int> _nodes(N*2);  
  21.     int (*nodes)[2] = (int(*)[2])&_nodes[0];  
  22.   
  23.     // The first O(N) pass: create N single-vertex trees 开始执行MAKE-SET,建立初始并查集结构  
  24.     for(i = 0; i < N; i++)  
  25.     {  
  26.         nodes[i][PARENT]=-1; // 初始化每个窗口的上一级为-1,即上一级不存在  
  27.         nodes[i][RANK] = 0; // 初始化每个窗口的秩为0  
  28.     }  
  29.   
  30.     // The main O(N^2) pass: merge connected components 开始执行UNION,合并相似窗口  
  31.     for( i = 0; i < N; i++ )  
  32.     {  
  33.         int root = i;  
  34.   
  35.         // find root  
  36.         while( nodes[root][PARENT] >= 0 ) // 即FIND-SET(root),寻找当前节点root的老大  
  37.             root = nodes[root][PARENT];  
  38.   
  39.         for( j = 0; j < N; j++ )  
  40.         {  
  41.             if( i == j || !predicate(vec[i], vec[j]))  
  42.                 continue;  
  43.             int root2 = j;  
  44.   
  45.             while( nodes[root2][PARENT] >= 0 ) // 即FIND-SET(root2),寻找root2的老大  
  46.                 root2 = nodes[root2][PARENT];  
  47.   
  48.             if( root2 != root ) // 当root和root2的老大不同时,LINK(root的老大, root2的老大),即LINK(FIND-SET(root),FIND-SET(root2))  
  49.             {  
  50.                 // unite both trees  
  51.                 int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];  
  52.                 if( rank > rank2 ) // 秩不相等时  
  53.                     nodes[root2][PARENT] = root;  
  54.                 else // rank = rank2,秩相等时按  
  55.                 {  
  56.                     nodes[root][PARENT] = root2;  
  57.                     nodes[root2][RANK] += rank == rank2;  
  58.                     root = root2;  
  59.                 }  
  60.                 assert( nodes[root][PARENT] < 0 );  
  61.   
  62.                 int k = j, parent;  
  63.   
  64.                 // compress the path from node2 to root 通过循环完成压缩路径  
  65.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  66.                 {  
  67.                     nodes[k][PARENT] = root;  
  68.                     k = parent;  
  69.                 }  
  70.   
  71.                 // compress the path from node to root 通过循环完成压缩路径  
  72.                 k = i;  
  73.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  74.                 {  
  75.                     nodes[k][PARENT] = root;  
  76.                     k = parent;  
  77.                 }  
  78.             }  
  79.         }  
  80.     }  
  81.   
  82.     // Final O(N) pass: enumerate classes  
  83.     labels.resize(N);  
  84.     int nclasses = 0;  
  85.   
  86.     for( i = 0; i < N; i++ )  
  87.     {  
  88.         int root = i;  
  89.         while( nodes[root][PARENT] >= 0 )  
  90.             root = nodes[root][PARENT];  
  91.         // re-use the rank as the class label  
  92.         if( nodes[root][RANK] >= 0 )  
  93.             nodes[root][RANK] = ~nclasses++;  
  94.         labels[i] = ~nodes[root][RANK];  
  95.     }  
  96.   
  97.     return nclasses;  
  98. }  

——————————————-

参考文献:

[1] Thomas H.Cormen、Charles E.Leiserson等.《算法导论》

[2] http://blog.csdn.net/dellaserss/article/details/7724401

——————————————-

欢迎在下面的评论区留言提问or交流,私信不回。

下一篇,我会逐步开始介绍opencv_traincasacde.exe的训练原理。

猜你喜欢

转载自blog.csdn.net/mnydxk/article/details/81061172