前一篇文章分析了OpenCV级联分类器结构,即“强分类器串联,弱分类器并联”,这一节我们来聊聊一些非常必要但是又容易人忽略的细节:如何利用并查集合并检测结果窗口。
缩进为了形象的说明并查集,首先来了解一个例子。江湖上存在各种各样的大侠,他们没什么正当职业,整天背着剑四处游荡,碰到其他大侠就大打出手。俗话说“双拳难敌四手”,这些大侠都会拉帮结派壮大实力。那么为了辨识每个大侠属于哪个帮派,就需要每个帮派都推举一个“老大”。这些大侠只需要知道自己和其他大侠的老大是不是同一个人,就能明白自己和对方是不是一个帮派,从而决定是否动手过招。
图2 江湖大侠关系图(箭头表示上一级)
缩进如图2,现在武当派和明教分别推举了张三丰和张无忌作为老大。当帮派很大时组,每个大侠无法完整记住自己所在帮派的组织结构,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门口遇到了,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩肯定是同门,必须不能动手了。而当宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,那就要拔剑分个你死我活。
缩进在武林中,仅仅结成帮派是不够的,还需要通过其他关系组建帮派联盟扩大势力。既然杨不悔嫁给了殷梨亭,不妨直接设将明教老大张无忌的上级设置为武当派老大张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线,这里只关心数据的代表,忽略数据的内部结构)。从此以后,当殷梨亭再和杨左使相遇,一级一级往上问后发现老大都是张三丰,他俩就知道自己是一伙人了。但是如果每次殷梨亭和杨左使相遇都像这样一级一级往上问,不仅速度很慢,而且次数多了上级大侠也会不耐烦。为了解决这个问题,需要压缩查询路径。一起来看看并查集。
- MAKE-SET(x)
- 1 p[x] ← x
- 2 rank[x] ← 0
- UNION(x, y)
- 1 LINK(FIND-SET(x), FIND-SET(y))
- LINK(x, y)
- 1 if rank[x] < rank[y]
- 2 p[x] ← y
- 3 else
- 4 p[y] ← x
- 5 if rank[x]==rank[y]
- 6 rank[x] = rank[x] + 1
- FIND-SET(x)
- 1 if x ≠ p[x]
- 2 p[x] ← FIND-SET(p[x])
- 3 return p[x]
1. 调用UNION(殷梨亭,杨不悔),二者结婚。
2. 调用FIND-SET(殷梨亭)和FIND-SET(杨不悔)。
3. 压缩路径后进行合并,即调用LINK(张三丰,张无忌)。
(二)利用并查集合并检测结果窗口
- class CV_EXPORTS SimilarRects
- {
- public:
- SimilarRects(double _eps) : eps(_eps) {}
- inline bool operator()(const Rect& r1, const Rect& r2) const
- {
- double delta = eps*(std::min(r1.width, r2.width) + std::min(r1.height, r2.height))*0.5;
- return std::abs(r1.x - r2.x) <= delta &&
- std::abs(r1.y - r2.y) <= delta &&
- std::abs(r1.x + r1.width - r2.x - r2.width) <= delta &&
- std::abs(r1.y + r1.height - r2.y - r2.height) <= delta;
- }
- double eps;
- }
1. 首先利用MAKE-SET函数建立Rect对象的并查集初始结构;2. 然后遍历整个并查集,用SimilarRects::operator()判断每2个窗口相似性,若相似则将这2个窗口合并为“一伙人”;3. 运行完步骤2后应该出现几个相互间不相似的窗口“团伙”,当“团伙”中的窗口数量小于阈值minNeighbors时,丢弃该“团伙”(认为这是零散分布的误检);4. 之后剩下若干组由大量重叠窗口组成的大“团伙”,分别求每个“团伙”中的所有窗口位置的平均值作为最终检测结果。
- // This function splits the input sequence or set into one or more equivalence classes and
- // returns the vector of labels - 0-based class indexes for each element.
- // predicate(a,b) returns true if the two sequence elements certainly belong to the same class.
- // 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));
- template<typename _Tp, class _EqPredicate> int
- partition( const vector<_Tp>& _vec, vector<int>& labels, _EqPredicate predicate=_EqPredicate())
- {
- int i, j, N = (int)_vec.size();
- const _Tp* vec = &_vec[0];
- const int PARENT=0;
- const int RANK=1;
- vector<int> _nodes(N*2);
- int (*nodes)[2] = (int(*)[2])&_nodes[0];
- // The first O(N) pass: create N single-vertex trees 开始执行MAKE-SET,建立初始并查集结构
- for(i = 0; i < N; i++)
- {
- nodes[i][PARENT]=-1; // 初始化每个窗口的上一级为-1,即上一级不存在
- nodes[i][RANK] = 0; // 初始化每个窗口的秩为0
- }
- // The main O(N^2) pass: merge connected components 开始执行UNION,合并相似窗口
- for( i = 0; i < N; i++ )
- {
- int root = i;
- // find root
- while( nodes[root][PARENT] >= 0 ) // 即FIND-SET(root),寻找当前节点root的老大
- root = nodes[root][PARENT];
- for( j = 0; j < N; j++ )
- {
- if( i == j || !predicate(vec[i], vec[j]))
- continue;
- int root2 = j;
- while( nodes[root2][PARENT] >= 0 ) // 即FIND-SET(root2),寻找root2的老大
- root2 = nodes[root2][PARENT];
- if( root2 != root ) // 当root和root2的老大不同时,LINK(root的老大, root2的老大),即LINK(FIND-SET(root),FIND-SET(root2))
- {
- // unite both trees
- int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];
- if( rank > rank2 ) // 秩不相等时
- nodes[root2][PARENT] = root;
- else // rank = rank2,秩相等时按
- {
- nodes[root][PARENT] = root2;
- nodes[root2][RANK] += rank == rank2;
- root = root2;
- }
- assert( nodes[root][PARENT] < 0 );
- int k = j, parent;
- // compress the path from node2 to root 通过循环完成压缩路径
- while( (parent = nodes[k][PARENT]) >= 0 )
- {
- nodes[k][PARENT] = root;
- k = parent;
- }
- // compress the path from node to root 通过循环完成压缩路径
- k = i;
- while( (parent = nodes[k][PARENT]) >= 0 )
- {
- nodes[k][PARENT] = root;
- k = parent;
- }
- }
- }
- }
- // Final O(N) pass: enumerate classes
- labels.resize(N);
- int nclasses = 0;
- for( i = 0; i < N; i++ )
- {
- int root = i;
- while( nodes[root][PARENT] >= 0 )
- root = nodes[root][PARENT];
- // re-use the rank as the class label
- if( nodes[root][RANK] >= 0 )
- nodes[root][RANK] = ~nclasses++;
- labels[i] = ~nodes[root][RANK];
- }
- return nclasses;
- }
-------------------------------------------
参考文献:
[1] Thomas H.Cormen、Charles E.Leiserson等.《算法导论》
OK,第三篇利用并查集进行窗口合并就结束了,欢迎留言提问或者提出意见!
下一篇,我会逐步开始介绍opencv_traincasacde.exe的训练原理。