Haar+Adaboost级联分类器分解(三):利用并查集合并检测结果窗口

转载:http://www.aichengxu.com/view/1501260

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

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





图1 检测结果合并窗口前后对比图
(一)并查集(Union-Set)[/align]
在了解如何合并窗口前,先来了解一种数据结构——并查集。

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



图2江湖大侠关系图(箭头表示上一级)
如图2,现在武当派和明教分别推举了张三丰和张无忌作为老大。当帮派很大时组,每个大侠无法完整记住自己所在帮派的组织结构,那么他们只需要记住自己的上一级是谁,一级一级往上问就知道老大是谁了。某日,宋青书和殷梨亭在武当山门口遇到了,那么宋青书问宋远桥后得知自己的老大是张三丰,而殷梨亭的老大也是张三丰,那么他俩肯定是同门,必须不能动手了。而当宋青书遇到陈友谅时,一级一级向上询问后发现老大不是一个人,那就要拔剑分个你死我活。
在武林中,仅仅结成帮派是不够的,还需要通过其他关系组建帮派联盟扩大势力。既然杨不悔嫁给了殷梨亭,不妨直接设将明教老大张无忌的上级设置为武当派老大张三丰,这样就可以将明教和武当组成一个更大的联盟(如图2红色虚线,这里只关心数据的代表,忽略数据的内部结构)。例如,以后当宋青书和范右使相遇,一级一级往上问后发现老大都是张三丰,他俩就知道自己是一伙人了。但是,每次宋青书和范右使相遇,都像这样一级一级往上问速度很慢,而且次数多了上级大侠也会不耐烦。为了解决这个问题,需要压缩路径。当宋青书和范右使知道自己的老大是张三丰时,可以把自己的上司改为张三丰,这样就不必每次去问上级了,同时路径上的上级大侠也都把自己的上级改为老大张三丰。以范右使为例,压缩路径过程如下:

1.范右使问张无忌:我的老大是谁
2.张无忌的上级就是老大张三丰,所以张无忌答复范右使:你的老大是张三丰
3.范右使知道自己的老大是张三丰,然后把自己的上级改为张三丰

宋青书询问老大过程类似。当宋青书与范右使相遇后,问完老大后压缩路径结果如图3,下次他们遇到不用问上级就能就知道知道自己的老大是谁了。



图3压缩路径后的江湖大侠关系图

并查集保持一组不相交的动态集合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)。并查集伪代码如下:
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]


其中,MAKE-SET函数用于在无序数据中初始化并查集数据结构,将每个结点父结点设为其本身;UNION函数通过调用LINK和FIND-SET实现带压缩路径的并查集合并;LINK函数通过秩进行并查集合并;FIND-SET是带压缩路径的寻找结点代表的函数。
还是以图2为例说明UNION函数。上次直接将明教老大张无忌的上级设置为了武当老大张三丰(如上文蓝色字体),导致了后续宋青书和范右使见面时查询老大路径太长。本着拒绝拖延症的原则,希望在合并明教和武当派时就提前压缩路径,不把问题留给宋青书和范右使。既然上面伪代码中的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[张无忌]←张三丰,所以最终结果图为图4(注意:图3是查询时压缩路径,图4是合并时压缩路径)。



图4

如果还有不明白的地方,建议查阅《算法导论》中的第21章:《用于不相交的数据结构》。

(二)利用并查集合并检测结果窗口

为了将并查集利用到合并窗口中,首先要定义窗口相似函数,即定义当前的2个窗口是不是“一伙人”。在OpenCV中,图像中的矩形窗口一般用Rect结构体表示,其包含x,y,width,height共4个成员变量,分别代表窗口的左上角点x坐标、y坐标、宽度和高度。下面代码定义了窗口相似函数SimilarRects::operator(),当2个窗口r1和r2位置很接近时返回TRUE,通过SimilarRects::operator()就可以将图1那些重叠的窗口合并在“一伙人”中。
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. 之后剩下若干组由大量重叠窗口组成的大“团伙”,分别求每个“团伙”中的所有窗口位置的平均值作为最终检测结果。

       下面的展示了OpenCV实现步骤1-2并查集归类窗口的代码。在之前算法描述中为了清晰简洁,使用递归实现了整个并查集;但在实际中递归需要保存现场并进行压栈,开销极大,所以OpenCV使用循环替代了递归。

// 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"
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;
        nodes[i][RANK] = 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 ) // 即LINK(root, root2)
            {
                // unite both trees
                int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];
                if( rank > rank2 )
                    nodes[root2][PARENT] = root;
                else
                {
                    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等.《算法导论》
[2] http://blog.csdn.net/dellaserss/article/details/7724401

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值