【人脸检测】OpenCV中的Haar+Adaboost级联分类器分解(四):利用并查集合并检测结果窗口

50 篇文章 0 订阅
17 篇文章 1 订阅

前一篇文章分析了OpenCV级联分类器结构,即“强分类器串联,弱分类器并联”,这一节我们来聊聊一些非常必要但是又容易人忽略的细节:如何利用并查集合并检测结果窗口。

-------------------------------------------
缩进在上一篇文章中,我曾提到:级联分类器通过移动检测窗口寻找图像中不同位置的目标,同时通过放大检测窗口寻找图像中大小不同的目标,最终寻找到图像中不同位置、不同的大小的所有目标。那么必然存在这样的情况:一个被检测器检测为目标的窗口其附近窗口也应该被检测到。例如在图像中的[x, y, w, h]窗口内检测到了人脸,那么[x-1, y-1, w+2, h+2]窗口也有极大的可能性被检测到,毕竟这2个窗口中的图像并没有明显差别(只是多了一个边而已)。
缩进图1展示了使用haarcascade_frontalface_alt2.xml检测一副含有人脸图像的结果,左边为合并检测结果窗口之前的结果,右边为合并之后的结果。从图1左边可以看出,每个目标(人脸)附近都有一组重叠检测结果窗口,除此之外还有零散分布的误检结果窗口。看到这里你应该明白了有必要对重叠的检测结果窗口进行合并,同时剔除零散分布的错误检测窗口。
   
图1 检测结果合并窗口前后对比图
(一)并查集(Union-Set)
缩进在了解如何合并窗口前,先来了解一种数据结构——并查集。

 

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

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

缩进如图2,现在武当派和明教分别推举了张三丰和张无忌作为老大。当帮派很大时组,每个大侠无法完整记住自己所在帮派的组织结构,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门口遇到了,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩肯定是同门,必须不能动手了。而当宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,那就要拔剑分个你死我活。

缩进在武林中,仅仅结成帮派是不够的,还需要通过其他关系组建帮派联盟扩大势力。既然杨不悔嫁给了殷梨亭,不妨直接设将明教老大张无忌的上级设置为武当派老大张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线,这里只关心数据的代表,忽略数据的内部结构)。从此以后,当殷梨亭再和杨左使相遇,一级一级往上问后发现老大都是张三丰,他俩就知道自己是一伙人了。但是如果每次殷梨亭和杨左使相遇都像这样一级一级往上问,不仅速度很慢,而且次数多了上级大侠也会不耐烦。为了解决这个问题,需要压缩查询路径。一起来看看并查集。

 

缩进并查集保持一组不相交的动态集合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. MAKE-SET(x)  
  2. 1 p[x] ← x  
  3. 2 rank[x] ← 0  
  4.   
  5. UNION(x, y)  
  6. 1 LINK(FIND-SET(x), FIND-SET(y))  
  7.   
  8. LINK(x, y)  
  9. if rank[x] < rank[y]  
  10. 2     p[x] ← y  
  11. else  
  12. 4     p[y] ← x  
  13. 5     if rank[x]==rank[y]  
  14. 6         rank[x] = rank[x] + 1  
  15.   
  16. FIND-SET(x)  
  17. if x ≠ p[x]  
  18. 2     p[x] ← FIND-SET(p[x])  
  19. return p[x]  
其中,MAKE-SET函数用于在无序数据中初始化并查集数据结构,将每个结点父结点设为其本身;UNION函数通过调用LINK和FIND-SET实现带压缩路径的并查集合并;LINK函数通过秩进行并查集合并;FIND-SET是带压缩路径的寻找结点代表的函数。
缩进 还是以图2为例说明UNION函数。上次直接将明教老大张无忌的上级设置为了武当老大张三丰(如上文蓝色字体),导致了后续殷梨亭和范右使见面时查询老大路径太长。本着拒绝拖延症的原则,希望在合并明教和武当派时就提前压缩路径,不把问题留给殷梨亭和范右使。既然上面伪代码中的UNION是带压缩路径的合并结点函数,我们来看看调用通过带压缩路径的方法结合把杨不悔嫁给殷梨亭会发生什么:
1. 调用UNION(殷梨亭,杨不悔),二者结婚。
缩进在调用LINK(FIND-SET(殷梨亭),FIND-SET(杨不悔))之前,首先调用FIND-SET(殷梨亭)和FIND-SET(杨不悔)。
2. 调用FIND-SET(殷梨亭)和FIND-SET(杨不悔)。
缩进FIND-SET压缩路径函数通过递归把查询路径上每个人的上级变为本帮派的老大,如杨不悔的上级被设置为张无忌(由于杨左使和范右使的上级就是老大张无忌,所以其上级不变);最后FIND-SET返回结点的老大,如FIND-SET(殷梨亭)返回张三丰,FIND-SET(杨不悔)返回张无忌。
3. 压缩路径后进行合并,即调用LINK(张三丰,张无忌)。
缩进由于rank[殷梨亭]==rank[杨不悔]==2秩相等,所以设置p[FIND-SET(杨不悔)]←FIND-SET(杨不悔),即设置p[张无忌]←张三丰,所以最终结果图为图3。
 
图3 使用带压缩路径的并查集合并结果
 
缩进当下次殷梨亭和杨左使见面后,查询路径就比上次短了。需要说明,由于例中调用UNION(殷梨亭,杨不悔)之前明教和武当派都没有经过压缩路径,所以合并后结果不是那么完美(如宋青书和范右使之间查询路径依旧很长);但是如果在明教和武当派组建之初就使用带压缩路径的并查集算法,则一定会得到完美的结果如果还有不明白的地方,建议查阅《算法导论》中的第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"                                                                   // by Cormen, Leiserson and Rivest, the chapter "Data structures for disjoint sets"                                             //                                                                                                                              // _vec参数用来传入存储检测结果窗口Rect的vector,labels参数输出与_vec对应的每个窗口Rect的类别                                             // predicate参数传入Rect同一类判别回调函数,一般来说predicate = SimilarRects(eps)                                                     //                                                                                                                              // 调用实例nclasses = partition(rectList, labels, SimilarRects(eps));  
  5. template<typename _Tp, class _EqPredicate> int  
  6. partition( const vector<_Tp>& _vec, vector<int>& labels, _EqPredicate predicate=_EqPredicate())                                   
  7. {  
  8.     int i, j, N = (int)_vec.size();  
  9.     const _Tp* vec = &_vec[0];  
  10.   
  11.     const int PARENT=0;  
  12.     const int RANK=1;  
  13.   
  14.     vector<int> _nodes(N*2);  
  15.     int (*nodes)[2] = (int(*)[2])&_nodes[0];  
  16.   
  17.     // The first O(N) pass: create N single-vertex trees 开始执行MAKE-SET,建立初始并查集结构  
  18.     for(i = 0; i < N; i++)  
  19.     {  
  20.         nodes[i][PARENT]=-1; // 初始化每个窗口的上一级为-1,即上一级不存在  
  21.         nodes[i][RANK] = 0; // 初始化每个窗口的秩为0  
  22.     }  
  23.   
  24.     // The main O(N^2) pass: merge connected components 开始执行UNION,合并相似窗口  
  25.     for( i = 0; i < N; i++ )  
  26.     {  
  27.         int root = i;  
  28.   
  29.         // find root  
  30.         while( nodes[root][PARENT] >= 0 ) // 即FIND-SET(root),寻找当前节点root的老大  
  31.             root = nodes[root][PARENT];  
  32.   
  33.         for( j = 0; j < N; j++ )  
  34.         {  
  35.             if( i == j || !predicate(vec[i], vec[j]))  
  36.                 continue;  
  37.             int root2 = j;  
  38.   
  39.             while( nodes[root2][PARENT] >= 0 ) // 即FIND-SET(root2),寻找root2的老大  
  40.                 root2 = nodes[root2][PARENT];  
  41.   
  42.             if( root2 != root ) // 当root和root2的老大不同时,LINK(root的老大, root2的老大),即LINK(FIND-SET(root),FIND-SET(root2))  
  43.             {  
  44.                 // unite both trees  
  45.                 int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];  
  46.                 if( rank > rank2 ) // 秩不相等时  
  47.                     nodes[root2][PARENT] = root;  
  48.                 else // rank = rank2,秩相等时按  
  49.                 {  
  50.                     nodes[root][PARENT] = root2;  
  51.                     nodes[root2][RANK] += rank == rank2;  
  52.                     root = root2;  
  53.                 }  
  54.                 assert( nodes[root][PARENT] < 0 );  
  55.   
  56.                 int k = j, parent;  
  57.   
  58.                 // compress the path from node2 to root 通过循环完成压缩路径  
  59.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  60.                 {  
  61.                     nodes[k][PARENT] = root;  
  62.                     k = parent;  
  63.                 }  
  64.   
  65.                 // compress the path from node to root 通过循环完成压缩路径  
  66.                 k = i;  
  67.                 while( (parent = nodes[k][PARENT]) >= 0 )  
  68.                 {  
  69.                     nodes[k][PARENT] = root;  
  70.                     k = parent;  
  71.                 }  
  72.             }  
  73.         }  
  74.     }  
  75.   
  76.     // Final O(N) pass: enumerate classes  
  77.     labels.resize(N);  
  78.     int nclasses = 0;  
  79.   
  80.     for( i = 0; i < N; i++ )  
  81.     {  
  82.         int root = i;  
  83.         while( nodes[root][PARENT] >= 0 )  
  84.             root = nodes[root][PARENT];  
  85.         // re-use the rank as the class label  
  86.         if( nodes[root][RANK] >= 0 )  
  87.             nodes[root][RANK] = ~nclasses++;  
  88.         labels[i] = ~nodes[root][RANK];  
  89.     }  
  90.   
  91.     return nclasses;  
  92. }  

-------------------------------------------

参考文献:

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

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

-------------------------------------------

OK,第三篇利用并查集进行窗口合并就结束了,欢迎留言提问或者提出意见!

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

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值