SLIC superpixel实现分析



http://infoscience.epfl.ch/record/149300这是SLIC算法的官网,网站有和SLIC相关的资源。

SLIC主要运用K-means聚类算法进行超像素的处理,聚类算法中的距离度量不仅仅包括颜色空间的颜色距离还包括像素坐标的欧氏距离。所以K-means聚类的中心点由五维向量组成。其中包括,记录LAB颜色空间下的像素以及该像素点的XY坐标,由于XY坐标不能和颜色空间直接进行计算,所以添加了一个紧密度的参数。

算法的实现过程:

1 对于一个包含N个像素的图像而言,如果对这个图像聚类为K个超像素块,那么每个超像素的范围大小为N/K个。如果每个超像素区域长和宽都均匀分布的话,那么每个像素的跨度为STEP=sqrt(N/K)

2 利用上面的数据对K-means聚类中心点尽心初始化,不过在初始化之后还需要注意一点。为了保证选出来的中心点不在像素的边缘上所以需要对对求出的中心点的便于梯度进行一个对比排除。就是和周围的8个像素点尽心比较直到找到像素梯度最小的点作为中心点。初始化之后就进行k-means聚类操作。

3 聚类之后还需要有一个增强处理,以便于将一个区域围起来的独立的像素点给归并到某一类中。

下面是整个代码的执行过程:

void SLIC::DoSuperpixelSegmentation_ForGivenNumberOfSuperpixels(
    const unsigned int*                             ubuff,
	const int					width,
	const int					height,
	int*&						klabels,
	int&						numlabels,
	const int&					K,//required number of superpixels
    const double&                                   compactness)//weight given to spatial distance
{
    const int superpixelsize = 0.5+double(width*height)/double(K);//每一个超像素的大小
    DoSuperpixelSegmentation_ForGivenSuperpixelSize(ubuff,width,height,klabels,numlabels,superpixelsize,compactness);
}

函数的参数中,ubuff是图像的像素内存指针,width和height是图像的高度,klabels和ubuff大小一样,用于返回每个像素所代表的label——也就是聚合之后属于哪一个类。numlabels返回实际聚类之后得到的数目,而K则是用户希望聚类的数目,compactness则表示距离转化为颜色空间的比例。函数的起始求出向上取整的颜色空间区域的大小。

void SLIC::DoSuperpixelSegmentation_ForGivenSuperpixelSize(
    const unsigned int*         ubuff,
	const int					width,
	const int					height,
	int*&						klabels,
	int&						numlabels,
    const int&					superpixelsize,
    const double&               compactness)
{
    	const int STEP = sqrt(double(superpixelsize))+0.5;
	vector<double> kseedsl(0);
	vector<double> kseedsa(0);
	vector<double> kseedsb(0);
	vector<double> kseedsx(0);
	vector<double> kseedsy(0);
	m_width  = width;
	m_height = height;
	int sz = m_width*m_height;
	for( int s = 0; s < sz; s++ ) klabels[s] = -1;
        DoRGBtoLABConversion(ubuff, m_lvec, m_avec, m_bvec);
        bool perturbseeds(true);//perturb seeds is not absolutely necessary, one can set this flag to false
	vector<double> edgemag(0);
	if(perturbseeds) DetectLabEdges(m_lvec, m_avec, m_bvec, m_width, m_height, edgemag);
	GetLABXYSeeds_ForGivenStepSize(kseedsl,kseedsa,kseedsb,kseedsx,kseedsy,STEP,perturbseeds,edgemag);
	PerformSuperpixelSLIC(kseedsl,kseedsa, kseedsb, kseedsx, kseedsy, klabels, STEP, edgemag,compactness);
	numlabels = kseedsl.size();
	int* nlabels = new int[sz];
	EnforceLabelConnectivity(klabels,m_width, m_height, nlabels, numlabels, double(sz)/double(STEP*STEP));
	{for(int i = 0; i < sz; i++ ) klabels[i] = nlabels[i];}
	if(nlabels) delete [] nlabels;
}

真正的函数实现是放在上面这个函数中的,首先定义的是聚类的中心点。然后对整个图像进行颜色空间的转换,将整个颜色空间由BGR转化为LAB,其中m_lvec,m_avec和m_bvec分表输出转换后的L,A和B颜色空间。然后根据用户的设定进行边缘检测,检测出来的边缘用于调整初始化时的中心点。经过上面的处理之后,就对需要聚类的中心点进行初始化。在初始化之后就可以执行聚类操作了,在聚类之后进行第三步增强处理,将一个区域范围内的没有被聚类的点和相邻的类进行一个聚类操作。

void SLIC::DetectLabEdges(
	const double*				lvec,
	const double*				avec,
	const double*				bvec,
	const int&					width,
	const int&					height,
	vector<double>&				edges)
{
	int sz = width*height;
	edges.resize(sz,0);
	for( int j = 1; j < height-1; j++ )
	{
		for( int k = 1; k < width-1; k++ )
		{
			int i = j*width+k;
			double dx = (lvec[i-1]-lvec[i+1])*(lvec[i-1]-lvec[i+1]) +
						(avec[i-1]-avec[i+1])*(avec[i-1]-avec[i+1]) +
						(bvec[i-1]-bvec[i+1])*(bvec[i-1]-bvec[i+1]);
			double dy = (lvec[i-width]-lvec[i+width])*(lvec[i-width]-lvec[i+width]) +
						(avec[i-width]-avec[i+width])*(avec[i-width]-avec[i+width]) +
						(bvec[i-width]-bvec[i+width])*(bvec[i-width]-bvec[i+width]);
			edges[i] = dx*dx + dy*dy;
		}
	}
}

得到边缘的操作很简单,就是利用在X和Y方向上的求导然后得到斜线上的斜率,这些斜率全部存放到edegs里面输出。

void SLIC::GetLABXYSeeds_ForGivenStepSize(
	vector<double>&				kseedsl,
	vector<double>&				kseedsa,
	vector<double>&				kseedsb,
	vector<double>&				kseedsx,
	vector<double>&				kseedsy,
    const int&					STEP,
    const bool&					perturbseeds,
    const vector<double>&       edgemag)
{
    	const bool hexgrid = false;
	int numseeds(0);
	int n(0);
	int xstrips = (0.5+double(m_width)/double(STEP));//向上取整
	int ystrips = (0.5+double(m_height)/double(STEP));
        int xerr = m_width  - STEP*xstrips;if(xerr < 0){xstrips--;xerr = m_width - STEP*xstrips;}
        int yerr = m_height - STEP*ystrips;if(yerr < 0){ystrips--;yerr = m_height- STEP*ystrips;}
	double xerrperstrip = double(xerr)/double(xstrips);
	double yerrperstrip = double(yerr)/double(ystrips);
	int xoff = STEP/2;
	int yoff = STEP/2;
	numseeds = xstrips*ystrips;//表明生成numseeds个超像素块
	kseedsl.resize(numseeds);
	kseedsa.resize(numseeds);
	kseedsb.resize(numseeds);
	kseedsx.resize(numseeds);
	kseedsy.resize(numseeds);
	for( int y = 0; y < ystrips; y++ )
	{
		int ye = y*yerrperstrip;
		for( int x = 0; x < xstrips; x++ )
		{
			int xe = x*xerrperstrip;
                        int seedx = (x*STEP+xoff+xe);
                        int seedy = (y*STEP+yoff+ye);
           		int i = seedy*m_width + seedx;
			kseedsl[n] = m_lvec[i];
			kseedsa[n] = m_avec[i];
			kseedsb[n] = m_bvec[i];
            		kseedsx[n] = seedx;
            		kseedsy[n] = seedy;
			n++;
		}
	}//初始化聚类的中心点
	if(perturbseeds)
	{
		PerturbSeeds(kseedsl, kseedsa, kseedsb, kseedsx, kseedsy, edgemag);
	}
}

首先,函数根据传递进来的跨度求出在宽度一定的图像中可以有多少个区域,然后再求出在高度一定的图像中有多少个区域,总共区域就是这个两个数的乘积。接下来求两个偏移,第一个是全局的偏移,这个偏移会随着整个图像的中块的位置而累积,还有一个偏移是指本身需要在块的中心点开始聚类。根据上面的解释,后面的一个两层循环就很好理解了。经过上面的初始化就得到了初始的聚类中心点,然后进行一个调整,使得所有的像素都不会在像素的边缘点上。这个操作通过在一个3*3的小区域内求出最小的梯度点更新初始的像素点中心。因为聚类必须具备的一个很好地性质就是不能讲图像中的一个目标给一分为二。

void SLIC::PerturbSeeds(
	vector<double>&				kseedsl,
	vector<double>&				kseedsa,
	vector<double>&				kseedsb,
	vector<double>&				kseedsx,
	vector<double>&				kseedsy,
        const vector<double>&                   edges)
{
	const int dx8[8] = {-1, -1,  0,  1, 1, 1, 0, -1};
	const int dy8[8] = { 0, -1, -1, -1, 0, 1, 1,  1};
	int numseeds = kseedsl.size();
	for( int n = 0; n < numseeds; n++ )
	{
		int ox = kseedsx[n];//original x
		int oy = kseedsy[n];//original y
		int oind = oy*m_width + ox;
		int storeind = oind;
		for( int i = 0; i < 8; i++ )
		{
			int nx = ox+dx8[i];//new x
			int ny = oy+dy8[i];//new y
			if( nx >= 0 && nx < m_width && ny >= 0 && ny < m_height)
			{
				int nind = ny*m_width + nx;
				if( edges[nind] < edges[storeind])
				{
					storeind = nind;
				}
			}
		}
		if(storeind != oind)
		{
			kseedsx[n] = storeind%m_width;
			kseedsy[n] = storeind/m_width;
			kseedsl[n] = m_lvec[storeind];
			kseedsa[n] = m_avec[storeind];
			kseedsb[n] = m_bvec[storeind];
		}
	}
}

上面是一个对初始中心点的更新,很简单,就是根据之前计算出来的梯度值来进行更新。

void SLIC::PerformSuperpixelSLIC(
	vector<double>&				kseedsl,
	vector<double>&				kseedsa,
	vector<double>&				kseedsb,
	vector<double>&				kseedsx,
	vector<double>&				kseedsy,
        int*&					klabels,
        const int&				STEP,
        const vector<double>&                   edgemag,
	const double&				M)
{
	int sz = m_width*m_height;
	const int numk = kseedsl.size();
	int offset = STEP;
	vector<double> clustersize(numk, 0);
	vector<double> inv(numk, 0);//to store 1/clustersize[k] values
	vector<double> sigmal(numk, 0);
	vector<double> sigmaa(numk, 0);
	vector<double> sigmab(numk, 0);
	vector<double> sigmax(numk, 0);
	vector<double> sigmay(numk, 0);
	vector<double> distvec(sz, DBL_MAX);
	double invwt = 1.0/((STEP/M)*(STEP/M));
	int x1, y1, x2, y2;
	double l, a, b;
	double dist;
	double distxy;
	for( int itr = 0; itr < 10; itr++ )
	{
		distvec.assign(sz, DBL_MAX);
		for( int n = 0; n < numk; n++ )
		{
                        y1 = max(0.0,			kseedsy[n]-offset);
                        y2 = min((double)m_height,	kseedsy[n]+offset);
                        x1 = max(0.0,			kseedsx[n]-offset);
                        x2 = min((double)m_width,	kseedsx[n]+offset);
			for( int y = y1; y < y2; y++ )
			{
				for( int x = x1; x < x2; x++ )
				{
					int i = y*m_width + x;
					l = m_lvec[i];
					a = m_avec[i];
					b = m_bvec[i];
					dist =(l-kseedsl[n])*(l-kseedsl[n])+(a-kseedsa[n])*(a-kseedsa[n])+(b-kseedsb[n])*(b-kseedsb[n]);
					distxy=(x-kseedsx[n])*(x-kseedsx[n])+(y -kseedsy[n])*(y - kseedsy[n]);
					dist += distxy*invwt;
					if( dist < distvec[i] )
					{
						distvec[i] = dist;//label的距离
						klabels[i]  = n;//表明属于哪个label
					}
				}
			}
		}
		sigmal.assign(numk, 0);
		sigmaa.assign(numk, 0);
		sigmab.assign(numk, 0);
		sigmax.assign(numk, 0);
		sigmay.assign(numk, 0);
		clustersize.assign(numk, 0);
		int ind(0);
		for( int r = 0; r < m_height; r++ )
		{
			for( int c = 0; c < m_width; c++ )
			{
				sigmal[klabels[ind]] += m_lvec[ind];//统计当前的l颜色通道的数据
				sigmaa[klabels[ind]] += m_avec[ind];
				sigmab[klabels[ind]] += m_bvec[ind];
				sigmax[klabels[ind]] += c;
				sigmay[klabels[ind]] += r;
				clustersize[klabels[ind]] += 1.0;//相应的label中的计数增加1
				ind++;
			}
		}
		for( int k = 0; k < numk; k++ )
		{
			if( clustersize[k] <= 0 ) clustersize[k] = 1;
			inv[k] = 1.0/clustersize[k];//computing inverse now to multiply, than divide later
		}
		for( int k = 0; k < numk; k++ )
		{
			kseedsl[k] = sigmal[k]*inv[k];//更新中心点
			kseedsa[k] = sigmaa[k]*inv[k];
			kseedsb[k] = sigmab[k]*inv[k];
			kseedsx[k] = sigmax[k]*inv[k];
			kseedsy[k] = sigmay[k]*inv[k];
		}
	}
}

上面的函数实现k-means聚类操作,首先给定中心点然后计算最短距离,然后根据计算得到的最短距离更新中心点。首先定义两个数据来辅助聚类操作,一个用于计数一个类中存在多少个像素点,另一个用于得到前者的倒数,用于进行归一化处理。整个聚类循环包含10次,这个是可以进行修改的。循环起始位置先将整个距离初始化为最长距离,然后再计算当中更新最短的距离。首先根据中心点的起始范围计算聚类的起始位置,跨度为2STEP*2STEP。然后计算颜色空间和像素位置的欧氏距离,其中位置距离需要进行一个调整,而不是直接作为欧氏距离使用。根据新计算出来的距离和类别对坐标和颜色进行更新,然后进行下一轮聚类操作。

void SLIC::EnforceLabelConnectivity(
	const int*					labels,
	const int					width,
	const int					height,
	int*&						nlabels,//new labels
	int&						numlabels,
	const int&					K) //the number of superpixels desired by the user
{
	const int dx4[4] = {-1,  0,  1,  0};//相应的像素的四个方向,左右上下
	const int dy4[4] = { 0, -1,  0,  1};
	const int sz = width*height;
	const int SUPSZ = sz/K;//超像素大小
	for( int i = 0; i < sz; i++ ) nlabels[i] = -1;
	int label(0);
	int* xvec = new int[sz];
	int* yvec = new int[sz];
	int oindex(0);
	int adjlabel(0);//adjacent label
	for( int j = 0; j < height; j++ )
	{
		for( int k = 0; k < width; k++ )
		{
			if( 0 > nlabels[oindex] )//找出相应的位置的label
			{
				nlabels[oindex] = label;
				xvec[0] = k;
				yvec[0] = j;
				for( int n = 0; n < 4; n++ )
				{
					int x = xvec[0] + dx4[n];
					int y = yvec[0] + dy4[n];
					if( (x >= 0 && x < width) && (y >= 0 && y < height) )
					{
						int nindex = y*width + x;
						if(nlabels[nindex] >= 0) adjlabel = nlabels[nindex];
					}
				}
				int count(1);
				for( int c = 0; c < count; c++ )
				{
					for( int n = 0; n < 4; n++ )
					{
						int x = xvec[c] + dx4[n];
						int y = yvec[c] + dy4[n];
						if( (x >= 0 && x < width) && (y >= 0 && y < height) )
						{
							int nindex = y*width + x;
							if(0>nlabels[nindex]&&labels[oindex]==labels[nindex] )
							{
								xvec[count] = x;
								yvec[count] = y;
								nlabels[nindex] = label;
								count++;
							}
						}
					}
				}
				if(count <= SUPSZ >> 2)//搜索范围为以某一个像素为中心点的2倍范围
				{
					for( int c = 0; c < count; c++ )
					{
						int ind = yvec[c]*width+xvec[c];
						nlabels[ind] = adjlabel;//将相应的label设置为附近的标签
					}
					label--;
				}
				label++;
			}
			oindex++;
		}
	}
	numlabels = label;
	if(xvec) delete [] xvec;
	if(yvec) delete [] yvec;
}

循环遍历每一个标签,使得每一个标签都有类别——当label大于0的时候,则表明存在标签否则不存在标签。其中,根据k和j作为搜索的起点,搜索依据是这个像素点附近的标签,如果这个像素点附近存在属于某一个类别的标签,则首先将其保存下来以便于后面的合并处理。否则根据这个起点开始搜索,搜索的依据是第一这个像素点还没有被搜索到,第二这个点的像素标签和其实的标签一样。更新标签之后,下一步就是看看是否处理了足够的标签,因为以一个像素四个方位的像素点进行处理,所以区域不能小于等于超像素值的1/4,如果小于这个值表明聚类还不够彻底,所以需要将标签递减一位表示下面的处理还是从当前的像素开始,经过这样的处理之后整个像素就被聚类了。

完整的代码可以在我资源页下载。




你可能感兴趣的:(C++,opencv,superpixel)