理论基础:
并查集是一种树型的数据结构,用于处理一些不相交集合(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);
}
}
本文介绍并查集数据结构的原理及其在图像处理领域的应用,包括人脸检测、模式识别等场景下的目标区域合并,通过具体示例展示了并查集算法的实现过程。
2037

被折叠的 条评论
为什么被折叠?



