目标融合之 ——并查集(Disjoint Sets)||(Union Find Sets)

本文介绍并查集数据结构的原理及其在图像处理领域的应用,包括人脸检测、模式识别等场景下的目标区域合并,通过具体示例展示了并查集算法的实现过程。

理论基础:

       并查集是一种树型的数据结构,用于处理一些不相交集合(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;
}
本文程序的github下载链接为, https://github.com/watersink/Disjoint-Sets-Union-Find,最后希望本文会对您产生一些帮助。


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);

	}
}









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值