理论基础:
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets也叫Union Find Sets)的合并及查询问题。常常在使用中以森林来表示。初始化时,每个元素都是一棵独立的树,合并过程中将具有某种关联关系的树进行合并,即指向规定的一个父节点,最后进行查找过程,判断2个节点是否属于同一个父亲节点,即2个节点是否有联系。
算法主要包括3个处理过程。
(1)进行所有点集的初始化工作,该过程只需在最开始的时候执行一次即可,时间复杂度为O(N)。
(2)查找节点中的所有元素,使用其根节点的父亲节点和其建立直接联系
(3)将其中具有某些联系的元素进行合并,合并到相同的父亲根节点。
算法整体流程图如下:
举个栗子
初始化时,(2,4),(5,7),(1,3),(8,9), (1,2),(5,6), (2,3)为原始相关节点。在合并过程中,将两两有关系的进行合并操作。最终,(2,4) (1,2) (2,3)合并为{1,2,3,4},(5,7) (5,6)合并为{5,6,7},(8,9)没有进行合并,继续保存为{8,9}。
(2,4) {2,4}
(5,7) {2,4}{5,7}
(1,3) {1,3}{2,4} {5,7}
(8,9) {1,3}{2,4} {5,7} {8,9}
(1,2) {1,2,3,4}{5,7} {8,9}
(5,6) {1,2,3,4}{5,6,7} {8,9}
(2,3) {1,2,3,4}{5,6,7} {8,9}
应用背景:
在人脸检测,模式识别等应用中,后续会产生许多的目标区域候选框,当然去掉这些候选框也有很多方法。而其中一种就是将满足某种关系的候选框进行合并,用一个大的候选框来代替之前的2个或者多个候选框。例如,opencv中自带的harrs,hog等就是用的这种方法进行后续处理的。这也就是本文要阐述的方法——并查集(Disjoint Sets)。
当然上面所讲的只是传统的并查集的一种应用。而实际其应用领域还更广。比如,在视频处理领域,可以快速的实现移动目标物(车,行人,其他物体等)的捕捉。真正的实现让运动跟踪来辅助检测识别,让检测识别来指导跟踪,相辅相成,实现高效的智慧城市战略。
这里贴一张应用的图片,图片右下角的一些信息,我已经擦掉。该图片为从视频中每隔一定帧数抽取的。其中蓝色为原始的候选框,红色为融合后的候选框。从融合效果可以看出基本满足多目标的完美捕捉和跟踪。
opencv源码:
- 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
- for(i = 0; i < N; i++)
- {
- nodes[i][PARENT]=-1;
- nodes[i][RANK] = 0;
- }
-
- // The main O(N^2) pass: merge connected components
- 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
- 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;
- }
程序解析:
为了验证本文并查集的融合效果,本文使用点的聚合(PointLike)和长方形框的融合(RectLike)进行双重验证。具体如下所示。
PointLike类,定义了partition需要的()重载操作以及聚合为一类后,相应的Disjoint_set_merge操作。
- class PointLike
- {
- public:
- int thresh;
- public:
- PointLike();
- PointLike(int thresh);
- bool operator()(cv::Point p1,cv::Point p2);
- int Disjoint_set_merge(vector<Point>pts,vector<int>& labels,PointLike rLike);
-
- };
-
- PointLike::PointLike()
- {
- thresh=30;
- }
- PointLike::PointLike(int thresh)
- {
- this->thresh = thresh;
- }
- bool PointLike::operator()(cv::Point p1,cv::Point p2)
- {
- int x = p1.x - p2.x;
- int y = p1.y - p2.y;
- return x*x+y*y <=thresh*thresh;
- }
- int PointLike::Disjoint_set_merge(vector<Point>pts,vector<int>& labels,PointLike pLike)
- { //并查集方法,实现了点集的合并
- //pts为合并前的点,labels为点的标签,p为合并的置信度
-
- int count = cv::partition(pts,labels,pLike);
- return count;
-
- }
PointLike的测试,主要实现在600*600的背景区域,随机产生50个位置的点,当点与点之间的距离小于50就聚合为一类。原始点用红色实心小圆点表示。聚合后的点,每一类用一个随机生成的颜色和空心圆表示,并在旁边附所属类别的数字说明。运行效果图如下图所示,随机生成的50个点被聚合为27个类。
- int main()
- {
- int image_width=600;
- int image_height=600;
- Mat image(image_height,image_width,CV_8UC3);
- image.setTo(0);
- int sample_num=50;
- srand(time(0));//保证每次产生的随机数不一样
- //PointLike的测试
- PointLike pLike(50);
- vector<int> labels;
- vector<Point>pts;
- for (size_t i=0;i<sample_num;i++)
- {
- pts.push_back(Point(rand()%image_width,rand()%image_height));
- }
-
- int count= pLike.Disjoint_set_merge(pts,labels, pLike);
-
- for(size_t i = 0;i<pts.size();i++)
- {
- cv::circle(image,pts[i],2,Scalar(0,0,255),-1);
- }
-
- vector<Scalar> color;
- for (size_t i=0;i<count;i++)
- color.push_back(Scalar(rand()%255,rand()%255,rand()%255));
- if (count!=labels.size())//说明有合并的
- for(size_t i = 0;i<pts.size();i++)
- {
- cv::circle(image,pts[i],10,color[labels[i]]);
- char text[100];
- sprintf(text,"%d",labels[i]);
- putText(image,text,pts[i],CV_FONT_HERSHEY_COMPLEX, 1,color[labels[i]] );
- }
-
- imshow("image",image);
- cout<<"总区域"<<count<<endl;
-
- waitKey(0);
- return 0;
- }
RectLike类的定义,每个变量程序都有说明,包含最核心的partition需要的重载()函数。
- class RectLike{
-
- public:
- double p;//公共区域所占的百分比,超过该比例则合并
- float start_x,start_y,end_x,end_y;//合并后公共区域的起始点
- float w,h;//r1和r2公共区域的长,宽
-
- public:
- RectLike();
- RectLike(double p);
- void overlap(Rect &r1,Rect &r2);
- float overlap_area(Rect r1,Rect r2);//判断框出图像的重叠面积
- bool operator()(cv::Rect r1,cv::Rect r2);
- Rect merge(cv::Rect r1,cv::Rect r2);
- int Disjoint_set_merge(vector<Rect> rects,vector<Rect>& merge_rects,vector<int>& labels,RectLike rLike);
- //并查集方法,只现实了区域的合并
- };
-
- RectLike::RectLike()
- {
- start_x=0;
- end_x=0;
- start_y=0;
- end_y=0;
- p=0.6;
- };
- RectLike::RectLike(double p)
- {this->p = p;}
- void RectLike:: overlap(Rect &r1,Rect &r2)
- {
- this->end_x=max(r1.x+r1.width,r2.x+r2.width);
- this->start_x=min(r1.x,r2.x);
- this->end_y=max(r1.y+r1.height,r2.y+r2.height);
- this->start_y=min(r1.y,r2.y);
- this->w=r1.width+r2.width-(end_x-start_x);
- this->h=r1.height+r2.height-(end_y-start_y);
- }
- float RectLike::overlap_area(Rect r1,Rect r2)
- {//判断框出图像的重叠面积
- overlap(r1, r2);
- if (w<=0||h<=0)
- return 0;
- else
- {
- float area=this->w*this->h;
- return area;
- }
- }
- bool RectLike::operator()(cv::Rect r1,cv::Rect r2)
- {
- //方法1,opencv hog,Cascade使用的合并方法
- double delta = p*(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;
- //方法2,求面积的合并方法
- int area1 = r1.area();
- int area2 = r2.area();
- int area = overlap_area(r1,r2);//相交Rect面积
- return area1<area2?area>=p*area1:area>=p*area2;
- //方法3,投影法
- int xlap=40;int ylap=40;
- int x1=r1.x,x2=r1.x+r1.width,x3=r2.x,x4=r2.x+r2.width;
- int y1=r1.y,y2=r1.y+r1.height,y3=r2.y,y4=r2.y+r2.height;
- if (x1<=x3)
- {
- if ((x2<=x3&&x3-x2<=xlap)||(x1<=x3&&x3<=x2&&x2<=x4)||(x1<=x3&&x3<=x4&&x4<=x2))
- {
- if ((y4<=y1&&y1-y4<=ylap)||(y2<=y3&&y3-y2<=ylap)||(y1<=y3&&y3<=min(y2,y4))||(y3<y1&&y1<=min(y2,y4)))
- {
- return true;
- }
-
- }
- }else
- {
- if ((x4<=x1&&x1-x4<=xlap)||(x3<=x1&&x1<=x4&&x4<=x2)||(x3<=x1&&x1<=x2&&x2<=x4))
- {
- if ((y2<=y3&&y3-y2<=ylap)||(y4<=y1&&y1-y4<=ylap)||(y3<=y1&&y1<=min(y4,y2))||(y1<y3&&y3<=min(y4,y2)))
- {
- return true;
- }
- }
- }
- return false;
- }
- Rect RectLike::merge(cv::Rect r1,cv::Rect r2)
- {
- overlap(r1, r2);
- Rect rect(start_x,start_y,end_x-start_x,end_y-start_y);
- return rect;
- }
- int RectLike::Disjoint_set_merge(vector<Rect> rects,vector<Rect>& merge_rects,vector<int>& labels,RectLike rLike)
- { //并查集方法,只现实了区域的合并
- //rects为合并前的矩形框,merge_rects为合并后矩形框,labels为矩形框的标签,p为合并的置信度
- vector<int>label_temp;
- for (size_t i=0;i<labels.size();i++)
- label_temp.push_back(1);
- int count = cv::partition(rects,labels,rLike);
- //如果没有合并,还是返回原始的rects,labels,如果合并了,返回以0开始的labels
- if (count!=labels.size())//两者不相等,说明进行了合并
- {
-
- for (size_t i=0;i<labels.size();i++)
- {
- if (label_temp[i]==0) continue;
- Rect rect_temp=rects[i];
- label_temp[i]=0;
- for(size_t j=i+1;j<labels.size();j++)
- {
- if(labels[i]==labels[j])
- {
- rect_temp=rLike.merge(rect_temp,rects[j]);
- label_temp[j]=0;
- }
-
- }
- merge_rects.push_back(rect_temp);
- }
-
- }
- return count;
-
- }
RectLike类的测试,由于()的实现采用了3种不同的方法,所以这里对三种实现的测试采样固定输入的数据进行测试。其中,黄色为原始框,红色为合并后的框。
方法1,为opencv的源码,hog,Cascade等对于检测后窗口的合并就是用的该方法。思想为先定义一个delta,当两个矩形框左上角x的差,y的差,矩形框右下角x的差,y的差都小于该delta就进行合并。测试数据的3个矩形框,分别为(80,80,180,120),(130,130,180,120) ,(300,300,50,50)。
方法2,为面积合并法,当相交的面积大于其中面积较小的矩形的面积*p,就将当前的2个矩形进行合并。测试数据的3个矩形框,分别为(80,80,180,120),(100,100,180,120) ,(300,300,50,50)。
方法3,为投影法,当矩形框在x轴的投影小于xlap并且在y轴的投影小于ylap,就将这2个比较的框进行合并。测试数据的3个矩形框,分别为(80,80,180,120),(100,270,180,120) ,(300,300,50,50)。
- int main()
- {
- int image_width=600;
- int image_height=600;
- Mat image(image_height,image_width,CV_8UC3);
- image.setTo(0);
- int sample_num=5;
- srand(time(0));//保证每次产生的随机数不一样
-
- //RectLike的测试
-
- vector<Rect> rects;
- vector<Rect> merge_rects;
- vector<int>labels;
-
- for (size_t i=0;i<sample_num;i++)
- {
- int x=rand()%(image_width-10);
- int y=rand()%(image_height-10);
- int width=abs(rand()%(image_width-x-10));
- int height=abs(rand()%(image_height-y-10));
- rects.push_back(Rect(x,y,width,height));
- labels.push_back(i);
- }
-
- RectLike rLike(0.4);
- int count=rLike.Disjoint_set_merge( rects,merge_rects,labels,rLike);
-
- for(size_t i = 0;i<rects.size();i++)
- {
- cv::rectangle(image,rects[i],Scalar(0,255,255),6);
- }
- for(size_t i = 0;i<merge_rects.size();i++)
- {
- cv::rectangle(image,merge_rects[i],Scalar(0,0,255),2);
- }
- imshow("image",image);
- cout<<"总区域"<<count<<endl;
-
- waitKey(0);
- return 0;
- }
PS:
对于Opencv中合并其实就是基于上面的模版类partition实现的。当然这个程序也写的很牛,直接用循环代替了递归,实现了很好的融合效果。在实际应用中,好多开源的国外大牛的程序其实都是自己实现的类似这样的功能,包括partition和后期的融合等。
本人也自己写了一个融合的子函数。有些时候真的觉得思维是很重要的。刚开始也是想的递归,后来改成了下面的程序,该程序的巧妙之处就在于,后面只要有一个合并的,就对已经循环的Rect中标签没有排除的重新进行扫描,从而改变了递归策略。下面的这一个函数接口实现了上面所有的功能。
通过将该函数接口和上文的RectLike融合中的方法3进行对比,两者实现了同样的融合效果。并且实际经过多段视频的测试,证明该函数的正确性。
其中,boundRect 待合并的长方形框,boundRectFlag待合并长方形框的标志,和boundRect维度一样,初始化全部为1,MergeBoundRect合并后的框,xlap,x方向小于该值就合并,ylap,y方向小于该值就合并。
合并的示意图如下:
- void MergeContours(vector<Rect> boundRect,vector<int>& boundRectFlag,vector<Rect>& MergeBoundRect,int xlap=40,int ylap=40)
- {
- for (int i=0;i<boundRect.size();i++)
- {
- if(boundRectFlag[i]==0)
- continue;
- Rect recttmp=boundRect[i];
- boundRectFlag[i]=0;
- for (int j=i+1;j<boundRect.size();j++)
- {
- if (boundRectFlag[j]!=0)
- {
- int x1=recttmp.x,x2=recttmp.x+recttmp.width,x3=boundRect[j].x,x4=boundRect[j].x+boundRect[j].width;
- int y1=recttmp.y,y2=recttmp.y+recttmp.height,y3=boundRect[j].y,y4=boundRect[j].y+boundRect[j].height;
- if (x1<=x3)
- {
- if ((x2<=x3&&x3-x2<=xlap)||(x1<=x3&&x3<=x2&&x2<=x4)||(x1<=x3&&x3<=x4&&x4<=x2))
- {
- if ((y4<=y1&&y1-y4<=ylap)||(y2<=y3&&y3-y2<=ylap)||(y1<=y3&&y3<=min(y2,y4))||(y3<y1&&y1<=min(y2,y4)))
- {
- recttmp.x=min(x1,x3);recttmp.width=max(x2,x4)-min(x1,x3);
- recttmp.y=min(y1,y3);recttmp.height=max(y2,y4)-min(y1,y3);
- boundRectFlag[j]=0;
- j=i;
- }
-
- }
- }else
- {
- if ((x4<=x1&&x1-x4<=xlap)||(x3<=x1&&x1<=x4&&x4<=x2)||(x3<=x1&&x1<=x2&&x2<=x4))
- {
- if ((y2<=y3&&y3-y2<=ylap)||(y4<=y1&&y1-y4<=ylap)||(y3<=y1&&y1<=min(y4,y2))||(y1<y3&&y3<=min(y4,y2)))
- {
- recttmp.x=min(x3,x1);recttmp.width=max(x2,x4)-min(x3,x1);
- recttmp.y=min(y3,y1);recttmp.height=max(y4,y2)-min(y3,y1);
- boundRectFlag[j]=0;
- j=i;
- }
- }
- }
- }
-
-
- }
- MergeBoundRect.push_back(recttmp);
-
- }
- }