http://lib.csdn.net/article/opencv/29324(原文地址)
1 并查集(Union-Set)
图2 江湖大侠关系图(箭头表示上一级)
如图2,现在武当和明教分别推举张三丰和张无忌为老大。当帮派很大时,每个大侠记不住帮派所有人,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门遇到,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩是同门。反之宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,就不是同门。
除此之外,在武林中还需要组建联盟扩大帮派势力。既然杨不悔嫁给了殷梨亭,不妨直接设张无忌的上级为张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线。需要说明,我们只关心数据的代表,而忽略数据内部结构)。从此以后当宋青书再和杨不悔相遇,一级一级查询后可以确定是同伙了。但是如果大侠们相遇都像这样一级一级往上问,查询路径很长。所以这种直接连接最上级的方法不是最优。
为了解决这个问题,需要压缩路径——每个人记住自己最终的老大就行(如宋青书记住自己老大是张三丰,不在去问宋远桥),基本思路如下:
1.以武当为例,张三丰创建门派(明教也类似)
2.宋远桥和殷梨亭加入武当派,上级设置为张三丰
3.宋青书通过与宋远桥的关系加入武当派,压缩路径后设置上级为张三丰,同时也设置其所有原上级的上级为张三丰(由于原上级宋远桥的上级就是张三丰,没有变化)。
压缩完路径后的武当与明教状态图如下,其中红色代表压缩路径:
4.杨不悔通过与殷梨亭的关系也加入武当派别,压缩路径后设置上级为张三丰,同时设置原上级张无忌的上级是张三丰。绿色代表此次压缩路径。
以后每次在合并中关系到了谁,就压缩谁的路径,同时压缩谁的所有上级的路径。此后宋青书和杨不悔的查询路径就短了很多。
5.假如某天范右使收徒了,徒弟也要加入联盟。在加入的时候,也需要压缩路径,设置徒弟的上级为张三丰;同时设置徒弟的原上级(范右使和张无忌)的上级为张三丰,如蓝色箭头。由于张无忌的上级就是张三丰,所以没有改变。这样,范右使的路径也得到压缩。
看完例子之后,一起来看看并查集定义。并查集保持一组不相交的动态集合S={S1,S2,…,Sk},每个动态集合Si通过一个代表ai来识别,代表是集合中的某个元素(ai∈Si)。在某些应用中,哪一个元素被选为代表是无所谓的,我们只关心在不修改动态集合的前提下分别寻找某一集合的代表2次获得的结果相同;在另外一些应用中,如何选择集合的代表可能存在预先说明的规则,如选择集合的最大or最小值作为代表。总之,在并查集中,不改变动态集合S则每个集合Si的代表ai不变。
- //创建Union-set
- MAKE-SET(x)
- 1 p[x] ← x //←号表示赋值
- 2 rank[x] ← 0
- //合并x和y,底层压缩路径
- 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]
2 利用并查集合并检测结果窗口
- 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;
- }
定义好窗口相似性函数后,就可以利用并查集合并窗口函数了,大致过程如下:
-
首先利用MAKE-SET函数建立Rect对象的并查集初始结构
-
然后遍历整个并查集,用SimilarRects::operator()判断每2个窗口相似性,若相似则将这2个窗口合并为“一伙人”;
-
运行完步骤2后应该出现几个相互间不相似的窗口“团伙”,当“团伙”中的窗口数量小于阈值minNeighbors时,丢弃该“团伙”(认为这是零散分布的误检);
-
之后剩下若干组由大量重叠窗口组成的大“团伙”,分别求每个“团伙”中的所有窗口位置的平均值作为最终检测结果。
- // 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输入存储原始检测结果窗口vector<Rect>,labels输出与_vec对应的每个窗口Rect的类别
- // predicate参数传入Rect同一类的判别回调用函数
- // 调用实例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等.《算法导论》
欢迎在下面的评论区留言提问or交流,私信不回。
下一篇,我会逐步开始介绍opencv_traincasacde.exe的训练原理。