• 【图像算法】彩色图像分割专题七:基于分水岭的彩色分割


     ------------------------------------------------------------------------------------------------------------------------

    【图像算法】彩色图像分割专题七:基于分水岭的彩色分割

          SkySeraph July 7th 2011  HQU

    Email:zgzhaobo@gmail.com    QQ:452728574

    Latest Modified Date:July 7th 2011 HQU

    ------------------------------------------------------------------------------------------------------------------------

    》原理

    分水岭算法有好好几种实现算法,拓扑学,形态学,浸水模拟和降水模拟等,最常用的就是Soille和Vincent首先提出的模拟浸没的算法,该算法分割结果效果好,速度快,具有较强的实用性,其简要概括如下。其它更多细节见后文给出的参考资料,不再细述。

    Vincent和Soille算法包括2个部分:第一个部分是排序;第二部分为泛洪。算法描述如下:

    (1)将根据图像中每个像素的RGB值根据公式Gray=0.299R+0.587G+0.114B得到与之对应的灰度值。

    (2)根据Sobel算子得到图像的梯度。(边缘像素的梯度为其邻域像素的梯度,梯度范围为0-255)

    (3)对梯度进行从小到大的排序,相同的梯度为同一个梯度层级。

    (4)处理第一个梯度层级所有的像素,如果其邻域已经被标识属于某一个区域,则将这个像素加入一个先进先出的队列。

    (5)先进先出队列非空时,弹出第一个元素。扫描该像素的邻域像素,如果其邻域像素的梯度属于同一层(梯度相等),则根据邻域像素的标识来刷新该像素的标识。一直循环到队列为空。

    (6)再次扫描当前梯度层级的像素,如果还有像素未被标识,说明它是一个新的极小区域,则当前区域的值(当前区域的值从0开始计数)加1后赋值给该为标识的像素。然后从该像素出发继续执行步骤5的泛洪直至没有新的极小区域。

    (7)返回步骤4,处理下一个梯度层级的像素,直至所有梯度层级的像素都被处理。流程如下:

    ------------------------------------------------------------------------------------------------------------------------

    》源码

    核心代码(部分源码来源网络,修改并做了很详细的注释)

    void MyWatershed::WatershedSegmentVincent(IplImage* img) 
    //分水岭分割:img为彩色
    {	
    	int x,y,i; //循环
    
    	IplImage* pSrc = NULL;
    	pSrc = cvCreateImage(cvGetSize(img),img->depth,img->nChannels);
    	cvCopyImage(img,pSrc);
    	int width = pSrc->width;			//图像宽度
    	int height = pSrc->height;			//图像高度
    	int depth = pSrc->depth;			//图像位深(IPL_DEPTH_8U...)
    	int channels = pSrc->nChannels;		//图像通道数(1、2、3、4)
    	int imgSize = pSrc->imageSize;		//图像大小 imageSize = height*widthStep
    	int step = pSrc->widthStep/sizeof(uchar);    
    	uchar* data    = (uchar *)pSrc->imageData;
    	int imageLen = width*height;	
    /*
    	cvNamedWindow("src");
    	cvShowImage("src",pSrc);
    	*/
    	
    	/*
    	//===================通过Sobel求取梯度图像====================//
    	//  彩色转灰度
    	IplImage* gray  = cvCreateImage(cvGetSize(pSrc),pSrc->depth,1);
    	cvCvtColor(pSrc,gray,CV_BGR2GRAY);	
    	cvNamedWindow("gray");
    	cvShowImage("gray",gray);
    	
    	//  Sobel
    	IplImage* sobel = cvCreateImage(cvGetSize(pSrc),IPL_DEPTH_16S,1); //IPL_DEPTH_16S: 以Sobel方式求完导数后会有负值,还有会大于255的值
    	cvSobel(gray,sobel,1,1,3);
    	IplImage *sobel8u=cvCreateImage(cvGetSize(sobel),IPL_DEPTH_8U,1);
    	cvConvertScaleAbs(sobel,sobel8u,1,0); //转为8位无符号
    	
    	//  临时显示sobel
    	cvNamedWindow("sobel8u");
    	cvShowImage("sobel8u",sobel8u);	*/
    	
    	
    	//===================① 获取原彩色图像的梯度值(先转灰度)===================//
    	int* deltar = new INT[imageLen]; //梯度模数组
    	//FLOAT* deltasita = new FLOAT[imgSize];//梯度角度数组;
    	//  彩色转灰度
    	IplImage* gray  = cvCreateImage(cvGetSize(pSrc),pSrc->depth,1);
    	cvCvtColor(pSrc,gray,CV_BGR2GRAY);	
    	//  求梯度
    	GetGra(gray,deltar);
    	//GetGra2(sobel8u,deltar);
    	
    
    	//===================② 梯度值排序===================//
    	//以下统计各梯度频率
    	MyImageGraPtWatershed*  graposarr = new MyImageGraPtWatershed[imageLen];
    	INT*  gradientfre = new INT[256];//图像中各点梯度值频率;
    	INT*  gradientadd = new INT[257];//各梯度起终位置;
    	memset( gradientfre, 0, 256*sizeof(INT));
    	memset( gradientadd, 0, 257*sizeof(INT));
    	
    	LONG xstart, imagepos, deltapos;
    	xstart = imagepos = deltapos = 0;
    	for (y=0; y<height; y++)
    	{
    		for ( x=0; x<width; x++)
    		{
    			xstart = y*width;
    			deltapos = xstart + x;
    			if (deltar[deltapos]>255)
    			{
    				deltar[deltapos] = 255;
    			}
    			INT tempi = (INT)(deltar[deltapos]);
    			gradientfre[tempi] ++;//灰度值频率;
    		}
    	}
    	
    	//统计各梯度的累加概率;
    	INT added = 0;
    	gradientadd[0] = 0;//第一个起始位置为0;
    	for (INT ii=1; ii<256; ii++)
    	{
    		added += gradientfre[ii-1];
    		gradientadd[ii] = added;
    	}
    	gradientadd[256] = imageLen;//最后位置;
    	
    	memset( gradientfre, 0, 256*sizeof(INT)); //清零,下面用作某梯度内的指针;
    	
    	//自左上至右下sorting....
    	for (y=0; y<height; y++)
    	{
    		xstart = y*width;
    		for ( x=0; x<width; x++)
    		{
    			deltapos = xstart + x;
    			INT tempi = (INT)(deltar[deltapos]);//当前点的梯度值,由于前面的步骤,最大只能为255;
    			//根据梯度值决定在排序数组中的位置;
    			INT tempos = gradientadd[tempi] + gradientfre[tempi];
    			gradientfre[tempi] ++;//梯度内指针后移;
    			graposarr[tempos].gradient = tempi;	//根据当前点的梯度将该点信息放后排序后数组中的合适位置中去;		
    			graposarr[tempos].x = x;
    			graposarr[tempos].y = y;
    		}
    	}
    
    
    	//===================③ 泛洪得每个像素所属区域标记flag=================//
    	INT rgnumber = 0;					//分割后的区域数
    	INT*   flag = new INT[imageLen];		//各点标识数组
    	Flood(graposarr,gradientadd,0,255,flag,rgnumber,width,height);
    	
    	
    	//===================④ 由flag标记求各个区域的LUV均值=================//
    	//以下准备计算各个区域的LUV均值;
    	//分割后各个区的一些统计信息,第一个元素不用,图像中各点所属区域的信息存放在flag数组中
    	MyRgnInfoWatershed*  rginfoarr = new MyRgnInfoWatershed[rgnumber+1];	
    	//清空该数组
    	for (i=0; i<rgnumber+1; i++) //LUV
    	{
    		rginfoarr[i].isflag = FALSE;
    		rginfoarr[i].ptcount = 0;
    		rginfoarr[i].l = 0;
    		rginfoarr[i].u = 0;
    		rginfoarr[i].v = 0;
    	}
    	
    	//以下保存LUV数据;
    	if ( luvData != NULL )
    	{
    		delete [] luvData;
    		luvData = NULL;
    	}
    	luvData = new MyLUV[width*height];
    	pMyColorSpace.RgbtoLuvPcm(data, width, height, luvData);
    
    	for (y=0; y<height; y++)
    	{
    		xstart = y*width;
    		for ( x=0; x<width; x++)
    		{
    			INT pos = xstart + x;
    			INT rgid = flag[pos];//当前位置点所属区域在区统计信息数组中的位置
    			//以下将该点的信息加到其所属区信息中去
    			rginfoarr[rgid].ptcount ++;
    			rginfoarr[rgid].l += luvData[pos].l;
    			rginfoarr[rgid].u += luvData[pos].u;
    			rginfoarr[rgid].v += luvData[pos].v;
    		}
    	}
    
    	//求出各个区的LUV均值;
    	for (i=0; i<=rgnumber; i++)
    	{
    		rginfoarr[i].l = (FLOAT) ( rginfoarr[i].l / rginfoarr[i].ptcount );
    		rginfoarr[i].u = (FLOAT) ( rginfoarr[i].u / rginfoarr[i].ptcount );
    		rginfoarr[i].v = (FLOAT) ( rginfoarr[i].v / rginfoarr[i].ptcount );
    	}
    
    	//===================⑤ 区域融合 ===================//
    	//根据各区luv均值(rginfoarr)和各区之间邻接关系(用flag计算)进行区域融合
    	INT* mergearr = new INT[rgnumber+1];
    	memset( mergearr, -1, sizeof(INT)*(rgnumber+1) );
    	INT mergednum = 0;
    	MergeRgs(rginfoarr, rgnumber, flag, width, height, mergearr, mergednum);
    	//MergeRgs(rginfoarr, rgnumber, flag, width, height);
    	
    
    	//===================⑥ 确定融合后各个像素所属区域===================//
    	//确定合并后各像素点所属区域;
    	for (y=0; y<(height); y++)
    	{
    		xstart = y*width;
    		for ( x=0; x<(width); x++)
    		{
    			INT pos = xstart + x;
    			INT rgid = flag[pos];//该点所属区域;
    			flag[pos] = FindMergedRgn(rgid, mergearr);
    		}
    	}
    	delete [] mergearr; 
    	mergearr = NULL;
    
    	//===================⑧ 绘制区域轮廓,保存结果===================//
    	//=================== ===================//
    	//以下根据标识数组查找边界点(不管四边点以减小复杂度)
    	IplImage* dstContour = cvCreateImage(cvGetSize(pSrc),IPL_DEPTH_8U,3);
    
    	for (y=1; y<(height-1); y++)
    	{
    		xstart = y*width;
    		for ( x=1; x<(width-1); x++)
    		{
    			INT pos = xstart + x;
    			INT imagepos = pos * 3;	
    			INT left = pos - 1;
    			INT up = pos - width;
    			INT right = pos +1;
    			INT down = pos + width;
    			if ( ( flag[right]!=flag[pos] ) || ( flag[down]!=flag[pos] ) )
    			//if ( flag[pos]==0 )
    			{
    				((uchar *)(dstContour->imageData+y*dstContour->widthStep))[x*dstContour->nChannels+0] = 0;
    				((uchar *)(dstContour->imageData+y*dstContour->widthStep))[x*dstContour->nChannels+1] = 0;
    				((uchar *)(dstContour->imageData+y*dstContour->widthStep))[x*dstContour->nChannels+2] = 250;
    				//imageData[imagepos] = 0;
    				//imageData[imagepos+1] = 0;
    				//imageData[imagepos+2] = 250;
    			}
    		}
    	}
    
    	cvNamedWindow("dstContour",1);
    	cvShowImage("dstContour",dstContour);
    
    	//=================== ===================//
    	//  在源图像上绘制轮廓
    	CvMemStorage* storage= cvCreateMemStorage(0);
        CvSeq* contour= 0;//可动态增长元素序列
    	IplImage* dstContourGray= cvCreateImage(cvGetSize(dstContour),8,1);
    	cvCvtColor(dstContour,dstContourGray,CV_BGR2GRAY);
    	cvNamedWindow("dstContourGray",1);
    	cvShowImage("dstContourGray",dstContourGray);
    
        cvFindContours(	//函数cvFindContours从二值图像中检索轮廓,并返回检测到的轮廓的个数
    		dstContourGray,//二值化图像
    		storage,	//轮廓存储容器
    		&contour,	//指向第一个轮廓输出的指针
    		sizeof(CvContour),	//序列头大小
    		CV_RETR_CCOMP,		//内外轮廓都检测; CV_RETR_EXTERNAL:只检索最外面的轮廓
    		CV_CHAIN_APPROX_SIMPLE//压缩水平垂直和对角分割,即函数只保留末端的象素点 CV_CHAIN_CODE//CV_CHAIN_APPROX_NONE
    		);
    	
    	IplImage* dst = cvCreateImage(cvGetSize(pSrc),IPL_DEPTH_8U,3);
    	cvCopyImage(pSrc,dst);
    	for (;contour!= 0;contour= contour->h_next)
        {
            CvScalar color= CV_RGB(rand()&255,rand()&255,rand()&255);
            cvDrawContours( //在图像中绘制轮廓
    			dst,
    			contour,//指向初始轮廓的指针
    			color,	//外层轮廓的颜色
    			color,	//内层轮廓的颜色
    			-1,		//绘制轮廓的最大等级(
    						//0:当前;
    						//1:相同级别下的轮廓;
    						//2:绘制同级与下一级轮廓;
    						//负数:不会绘制当前轮廓之后的轮廓,但会绘制子轮廓,到(abs(该参数)-1)为止)
    			1,		//轮廓线条粗细
    			8		//线条的类型
    			);
        }
    	cvNamedWindow("Contours",1);
        cvShowImage("Contours",dst);
    	cvSaveImage(".\\result.jpg",dst);
    	cvReleaseImage(&dstContour);
    	cvReleaseImage(&dst);
        cvReleaseImage(&dstContourGray);
        cvReleaseMemStorage(&storage);
    
    	cvWaitKey(0);
    	cvDestroyAllWindows();
    
    	//  释放资源
    	delete [] gradientadd; gradientadd = NULL;//大小256
    	delete [] gradientfre; gradientfre = NULL;//大小256
    	delete [] deltar;    deltar    = NULL;//大小imageLen
    	delete [] graposarr; graposarr = NULL;//大小imageLen
    	cvReleaseImage(&pSrc);
    	cvReleaseImage(&gray);	
    	
    
    }
    

    函数模块

    void MyWatershed::GetGra(IplImage* image, INT* deltar)
    //通过sobel获取灰度图像image的梯度值存数组deltar
    {
    	//   定义
    	IplImage* pSrc = NULL;
    	pSrc = cvCreateImage(cvGetSize(image),image->depth,image->nChannels);
    	cvCopyImage(image,pSrc);
    	int width = pSrc->width;
    	int height = pSrc->height;
    
    /*  测试参数
    	CString mPath;
    	//m_Path.GetWindowText(mPath);
    	mPath.Format(_T("%d, height: %d"),width,height);
    	AfxMessageBox(mPath);
    	*/
    
    	static int nWeight[2][3][3];
    	nWeight[0][0][0]=-1;
    	nWeight[0][0][1]=0;
    	nWeight[0][0][2]=1;
    	nWeight[0][1][0]=-2;
    	nWeight[0][1][1]=0;
    	nWeight[0][1][2]=2;
    	nWeight[0][2][0]=-1;
    	nWeight[0][2][1]=0;
    	nWeight[0][2][2]=1;
    	nWeight[1][0][0]=1;
    	nWeight[1][0][1]=2;
    	nWeight[1][0][2]=1;
    	nWeight[1][1][0]=0;
    	nWeight[1][1][1]=0;
    	nWeight[1][1][2]=0;
    	nWeight[1][2][0]=-1;
    	nWeight[1][2][1]=-2;
       	nWeight[1][2][2]=-1;
    	
        int nTmp[3][3];
    	FLOAT dGray;
    	FLOAT dGradOne;
    	FLOAT dGradTwo;
    	int x,y;
        int* gra=new int[height*width];
    	int gr;
    	for (y=0;y<height;y++)
    	{
    		for (x=0;x<width;x++)
    		{
    			int image_pos=width*y+x;
    			/*
    			int memory_pos1=3*width*y+3*x;
    			int memory_pos2=3*width*y+3*x+1;
    			int memory_pos3=3*width*y+3*x+2;
                FLOAT b=image[memory_pos1];
    			FLOAT g=image[memory_pos2];
    			FLOAT r=image[memory_pos3];
    			int gr=(int)(0.299*r+0.587*g+0.114*b);*/
    			gr = ((uchar *)(pSrc->imageData))[y*pSrc->widthStep+x];
    			gra[image_pos]=gr;
    		}
    	}
    
        //暂不计算边缘点
    	for (y=1;y<height-1;y++)
    	{
    		for (x=1;x<width-1;x++)
    		{
    			dGray=0;
    			dGradOne=0;
    			dGradTwo=0;
    			nTmp[0][0]=gra[(y-1)*width+x-1];
    			nTmp[0][1]=gra[(y-1)*width+x];
    			nTmp[0][2]=gra[(y-1)*width+x+1];
    			nTmp[1][0]=gra[y*width+x-1];
    			nTmp[1][1]=gra[y*width+x];
    			nTmp[1][2]=gra[y*width+x+1];
    			nTmp[2][0]=gra[(y+1)*width+x-1];
    			nTmp[2][1]=gra[(y+1)*width+x];
    			nTmp[2][2]=gra[(y+1)*width+x+1];
    			for (int yy=0;yy<3;yy++)
    			{
    				for (int xx=0;xx<3;xx++)
    				{
    					dGradOne+=nTmp[yy][xx]*nWeight[0][yy][xx];
    					dGradTwo+=nTmp[yy][xx]*nWeight[1][yy][xx];
    				}
    			}
    			dGray=dGradOne*dGradOne+dGradTwo*dGradTwo;
    			dGray=sqrtf(dGray);
    			deltar[y*width+x]=(int)dGray;
    		}
    	}
        
    	//边缘赋为其内侧点的值
    	for (y=0; y<height; y++)
    	{
    		INT x1 = 0;
    		INT pos1 = y*width + x1;
    		deltar[pos1] = deltar[pos1+1];	
    		INT x2 = width-1;
    		INT pos2 = y*width + x2;
    		deltar[pos2] = deltar[pos2-1];
    	}
    	for ( x=0; x<width; x++)
    	{
    		INT y1 = 0;
    		INT pos1 = x;
    		INT inner = x + width;//下一行;
    		deltar[pos1] = deltar[inner];
    		INT y2 = height-1;
    		INT pos2 = y2*width + x;
    		inner = pos2 - width;//上一行;
    		deltar[pos2] = deltar[inner];
    	}
        
    	delete [] gra;
    	gra=NULL;
    
    }
    
    
    //////////////////////////////////////////////////////////////////////////
    // Luc Vincent and Pierre Soille的分水岭分割flood步骤的实现代码, 
    // 修改自相应伪代码, 伪代码来自作者论文《Watersheds in Digital Spaces:
    // An Efficient Algorithm Based on Immersion Simulations》
    // IEEE TRANSACTIONS ON PATTERN ANALYSIS AND MACHINE INTELLIGENCE.
    // VOL.13, NO.6, JUNE 1991;
    // by dzj, 2004.06.28 
    // MyImageGraPt* imiarr - 输入的排序后数组
    // INT* graddarr -------- 输入的各梯度数组,由此直接存取各H像素点
    // INT minh,INT maxh --- 最小最大梯度
    // INT* flagarr --------- 输出标记数组
    // 注意:目前不设分水岭标记,只设每个像素所属区域;
    //////////////////////////////////////////////////////////////////////////
    void MyWatershed::Flood(MyImageGraPtWatershed* imiarr, INT* graddarr, 
    						 INT minh, INT maxh, 
    						 INT* flagarr, INT& outrgnumber,
    						 INT width,INT height)
    {
    	int imageWidth=width;
        int imageHeight=height;
    
    	const INT INIT = -2;//initial value of lable image
    	const INT MASK = -1;//initial value at each level
    	const INT WATERSHED = 0; //label of the watershed pixels
    	INT h = 0;
    	INT imagelen = imageWidth * imageHeight;//图像中像素的个数
    	for (INT i=0; i<imagelen; i++)
    	{
    		flagarr[i] = INIT;
    	}
    	//memset(flagarr, INIT, sizeof(INT)*imagelen);
    	INT* imd = new INT[imagelen]; //距离数组,直接存取;
    	for (i=0; i<imagelen; i++)
    	{
    		imd[i] = 0;
    	}
    	//memset(imd, 0, sizeof(INT)*imagelen);
    	std::queue <int> myqueue;
    	INT curlabel = 0;//各盆地标记;
    	
    	for (h=minh; h<=maxh; h++)
    	{
    		INT stpos = graddarr[h];
    		INT edpos = graddarr[h+1];
    		for (INT ini=stpos; ini<edpos; ini++)
    		{
    			INT x = imiarr[ini].x;
    			INT y = imiarr[ini].y;
    			INT ipos = y*imageWidth + x;
    			flagarr[ipos] = MASK;
    			//以下检查该点邻域是否已标记属于某区或分水岭,若是,则将该点加入fifo;
    			INT left = ipos - 1;
    			if (x-1>=0) 
    			{
    				if (flagarr[left]>=0)
    				{
    					imd[ipos] = 1;
    					myqueue.push(ipos);//点位置压入fifo;
    					continue;
    				}				
    			}
    			INT right = ipos + 1;
    			if (x+1<imageWidth) 
    			{
    				if (flagarr[right]>=0) 
    				{
    					imd[ipos] = 1;
    					myqueue.push(ipos);//点位置压入fifo;
    					continue;
    				}
    			}
    			INT up = ipos - imageWidth;
    			if (y-1>=0) 
    			{
    				if (flagarr[up]>=0)
    				{
    					imd[ipos] = 1;
    					myqueue.push(ipos);//点位置压入fifo;
    					continue;
    				}				
    			}
    			INT down = ipos + imageWidth;
    			if (y+1<imageHeight)
    			{
    				if (flagarr[down]>=0) 
    				{
    					imd[ipos] = 1;
    					myqueue.push(ipos);//点位置压入fifo;
    					continue;
    				}			
    			}
    		}
    		
    		//以下根据先进先出队列扩展现有盆地;
    		INT curdist = 1; myqueue.push(-99);//特殊标记;
    		while (TRUE)
    		{
    			INT p = myqueue.front();
    			myqueue.pop();
    			if (p == -99)
    			{
    				if ( myqueue.empty() )
    				{
    					break;
    				}else
    				{
    					myqueue.push(-99);
    					curdist = curdist + 1;
    					p = myqueue.front();
    					myqueue.pop();
    				}
    			}
    			
    			//以下找p的邻域;
    			INT y = (INT) (p/imageWidth);
    			INT x = p - y*imageWidth;
    			INT left = p - 1;
    			if  (x-1>=0)
    			{
    				if ( ( (imd[left]<curdist) && flagarr[left]>0)
    					|| (flagarr[left]==0) ) 
    				{
    					if ( flagarr[left]>0 )
    					{
    						//ppei属于某区域(不是分水岭);
    						if ( (flagarr[p]==MASK) 
    							|| (flagarr[p]==WATERSHED) )
    						{
    							//将其设为邻点所属区域;
    							flagarr[p] = flagarr[left];
    						}else if (flagarr[p]!=flagarr[left])
    						{
    							//原来赋的区与现在赋的区不同,设为分水岭;
    							//flagarr[p] = WATERSHED;
    						}
    					}else if (flagarr[p]==MASK)//ppei为分岭;
    					{
    						flagarr[p] = WATERSHED;
    					}
    				}else if ( (flagarr[left]==MASK) && (imd[left]==0) )
    					//ppei中已MASK的点,但尚未标记(即不属某区也不是分水岭);
    				{
    					imd[left] = curdist + 1; myqueue.push(left);
    				}
    			}
    			
    			INT right = p + 1;
    			if (x+1<imageWidth) 
    			{
    				if ( ( (imd[right]<curdist) &&  flagarr[right]>0)
    					|| (flagarr[right]==0) )
    				{
    					if ( flagarr[right]>0 )
    					{
    						//ppei属于某区域(不是分水岭);
    						if ( (flagarr[p]==MASK) 
    							|| (flagarr[p]==WATERSHED) )
    						{
    							//将其设为邻点所属区域;
    							flagarr[p] = flagarr[right];
    						}else if (flagarr[p]!=flagarr[right])
    						{
    							//原来赋的区与现在赋的区不同,设为分水岭;
    							//flagarr[p] = WATERSHED;
    						}
    					}else if (flagarr[p]==MASK)//ppei为分岭;
    					{
    						flagarr[p] = WATERSHED;
    					}
    				}else if ( (flagarr[right]==MASK) && (imd[right]==0) )
    					//ppei中已MASK的点,但尚未标记(即不属某区也不是分水岭);
    				{
    					imd[right] = curdist + 1; myqueue.push(right);
    				}
    			}
    			
    			INT up = p - imageWidth;
    			if (y-1>=0) 
    			{
    				if ( ( (imd[up]<curdist) &&  flagarr[up]>0)
    					|| (flagarr[up]==0) )
    				{
    					if ( flagarr[up]>0 )
    					{
    						//ppei属于某区域(不是分水岭);
    						if ( (flagarr[p]==MASK) 
    							|| (flagarr[p]==WATERSHED) )
    						{
    							//将其设为邻点所属区域;
    							flagarr[p] = flagarr[up];
    						}else if (flagarr[p]!=flagarr[up])
    						{
    							//原来赋的区与现在赋的区不同,设为分水岭;
    							//flagarr[p] = WATERSHED;
    						}
    					}else if (flagarr[p]==MASK)//ppei为分岭;
    					{
    						flagarr[p] = WATERSHED;
    					}
    				}else if ( (flagarr[up]==MASK) && (imd[up]==0) )
    					//ppei中已MASK的点,但尚未标记(即不属某区也不是分水岭);
    				{
    					imd[up] = curdist + 1; myqueue.push(up);
    				}
    			}
    			
    			INT down = p + imageWidth;
    			if (y+1<imageHeight) 
    			{
    				if ( ( (imd[down]<curdist) &&  flagarr[down]>0)
    					|| (flagarr[down]==0) )
    				{
    					if ( flagarr[down]>0 )
    					{
    						//ppei属于某区域(不是分水岭);
    						if ( (flagarr[p]==MASK) 
    							|| (flagarr[p]==WATERSHED) )
    						{
    							//将其设为邻点所属区域;
    							flagarr[p] = flagarr[down];
    						}else if (flagarr[p]!=flagarr[down])
    						{
    							//原来赋的区与现在赋的区不同,设为分水岭;
    							//flagarr[p] = WATERSHED;
    						}
    					}else if (flagarr[p]==MASK)//ppei为分岭;
    					{
    						flagarr[p] = WATERSHED;
    					}
    				}else if ( (flagarr[down]==MASK) && (imd[down]==0) )
    					//ppei中已MASK的点,但尚未标记(既不属某区也不是分水岭);
    				{
    					imd[down] = curdist + 1; myqueue.push(down);
    				}	
    			}
    			
    		}//以上现有盆地的扩展;
    		
    		//以下处理新发现的盆地;
    		for (ini=stpos; ini<edpos; ini++)
    		{
    			INT x = imiarr[ini].x;
    			INT y = imiarr[ini].y;
    			INT ipos = y*imageWidth + x;
    			imd[ipos] = 0;//重置所有距离
    			if (flagarr[ipos]==MASK)
    			{
    				//经过前述扩展后该点仍为MASK,则该点必为新盆地的一个起始点;
    				curlabel = curlabel + 1;
    				myqueue.push(ipos); 
    				flagarr[ipos] = curlabel;
    				
    				while ( myqueue.empty()==FALSE )
    				{
    					INT ppei = myqueue.front();
    					myqueue.pop();
    					INT ppeiy = (INT) (ppei/imageWidth);
    					INT ppeix = ppei - ppeiy*imageWidth;
    					
    					INT ppeileft = ppei - 1;
    					if ( (ppeix-1>=0) && (flagarr[ppeileft]==MASK) )
    					{
    						myqueue.push(ppeileft);//点位置压入fifo;
    						flagarr[ppeileft] = curlabel;
    					}
    					INT ppeiright = ppei + 1;
    					if ( (ppeix+1<imageWidth) && (flagarr[ppeiright]==MASK) )
    					{
    						myqueue.push(ppeiright);//点位置压入fifo;
    						flagarr[ppeiright] = curlabel;
    					}
    					INT ppeiup = ppei - imageWidth;
    					if ( (ppeiy-1>=0) && (flagarr[ppeiup]==MASK) )
    					{
    						myqueue.push(ppeiup);//点位置压入fifo;
    						flagarr[ppeiup] = curlabel;
    					}
    					INT ppeidown = ppei + imageWidth;
    					if ( (ppeiy+1<imageHeight) && (flagarr[ppeidown]==MASK) )
    					{
    						myqueue.push(ppeidown);//点位置压入fifo;
    						flagarr[ppeidown] = curlabel;
    					}					
    				}				
    			}
    		}//以上处理新发现的盆地;
    		
    	}
    	
    	outrgnumber = curlabel;	
    	delete [] imd; 
    	imd = NULL;
    	
    	/*
    	const INT INIT = -2;
    	const INT MASK = -1;
    	const INT WATERSHED = 0;
    	INT imagelen=width*height;
    	INT *imd=new INT[imagelen];
        memset(imd, 0, sizeof(INT)*imagelen);
    	std::queue <int> myqueue;
    	INT curlabe=0;
    	for (int i=minh;i<=maxh;i++)
    	{
    	INT stpos=graddarr[i];
    	INT edpos=graddarr[i+1];
    	INT x,y;
    	static int disX[]={1,0,-1,0};
    	static int disY[]={0,1,0,-1};
    	for (int j=stpos;j<edpos;j++)
    	{
    	x=imiarr[j].x;
    			 y=imiarr[j].y;
    			 int pos=y*width+x;
    			 flagarr[pos]=MASK;	
    			 for (int ii=0;ii<4;ii++)
    			 {
    			 int nepos=(y+disY[ii])*width+x+disX[ii];
    			 if (((x+disX[ii])>=0)&&((x+disX[ii])<width)&&
    			 ((y+disY[ii])>=0)&&((y+disY[ii])<height)&&(flagarr[nepos]>0||flagarr[nepos]==0))
    			 {		
    			 imd[pos]=1;
    			 myqueue.push(pos);
    			 break;
    			 }
    			 }
    			 }
    			 
    			   int curdist=1;//距离标志,个人感觉标志着是否被处理了
    			   myqueue.push(-99);//特殊标记
    			   while (TRUE)
    			   {
    			   int siz=myqueue.size();
    			   int p=myqueue.front();
    			   myqueue.pop();
    			   if (p==-99)
    			   {
    			   if (myqueue.empty()==TRUE)
    			   {
    			   break;
    			   }
    			   else
    			   {
    			   myqueue.push(-99);
    			   curdist+=1;
    			   p=myqueue.front();
    			   myqueue.pop();
    			   }
    			   }
    			   //搜索p的临点
    			   for(int ii=0;ii<4;ii++)
    			   {
                   y=(INT)(p/width);
    			   x=imagelen-y*width;
                   if (((x+disX[ii])>0||((x+disX[ii])==0))&&((x+disX[ii])<width)&&
    			   ((y+disY[ii])>0||((y+disY[ii])==0))&&((y+disY[ii])<height))
    			   {
    			   int npos=(y+disY[ii])*width+x+disX[ii];
    			   if (imd[npos]<curdist&&(flagarr[npos]>0||flagarr[npos]==0))
    			   {
    			   
    				 if (flagarr[npos]>0)
    				 {
    				 if (flagarr[p]==MASK||flagarr[p]==WATERSHED)
    				 {
    				 flagarr[p]=flagarr[npos];
    				 }
    				 else if (flagarr[p]!=flagarr[npos])
    				 {
    				 flagarr[p]=WATERSHED;
    				 }
    				 }
    				 else if (flagarr[p]==MASK)
    				 {
    				 flagarr[p]=WATERSHED;
    				 }
    				 
    				   }
    				   else if (flagarr[npos]==MASK&&imd[npos]==0)
    				   {
    				   imd[npos]=curdist+1;
    				   myqueue.push(npos);
    				   }				   
    				   }
    				   }
    				   }//while
    				   
    					 
    					   //搜索临点的for
    					   for (int jj=stpos;jj<edpos;jj++)
    					   {
    					   x=imiarr[jj].x;
    					   y=imiarr[jj].y;
    					   INT ipos = y*width + x;
    					   imd[ipos] = 0;//重置所有距离
    					   if (flagarr[ipos]==MASK)
    					   {
    					   //经过前述扩展后该点仍为MASK,则该点必为新盆地的一个起始点;
    					   curlabe = curlabe + 1;
    					   myqueue.push(ipos); 
    					   flagarr[ipos] = curlabe;
    					   while ( myqueue.empty()==FALSE )
    					   {
    					   INT ppei = myqueue.front();
    					   myqueue.pop();
    					   int s=myqueue.size();
    					   INT ppeiy = (INT) (ppei/width);
    					   INT ppeix = ppei - ppeiy*width;
    					   
    						 INT ppeileft = ppei - 1;
    						 if ( (ppeix-1>=0) && (flagarr[ppeileft]==MASK) )
    						 {
    						 myqueue.push(ppeileft);//点位置压入fifo;
    						 flagarr[ppeileft] = curlabe;
    						 }
    						 INT ppeiright = ppei + 1;
    						 if ( (ppeix+1<width) && (flagarr[ppeiright]==MASK) )
    						 {
    						 myqueue.push(ppeiright);//点位置压入fifo;
    						 flagarr[ppeiright] = curlabe;
    						 }
    						 INT ppeiup = ppei - width;
    						 if ( (ppeiy-1>=0) && (flagarr[ppeiup]==MASK) )
    						 {
    						 myqueue.push(ppeiup);//点位置压入fifo;
    						 flagarr[ppeiup] = curlabe;
    						 }
    						 INT ppeidown = ppei + width;
    						 if ( (ppeiy+1<height) && (flagarr[ppeidown]==MASK) )
    						 {
    						 myqueue.push(ppeidown);//点位置压入fifo;
    						 flagarr[ppeidown] = curlabe;
    						 }					
    						 }				
    						 }
    						 }//搜索临点的for
    						 
    						   
    							 }//最后的一个for
    							 
    							   outrgnumber=curlabe;
    							   delete [] imd;
    							   imd=NULL;
    */
    }
    
    #define NearMeasureBias 200.0//判定区域颜色相似的阈值;
    void MyWatershed::MergeRgs(MyRgnInfoWatershed* rginfoarr, INT rgnumber, INT* flag, INT width, INT height, INT* outmerge, INT& rgnum)
    //合并相似区域;
    {
    	//////////////////////////////////////////////////////////////////////////
    	//1、建立各区的邻域数组;
    	//2、依次扫描各区域,寻找极小区域;
    	//3、对每个极小区(A),在相邻区中找到最相似者;
    	//4、与相似区(B)合并(各种信息刷新),在极小区(A)的邻域中
    	//   删除相似区(B),在邻域数组中删除相似区(B)对应的项,将
    	//   相似区(B)的相邻区s加到极小区(A)的邻域中去;
    	//5、记录合并信息,设一数组专门存放该信息,该数组的第A个元素值设为B;
    	//6、判断是否仍为极小区,若是则返回3;
    	//7、是否所有区域都已处理完毕,若非则返回2;
    	//
    	//   由于各区的相邻区不会太多,因此采用邻接数组作为存储结构;
    	//////////////////////////////////////////////////////////////////////////
    	CString* neiarr = new CString[rgnumber+1];//第一个不用;
    	INT* mergearr = outmerge;//记录合并情况数组;
    
    	//建立邻域数组;
    	for (INT y=0; y<height; y++)
    	{
    		INT lstart = y * width;
    		for (INT x=0; x<width; x++)
    		{
    			INT pos = lstart + x;
    			INT left=-1, right=-1, up=-1, down=-1;
    			pMyMath.GetNeiInt(x, y, pos, width, height, left, right, up, down);//找pos的四个邻域;
    			//确定并刷新邻域区信息;
    			INT curid = flag[pos];
    			AddNeiOfCur(curid, left, right, up, down, flag, neiarr);
    		}
    	}//建立邻域数组;
    	
    	//区域信息数组中的有效信息从1开始,第i个位置存放第i个区域的信息;
    	for (INT rgi=1; rgi<=rgnumber; rgi++)
    	{
    		//扫描所有区域,找极小区;
    		LONG allpoints = width * height;
    		LONG nmin = (LONG) (allpoints / 400);
    		INT curid = rgi;
    
    		//rginfoarr[rgi].isflag初始为FALSE,在被合并到其它区后改为TRUE;
    		while ( ( (rginfoarr[rgi].ptcount)<nmin ) 
    			&& !rginfoarr[rgi].isflag )
    		{
    			//该区为极小区,遍历所有相邻区,找最接近者;
    			CString neistr = neiarr[curid];
    			INT nearid = FindNearestNei(curid, neistr, rginfoarr, mergearr);
    			//合并curid与nearid;
    			MergeTwoRgn(curid, nearid, neiarr, rginfoarr, mergearr);			
    		} 
    	}
    
    	//以下再合并相似区域,(无论大小),如果不需要,直接将整个循环注释掉就行了;
    	INT countjjj = 0;
    	//区域信息数组中的有效信息从1开始,第i个位置存放第i个区域的信息;
    	for (INT ii=1; ii<=rgnumber; ii++)
    	{
    		if (!rginfoarr[ii].isflag)
    		{
    			INT curid = ii;
    			MergeNearest(curid, rginfoarr, neiarr, mergearr);
    		}
    	}
    
    	INT counttemp = 0;
    	for (INT i=0; i<rgnumber; i++)
    	{
    		if (!rginfoarr[i].isflag)
    		{
    			counttemp ++;
    		}
    	}
    
    	rgnum = counttemp;
    
    	delete [] neiarr; 
    	neiarr = NULL;
    }
    
    
    void MyWatershed::MergeNearest(INT curid, MyRgnInfoWatershed* rginfoarr, CString* neiarr, INT* mergearr)
    //合并相似区域;
    {
    	//依次处理各个邻域,若相似,则合并;
    	//CString neistr = neiarr[curid];
    	FLOAT cl, cu, cv;
    	cl = rginfoarr[curid].l;//当前区的LUV值;
    	cu = rginfoarr[curid].u;
    	cv = rginfoarr[curid].v;
    	BOOL loopmerged = TRUE;//一次循环中是否有合并操作发生,若无,则退出循环;
    
    	while (loopmerged)
    	{
    		loopmerged = FALSE;
    		CString tempstr = neiarr[curid];//用于本函数内部处理;
    		while (tempstr.GetLength()>0)
    		{
    			INT pos = tempstr.Find(" ");
    			ASSERT(pos>=0);
    			CString idstr = tempstr.Left(pos);
    			tempstr.Delete(0, pos+1);
    			
    			INT idint = (INT) strtol(idstr, NULL, 10);
    			//判断该区是否已被合并,若是,则一直找到该区当前的区号;
    			idint = FindMergedRgn(idint, mergearr);
    			if (idint==curid)
    			{
    				continue;//这个邻区已被合并到当前区,跳过;
    			}
    			FLOAT tl, tu, tv;
    			tl = rginfoarr[idint].l;//当前处理的邻区的LUV值;
    			tu = rginfoarr[idint].u;
    			tv = rginfoarr[idint].v;
    			DOUBLE tempdis = pow(tl-cl, 2) 
    				+ pow(tu-cu, 2) + pow(tv-cv, 2);
    			if (tempdis<NearMeasureBias)
    			{
    				MergeTwoRgn(curid, idint, neiarr, rginfoarr, mergearr);
    				cl = rginfoarr[curid].l;//当前区的LUV值刷新;
    				cu = rginfoarr[curid].u;
    				cv = rginfoarr[curid].v;
    				loopmerged = TRUE;
    			}		
    		}
    	}
    }
    
    
    void MyWatershed::MergeTwoRgn(INT curid, INT nearid
    	, CString* neiarr, MyRgnInfoWatershed* rginfoarr, INT* mergearr)
    //将nearid合并到curid中去,更新合并后的区信息,并记录该合并;
    {
    	//将区信息中nearid对应项的标记设为已被合并;
    	rginfoarr[nearid].isflag = TRUE;
    	//更新合并后的LUV信息;
    	LONG ptincur = rginfoarr[curid].ptcount;
    	LONG ptinnear = rginfoarr[nearid].ptcount;
    	DOUBLE curpercent = (FLOAT)ptincur / (FLOAT)(ptincur+ptinnear);
    	rginfoarr[curid].ptcount = ptincur + ptinnear;
    	rginfoarr[curid].l = (FLOAT) ( curpercent * rginfoarr[curid].l
    		+ (1-curpercent) * rginfoarr[nearid].l );
    	rginfoarr[curid].u = (FLOAT) ( curpercent * rginfoarr[curid].u
    		+ (1-curpercent) * rginfoarr[nearid].u );
    	rginfoarr[curid].v = (FLOAT) ( curpercent * rginfoarr[curid].v
    		+ (1-curpercent) * rginfoarr[nearid].v );
    	//将nearid的邻域加到curid的邻域中去;
    	AddBNeiToANei(curid, nearid, neiarr, mergearr);
    	//记录该合并;
    	mergearr[nearid] = curid;
    }
    
    void MyWatershed::AddBNeiToANei(INT curid, INT nearid, CString* neiarr, INT* mergearr)
    //将nearid的邻域加到curid的邻域中去;
    {
    	//先从curid的邻区中把nearid删去;
    /*
    	CString tempstr;
    	tempstr.Format("%d ", nearid);
    	INT temppos = neiarr[curid].Find(tempstr, 0);
    	while (temppos>0 && neiarr[curid].GetAt(temppos-1)!=' ')
    	{
    		temppos = neiarr[curid].Find(tempstr, temppos+1);
    	}
    	if (temppos>=0)
    	{
    		//否则邻近区为合并过来的区,忽略;
    		neiarr[curid].Delete(temppos, tempstr.GetLength());
    	}
    */
        //将nearid的邻区依次加到curid的邻区中去;
    	CString neistr = neiarr[nearid];
    	CString curstr = neiarr[curid];
    	//一般说来,极小区的邻域应该较少,因此,为着提高合并速度,将
    	//curstr加到neistr中去,然后将结果赋给neiarr[curid];
    	while ( curstr.GetLength()>0 )
    	{
    		INT pos = curstr.Find(" ");		
    		ASSERT(pos>=0);
    		CString idstr = curstr.Left(pos);
            curstr.Delete(0, pos+1);
    		INT idint = (INT) strtol(idstr, NULL, 10);
    		idint = FindMergedRgn(idint, mergearr);
    		idstr += " ";
    		if ( (idint == curid) || (idint == nearid) )
    		{
    			continue;//本区不与本区相邻;
    		}else
    		{
    			if ( neistr.Find(idstr, 0) >= 0 )
    			{
    				continue;
    			}else
    			{
    				neistr += idstr;//加到邻区中去;
    			}
    		}		
    	}
    	neiarr[curid] = neistr;
    /*
    	CString toaddneis = neiarr[nearid];
    	while (toaddneis.GetLength()>0)
    	{
    		INT pos = toaddneis.Find(" ");		
    		ASSERT(pos>=0);
    		CString idstr = toaddneis.Left(pos);
            toaddneis.Delete(0, pos+1);
    		INT idint = (INT) strtol(idstr, NULL, 10);
    		idint = FindMergedRgn(idint, mergearr);
    		idstr += " ";
    		if ( (idint == curid) || (idint == nearid) )
    		{
    			continue;//本区不与本区相邻;
    		}else
    		{
    			if ( neiarr[curid].Find(idstr, 0) >= 0 )
    			{
    				continue;
    			}else
    			{
    				neiarr[curid] += idstr;//加到邻区中去;
    			}
    		}		
    	}
    */
    }
    
    INT MyWatershed::FindNearestNei(INT curid, CString neistr, MyRgnInfoWatershed* rginfoarr, INT* mergearr)
    //寻找neistr中与curid最接近的区,返回该区id号;
    {
    	INT outid = -1;
    	DOUBLE mindis = 999999;
    	FLOAT cl, cu, cv;
    	cl = rginfoarr[curid].l;//当前区的LUV值;
    	cu = rginfoarr[curid].u;
    	cv = rginfoarr[curid].v;
    
    	CString tempstr = neistr;//用于本函数内部处理;
    	while (tempstr.GetLength()>0)
    	{
    		INT pos = tempstr.Find(" ");
    		ASSERT(pos>=0);
    		CString idstr = tempstr.Left(pos);
    		tempstr.Delete(0, pos+1);
    
    		INT idint = (INT) strtol(idstr, NULL, 10);
    		//判断该区是否已被合并,若是,则一直找到该区当前的区号;
    		idint = FindMergedRgn(idint, mergearr);
    		if (idint==curid)
    		{
    			continue;//这个邻区已被合并到当前区,跳过;
    		}
    		FLOAT tl, tu, tv;
    		tl = rginfoarr[idint].l;//当前处理的邻区的LUV值;
    		tu = rginfoarr[idint].u;
    		tv = rginfoarr[idint].v;
    		DOUBLE tempdis = pow(tl-cl, 2) 
    			+ pow(tu-cu, 2) + pow(tv-cv, 2);
    		if (tempdis<mindis)
    		{
    			mindis = tempdis;//最大距离和对应的相邻区ID;
    			outid = idint;
    		}		
    	}
    
    	return outid;
    }
    
    INT MyWatershed::FindMergedRgnMaxbias(INT idint, INT* mergearr, INT bias)
    //大阈值终止查找合并区,用于coarse watershed, 
    //调用者必须保证idint有效,即:mergearr[idint]>0;
    //以及mergearr有效,即:mergearr[idint]<idint;
    {
    	INT outid = idint;
    	while ( mergearr[outid]<bias )
    	{
    		outid = mergearr[outid];
    	}
    	return mergearr[outid];
    }
    
    INT MyWatershed::FindMergedRgn(INT idint, INT* mergearr)
    //找到idint最终所合并到的区号;
    {
    	INT outid = idint;
    	while ( mergearr[outid] > 0 )
    	{
    		outid = mergearr[outid];
    	}
    	return outid;
    }
    
    void MyWatershed::AddNeiRgn(INT curid, INT neiid, CString* neiarr)
    //增加neiid为curid的相邻区
    {
    	CString tempneis = neiarr[curid];//当前的相邻区;
    	CString toaddstr;
    	toaddstr.Format("%d ", neiid);
    
    	INT temppos = tempneis.Find(toaddstr, 0);
    	while (temppos>0 && neiarr[curid].GetAt(temppos-1)!=' ')
    	{
    		temppos = neiarr[curid].Find(toaddstr, temppos+1);
    	}
    	
    	if ( temppos<0 )
    	{
    		//当前相邻区中没有tempneis,则加入
    		neiarr[curid] += toaddstr;
    	}
    }
    
    void MyWatershed::AddNeiOfCur(INT curid, INT left, INT right, INT up, INT down, INT* flag, CString* neiarr)
    //刷新当前点的所有相邻区;
    {
    	INT leftid, rightid, upid, downid;
    	leftid = rightid = upid = downid = curid;
    	if (left>=0)
    	{
    		leftid = flag[left];
    		if (leftid!=curid)
    		{
    			//邻点属于另一区, 加邻域点信息;
    			AddNeiRgn(curid, leftid, neiarr);
    		}
    	}
    	if (right>0)
    	{
    		rightid = flag[right];
    		if (rightid!=curid)
    		{
    			//邻点属于另一区, 加邻域点信息;
    			AddNeiRgn(curid, rightid, neiarr);
    		}
    	}
    	if (up>=0)
    	{
    		upid = flag[up];
    		if (upid!=curid)
    		{
    			//邻点属于另一区, 加邻域点信息;
    			AddNeiRgn(curid, upid, neiarr);
    		}
    	}
    	if (down>0)
    	{
    		downid = flag[down];
    		if (downid!=curid)
    		{
    			//邻点属于另一区, 加邻域点信息;
    			AddNeiRgn(curid, downid, neiarr);
    		}
    	}
    }
    

    -----------------------------------------------------------------------------------------------------------------------

    》效果

    找了一辆车做测试,效果勉强,运行速度很慢。

    原图:

    轮廓:

    轮廓图:

    ------------------------------------------------------------------------------------------------------------------------

    》参考资料:

    Watershed (image processing): http://en.wikipedia.org/wiki/Watershed_(image_processing)

    IMAGE SEGMENTATION AND MATHEMATICAL MORPHOLOGY http://cmm.ensmp.fr/~beucher/wtshed.html

    分水岭算法(Watershed Algorithm):http://hi.baidu.com/changfeng01200/blog/item/0ed6920b51f82e1794ca6bc5.html

    分水岭分割方法:http://hi.baidu.com/huangwenzhixin/blog/item/039d5713c81bf62add5401e2.html

    分水岭算法简单实现http://blog.csdn.net/tt2com/article/details/6321610

    分水岭分割算法http://hi.baidu.com/%CA%D8%CD%FB%CC%EC%B1%DF%B5%C4%D4%C2/blog/item/57926db72b319ee531add1d2.html

    ------------------------------------------------------------------------------------------------------------------------

    Author:         SKySeraph

    Email/GTalk: zgzhaobo@gmail.com    QQ:452728574

    From:         http://www.cnblogs.com/skyseraph/

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.

     ------------------------------------------------------------------------------------------------------------------------

  • 相关阅读:
    C#之获取本地IP地址
    C#中对Excel进行操作
    C#中的TCP通讯与UDP通讯
    Flex 学习
    正则表达式实例
    sass调试--页面看到sass文件而不是css文件问题
    webpack+vue-loader 在单独.vue组件中使用sass-loader编译sass报错问题not a valid Win32 applictation
    SVG图案填充-Pattern
    jQuery小技巧
    代码整洁一
  • 原文地址:https://www.cnblogs.com/skyseraph/p/2100426.html
Copyright © 2020-2023  润新知