第一步:将所有矩形框进行初步分类。分类原则是依据矩形框的相似性进行归类。
第二步:计算上步分类后的每一类别的平均矩形框位置,即每一个类别最终对应一个矩形框。
第三部:将第二步得到的矩形框再次进行过滤。过滤原理:1)将每一个类别中矩形框个数较少的类别过滤掉。2)将嵌在大矩形框内部的小矩形框过滤掉。剩下的矩形框为最终融合的结果。
partition函数详解
- template<typename _Tp, class _EqPredicate> int
- partition( const std::vector<_Tp>& _vec, std::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;
- std::vector<int> _nodes(N*2);
- int (*nodes)[2] = (int(*)[2])&_nodes[0];
- // The first O(N) pass: create N single-vertex trees
- // nodes[i][PARENT] = -1表示无父节点,所有节点初始化为单独的节点
- for(i = 0; i < N; i++)
- {
- nodes[i][PARENT]=-1;
- nodes[i][RANK] = 0;
- }
- // The main O(N^2) pass: merge connected components
- // 每一个节点都和其他所有节点比较,看是否属于同一类
- // 属于同一类的判断 predicate(vec[i], vec[j]),predicate为传入的SimilarRects
- // SimilarRects判断两个矩形框的四个相应顶点的差值的绝对值都在deta范围内,则认为属于同一类,否则是不同类
- // 两层for循环和后面的压缩策略保证了最终形成很多类,每一类以根节点为中心,该类的其余节点的父节点指向根节点
- for( i = 0; i < N; i++ )
- {
- int root = i;
- // find root
- // 寻找根节点,每次都是和每个节点对应的根节点比较,如果是单独的节点,根节点就是本身
- while( nodes[root][PARENT] >= 0 )
- 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 )
- root2 = nodes[root2][PARENT];
- // 保证已经连接在同一类的不再连接
- if( root2 != root )
- {
- // unite both trees
- // rank表示级别,根节点rank大为0,普通点rank为0,并且根节点的rank随着连接同级根节点的次数增多而增大
- int rank = nodes[root][RANK], rank2 = nodes[root2][RANK];
- // root为根节点,root2为单独节点,将root2连接到root上,根节点不变
- if( rank > rank2 )
- nodes[root2][PARENT] = root;
- // 当root和root2都为根节点,将root连接到root2,并将root2对应的rank加1,root2为根节点,root为单独点,将root连接
- // 到root2上。二者都将根节点更新为root2
- else
- {
- nodes[root][PARENT] = root2;
- nodes[root2][RANK] += rank == rank2;
- root = root2;
- }
- // 根节点的parent必须小于0
- CV_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
- // 小于0,则已经统计过
- if( nodes[root][RANK] >= 0 )
- nodes[root][RANK] = ~nclasses++;
- // 每个根节点保存着类别ID的非值,其非值小于0
- labels[i] = ~nodes[root][RANK];
- }
- return nclasses;
- }
- } // cv
groupRectangle函数实现矩形框聚合。原因:多尺度检测后,获取的矩形之间会存在重合、重叠和包含关系。因尺度缩放,可能导致同一个目标在多个尺度上被检测出来,故有必要进行融合。OpenCV中实现的融合有两种:1)按权重合并;2)使用Meanshift算法进行合并。
下面是简单的合并,其直接按照位置和大小关系进行合并。其实现主要为:1)多所有矩形按照大小位置合并成不同的类别;2)将同类别中的矩形合并成一个矩形,当不满足给出阈值条件时,矩形被舍弃,否则留下。
- //函数主要功能为对矩形框进行融合操作。
- void groupRectangles(std::vector<Rect>& rectList, int groupThreshold, double eps,
- std::vector<int>* weights, std::vector<double>* levelWeights)
- {
- if( groupThreshold <= 0 || rectList.empty() )
- {
- if( weights )
- {
- size_t i, sz = rectList.size();
- weights->resize(sz);
- for( i = 0; i < sz; i++ )
- (*weights)[i] = 1;
- }
- return;
- }
- std::vector<int> labels;
- // 调用partition函数,将所有的矩形框初步分为几类,其中labels为每个矩形框对应的类别编号,eps为判断两个矩形框是否属于
- // 同一类的控制参数。如果两个矩形框的四个相应顶点的差值的绝对值都在deta范围内,则认为属于同一类,否则是不同类。
- int nclasses = partition(rectList, labels, SimilarRects(eps));
- std::vector<Rect> rrects(nclasses);
- std::vector<int> rweights(nclasses, 0);
- std::vector<int> rejectLevels(nclasses, 0);
- std::vector<double> rejectWeights(nclasses, DBL_MIN);
- int i, j, nlabels = (int)labels.size();
- for( i = 0; i < nlabels; i++ )
- {
- int cls = labels[i];
- rrects[cls].x += rectList[i].x;
- rrects[cls].y += rectList[i].y;
- rrects[cls].width += rectList[i].width;
- rrects[cls].height += rectList[i].height;
- rweights[cls]++;
- }
- bool useDefaultWeights = false;
- if ( levelWeights && weights && !weights->empty() && !levelWeights->empty() )
- {
- for( i = 0; i < nlabels; i++ )
- {
- int cls = labels[i];
- if( (*weights)[i] > rejectLevels[cls] )
- {
- rejectLevels[cls] = (*weights)[i];
- rejectWeights[cls] = (*levelWeights)[i];
- }
- else if( ( (*weights)[i] == rejectLevels[cls] ) && ( (*levelWeights)[i] > rejectWeights[cls] ) )
- rejectWeights[cls] = (*levelWeights)[i];
- }
- }
- else
- useDefaultWeights = true;
- // 计算每一类别的平均矩形框位置,即每一个类别最终对应一个矩形框
- for( i = 0; i < nclasses; i++ )
- {
- Rect r = rrects[i];
- float s = 1.f/rweights[i];
- rrects[i] = Rect(saturate_cast<int>(r.x*s),
- saturate_cast<int>(r.y*s),
- saturate_cast<int>(r.width*s),
- saturate_cast<int>(r.height*s));
- }
- rectList.clear();
- if( weights )
- weights->clear();
- if( levelWeights )
- levelWeights->clear();
- // 再次过滤上面分类中得到的所有矩形框
- for( i = 0; i < nclasses; i++ )
- {
- Rect r1 = rrects[i];
- int n1 = rweights[i];
- double w1 = rejectWeights[i];
- int l1 = rejectLevels[i];
- // filter out rectangles which don't have enough similar rectangles
- // 将每一类别中矩形框个数较少的类别过滤掉。
- if( n1 <= groupThreshold )
- continue;
- // filter out small face rectangles inside large rectangles
- // 将嵌在大矩形框内部的小矩形框过滤掉。最后剩下的矩形框为聚类的结果。
- for( j = 0; j < nclasses; j++ )
- {
- int n2 = rweights[j];
- if( j == i || n2 <= groupThreshold )
- continue;
- Rect r2 = rrects[j];
- int dx = saturate_cast<int>( r2.width * eps );
- int dy = saturate_cast<int>( r2.height * eps );
- if( i != j &&
- r1.x >= r2.x - dx &&
- r1.y >= r2.y - dy &&
- r1.x + r1.width <= r2.x + r2.width + dx &&
- r1.y + r1.height <= r2.y + r2.height + dy &&
- (n2 > std::max(3, n1) || n1 < 3) )
- break;
- }
- if( j == nclasses )
- {
- rectList.push_back(r1);
- if( weights )
- weights->push_back(useDefaultWeights ? n1 : l1);
- if( levelWeights )
- levelWeights->push_back(w1);
- }
- }
- }