精华内容
下载资源
问答
  • waterShed-分水岭算法原理及实现

    万次阅读 2015-08-04 09:28:54
    Watershed Algorithm(分水岭算法),顾名思义,就是根据分水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那么那一定是水绕 山,山围水的情形。当然在需要的时候,要人工构筑分水岭,以...
    所谓分水岭算法有好多种实现算法,拓扑学,形态学,浸水模拟和降水模拟等方式。要搞懂就不容易了。Watershed Algorithm(分水岭算法),顾名思义,就是根据分水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那么那一定是水绕
     山,山围水的情形。当然在需要的时候,要人工构筑分水岭,以防集水盆之间的互相穿透。而区分高山(plateaus)与水的界线,以及湖与湖之间的间隔或 都是连通的关系,就是我们可爱的分水岭(watershed)。为了得到一个相对集中的集水盆,那么让水涨到都接近周围的最高的山顶就可以了,再涨就要漏 水到邻居了,而邻居,嘿嘿,水质不同诶,会混淆自我的。那么这样的话,我们就可以用来获取边界灰阶大,中间灰阶小的物体区域了,它就是集水盆。 

    浸水法,就是先通过一个适当小的阈值得到起点,即集水盆的底;然后是向周围淹没也就是浸水的过程,直到得到分水岭。当然如果我们要一直淹没到山顶,即是一 直处理到图像灰阶最高片,那么,当中就会出现筑坝的情况,不同的集水盆在这里想相遇了,我们要洁身自爱,到这里为止,因为都碰到边界了;那么即使在相遇时 没有碰到最高灰阶的地方,也需要人工构筑分水岭,区分不同的区域。不再上山。构筑属于自己的分水岭 

    在计算机图形学中,可利用灰度表征地貌高。图像中我们可以利用灰度高与地貌高的相似性来研究图像的灰度在空间上的变化。这是空域分析,比如还可以通过各种 形式的梯度计算以得到算法的输入,进行浸水处理。分水岭具有很强的边缘检测能力,对微弱的边缘也有较好的效果。这与分水岭扩张的阈值的设置有关系,阈值可 以决定集水盆扩张的范围。但自我构筑的能力却不受影响。 

    为会么这么说呢?为什么有很强的边缘检测能力,而又能得到相对集中的连通的集水盆?现实中很好办,我们在往凹地加水的时候,直到它涨到这一块紧凑的山岭边 缘就不加了;但是如果有一条小山沟存在,那没办法,在初始阈值分割的时候,也就是山沟与集水盆有同样的极小值,而且它们之间是以这个高度一直连接的。那没 关系,我们将它连通。在图像上呢?如何实现? 

    看看算法,算法思想是这样的: 

    首先准备好山和初始的水。这山就是我们的初始图像了,比如用自然获取的图像的梯度来表征山地的每一点的高度吧;而初始的水就是在阈值记为Thre底下,所 有的低于这个高度的整个山地都加水,直到这个阈值Thre高度。从而有三个初始量:unsigned char** Ori_image、char** Seed_image和int** Label_image。最后一个是为最终的结果做准备的。当然要做好初始化,比如,Ora_image赋值为原图像(256色灰度图)的梯度 值,Seed_image则是初始状态下有水的置位,无水的复位,而Label_image则全初始化为0,最终得到的是各点对应的区域号。 

    接下来是考虑将已加的水进行记录,记录成连通的区域,也就是看看有多少个互不相关的集水盆,有五个,那么我们就涨出五个湖,而且尽可能的高,只要大家想到 不溢出。在算法上,有多少个连通的区域就记录成多少个数据结构,工夫就在于如何将这些连通的区域连接成一块,并由一个数据结构来表达了。很好,我们准备用 一个向量容器来实现初始保存,保存所有标记区域种子队列的数组,里面放的是种子队列的指针 vector<queue<POINT>*> vque; ,而且这个队列是由一系列属于同一个区域的图像点组成,我们来自一个集水盆:);其保存方式是这样的:queue<point> *pque=new queue<point>[256];vque.push_back(pque),这样便将一个成员放进到这个区域来了,即容器--集水盆的 保管都,容器中的每个指针,都指向一个集水盆,也就是我们要的连通区域;所以我们可以方便地由这个容器数据结构直接读值的方便性进行操作,一个脚标就可以 得到一个区域(队列指针)的指针;而每个队列还不简单,并不是一列整形数那么易搞,所以说啊,这个算法,真头痛,这个队列的一个成员是一个点;而注意到 vque里存放的一256个队列的的起始指针,真够残忍的。也就是说vque [m] [n]就表达了一个队列,这个队列里可以存储操作一系列的点;显然容量取256是因为所有的初始或者是最终的区域中可能有0-256之间的不同的灰阶的 点,那么我一个区域分用256个队列来记录这些成员点啦,很有可能,这里就只有一个集水盆,那么,256个灰阶的点都存在一个区域就有可能了 

    统计初始连通区域的方法是,八连通邻域法(还有其他方法:四连通域,六连通域),即从逐一扫描输入的Seed_image的每个像素点,将所有的标记了的 初始集水盆一一纳入各自的区域,这是整修图像的扫描,形成外循环。先创建一个临时队列que,用来处理当前初始集水盆的连通连接,将逐一扫描到的属 于一个特定的初始集水盆区域的可生长点暂存,并形成一个内循环。对当前扫描点的处理是,首先判断该点是否为某个初始集水盆的点,如果不是跳过;接下来是, 如果是初始集水盆的点,那么它的八连通域中是否存在不可生长的点(这里的不可生长是指Seed_image中没有标记的点),扫描的八连通邻域中的点是可 生长的,即有标记的,则将之加入到临时队列中que;如果扫描到的连通邻域中有不可生长的至少一个点存在,那么加入到种子队列,记当前区域号为 Num,当前扫描点为(m,n),从而当前的灰阶为Ori_image[m][n],将当前点添加到种子队列:qu[Num-1] [Ori_image[m][n]].push(POINT(m,n))。这里有两个循环,一个是que,另一个是Seed_image。直到两个 循环完整结束,那么就得到了各个连通初始集水盆的记录,保存标记是区域号Num;而我们同时得到了初始的分水岭,那就放在了保存地点qu,这里面标识 了它们对应的区域号,和区域里面对应的点的灰阶,即是特定区域特定灰阶对应的点的集合;我们可以获取这些分水岭的点所在的区域号,可以得到该区域的所有的 灰阶的点信息。一句话,统计连通区域的功能有两个,一是标记初始区域,二是找分水岭 

    初始的区域标记好了,分岭也找到了,那么可以开始“水漫梁山”了。这就是淹没过程。淹没过程由也是由一个内嵌循环的循环来实现的:外循环是做水位上升(这 里循环次数一定要256以内),waterlevel的上升,原来是已经做过了初始的水位分割,那么现在可以从Thre开始了,让水位慢慢上升,让它原本 的湖慢慢扩张,尽量利用其应有的空间,而又不至于淹没到其它的邻居湖泊。内循环是扫描每个初始区域(当前Num,从而有Num个循环)的分水岭的点(在 qu[][]中),按照给定的水位进行扩张。扩张过程是这样的:扫描到的分水岭的当前点,对其进行四连通邻域进行逐一检查,如果四连通域中有点没有标 记的(那这一定是高度较高的点,较低的前面一定扫描过),那么先对该点以本区域号做标记Num(注意是当前的Num);再判断它在当前水位下是否可生长 (灰阶是否小于等于waterlevel),如果可生长那么加入到qu[Num][waterlevel]种子队列中,将会再次进入内循环,否则如果 在当前水位下不可生长,则加入到这个邻域点的分水岭集合中qu[Num][Ori_image[邻域点]]队列中。如此往复循环,直到对应区域完成, 一个水位,扫描所有的区域的分水岭,这样各自同时在一个水位下扩张,保证了不出现跳跃的情况出现(就是一个水位一个区域全局扩张)。 

    最终,所有的区域在每个水位都扩张完毕,得到了分割图,我们的大湖泊形成了 

    这是分水岭算法的一种实现方式。仔细考察不难发现这种实现方式不能产生新的集水盆,也就是说,由于初始集水盆的局限性,很可能会浪费大部分没有发掘出来的 起始点较高的集水盆地。这样,我们就要对算法进行修改了。实现方式是:在淹没的过程中,我们是由阈值Thre的水位开始淹没的,那么我们可以对初始区域之 外的没有标记的点(从Seed_image中考察),对之进行标记,条件是先把这一轮的内循环做好,然后在剩下的没标记区域中发掘新的集水盆,并加入到我 们的种子队列中,下一个水位开始,就又多了一个新成员了,让它们不断膨胀,成长,拥有自己的小天的成员就会逐一的被分割出来。不过话说回来,我们这里是采 用梯度图像,一般情况下,阈值初始分割能够满足我们的要求,把灰阶变化平滑的先截取出来,梯度信息已然足够强大;而如果采用了新盆地扩张,则比较适用于原 始图像。 

    分水岭算法主要的分割目的在于找到图像的连通区域。利用梯度信息作为输入图像,会有一个矛盾点,如果对原始图像进行梯度计算时不作滤波平滑处理,很容易将 物体分割成多个物体,那是因为噪声的影响;而如果进行滤波处理,又容易造成将某些原本几个的物体合成一个物体。当然这里的物体主要还是指图像变化不大或者 说是灰度值相近的目标区域。

    /********  标记-分水岭算法对输入图像进行分割
    输入参数:
    OriginalImage - 输入图像(灰度图,0~255)
    SeedImage     - 标记图像(二值图像,0非标记,1标记)
    LabelImage    - 输出图像(1第一个分割区域;2第二个分割区域;3...)
    row,col       - 图像行列数目
    返回值:
    分割区域数目
    */
    int Watershed(uchar **OriginalImage, uchar** SeedImage, uchar **LabelImage, int row, int col)
    {
    	using namespace std;
    
    	int Num = 0;                     //标志区域号,从1开始  
    	int i, j;
    
    	vector<int*> SeedCounts;       //保存每个队列种子个数容器  
    	queue<POINT> que;              //临时种子队列  
    	vector<queue<POINT>* > qu;     //保存所有标记区域种子队列的数组  
    
    	int* array;
    	queue<POINT> *uu;              //指向种子队列的指针  
    	POINT temp;
    
    	for (i = 0; i < row; i++)
    	for (j = 0; j < col; j++)
    		LabelImage[i][j] = 0;
    
    
    	int m, n, k = 0;
    	int up, down, right, left, upleft, upright, downleft, downright;
        // 预处理,提取区分每个标记区域,并初始化每个标记的种子队列  
    	// 种子是指标记区域边缘的点,它们在水位上升时向外生长。   
    	for (i = 0; i < row; i++)
    
    	for (j = 0; j < col; j++)
    	{
    		if (SeedImage[i][j] == 1 || SeedImage[i][j] == 255)  //找到一个标记区域  
    		{
    			Num++;                                      //标志号加1  
    			array = new int[256];
    			ZeroMemory(array, 256 * sizeof(int));
    			//  
    			SeedCounts.push_back(array);
    			uu = new queue<POINT>[256];
    			qu.push_back(uu);
    			temp.x = i;
    			temp.y = j;
    			que.push(temp);
    			LabelImage[i][j] = Num;
    			SeedImage[i][j] = 127;
    
    			while (!que.empty())
    			{
    				up = down = right = left = 0;
    				upleft = upright = downleft = downright = 0;
    				temp = que.front();
    				m = temp.x;
    				n = temp.y;
    				que.pop();
    
    				if (m > 0)
    				{
    					if (SeedImage[m - 1][n] == 1)
    					{
    						temp.x = m - 1;
    						temp.y = n;
    						que.push(temp);
    						LabelImage[m - 1][n] = Num;
    						SeedImage[m - 1][n] = 127;
    					}
    					else
    					{
    						up = 1;
    					}
    				}
    				if (m > 0 && n > 0)
    				{
    					if (SeedImage[m - 1][n - 1] == 1)
    					{
    						temp.x = m - 1;
    						temp.y = n - 1;
    						que.push(temp);
    						LabelImage[m - 1][n - 1] = Num;
    						SeedImage[m - 1][n - 1] = 127;
    					}
    					else
    					{
    						upleft = 1;
    					}
    				}
    
    				if (m < row - 1)
    				{
    					if (SeedImage[m + 1][n] == 1)
    					{
    						temp.x = m + 1;
    						temp.y = n;
    						que.push(temp);
    						LabelImage[m + 1][n] = Num;
    						SeedImage[m + 1][n] = 127;
    					}
    					else
    					{
    						down = 1;
    					}
    				}
    				if (m < (row - 1) && n < (col - 1))
    				{
    					if (SeedImage[m + 1][n + 1] == 1)
    					{
    						temp.x = m + 1;
    						temp.y = n + 1;
    						que.push(temp);
    						LabelImage[m + 1][n + 1] = Num;
    						SeedImage[m + 1][n + 1] = 127;
    					}
    					else
    					{
    						downright = 1;
    					}
    				}
    
    				if (n < col - 1)
    				{
    					if (SeedImage[m][n + 1] == 1)
    					{
    						temp.x = m;
    						temp.y = n + 1;
    						que.push(temp);
    						LabelImage[m][n + 1] = Num;
    						SeedImage[m][n + 1] = 127;
    					}
    					else
    					{
    						right = 1;
    					}
    				}
    				if (m > 0 && n < (col - 1))
    				{
    					if (SeedImage[m - 1][n + 1] == 1)
    					{
    						temp.x = m - 1;
    						temp.y = n + 1;
    						que.push(temp);
    						LabelImage[m - 1][n + 1] = Num;
    						SeedImage[m - 1][n + 1] = 127;
    					}
    					else
    					{
    						upright = 1;
    					}
    				}
    
    				if (n > 0)
    				{
    					if (SeedImage[m][n - 1] == 1)
    					{
    						temp.x = m;
    						temp.y = n - 1;
    						que.push(temp);
    						LabelImage[m][n - 1] = Num;
    						SeedImage[m][n - 1] = 127;
    					}
    					else
    					{
    						left = 1;
    					}
    				}
    				if (m<(row - 1) && n>0)
    				{
    					if (SeedImage[m + 1][n - 1] == 1)
    					{
    						temp.x = m + 1;
    						temp.y = n - 1;
    						que.push(temp);
    						LabelImage[m + 1][n - 1] = Num;
    						SeedImage[m + 1][n - 1] = 127;
    					}
    					else
    					{
    						downleft = 1;
    					}
    				}
    
    				//上下左右只要有一点不可生长,则本点为初始种子队列的一员  
    					if (up || down || right || left || upleft || downleft || upright || downright)
    					{
    						temp.x = m;
    						temp.y = n;
    						qu[Num - 1][OriginalImage[m][n]].push(temp);
    						SeedCounts[Num - 1][OriginalImage[m][n]]++;
    					}
    			}
    		}
    	}
    }
    
    bool actives;                                 //某一水位,所有标记种子生长完的标志  
    int WaterLevel;                         
    //淹没过程开始,水位从零上升,水位对应灰度级,采用四联通法  
    for (WaterLevel = 1; WaterLevel < 255; WaterLevel++)
    {
    	actives = true;
    	while (actives)
    	{
    		
    			actives = false;
    		for (i = 0; i<Num; i++)
    		{
    			if (!qu[i][WaterLevel].empty())
    			{
    				actives = true;
    				while (SeedCounts[i][WaterLevel]>0)
    				{
    					SeedCounts[i][WaterLevel]--;
    					temp = qu[i][WaterLevel].front();
    					qu[i][WaterLevel].pop();
    					m = temp.x;
    					n = temp.y;
    					if (m > 0)
    					{
    						if (!LabelImage[m - 1][n])
    						{
    							temp.x = m - 1;
    							temp.y = n;
    							lImage[m - 1][n] = i + 1;
    
    								//上方点标记为已淹没区域。这个标记与扫描点区域号相同,一定在标号所在区域。  
    							if (OriginalImage[m - 1][n] <= WaterLevel)
    							{
    								qu[i][WaterLevel].push(temp);
    							}
    							else
    							{
    								qu[i][OriginalImage[m - 1][n]].push(temp);
    								SeedCounts[i][OriginalImage[m - 1][n]]++;
    							}
    						}
    					}
    
    					if (m < row - 1)
    					{
    						if (!LabelImage[m + 1][n])
    						{
    							temp.x = m + 1;
    							temp.y = n;
    							LabelImage[m + 1][n] = i + 1;
    
    							if (OriginalImage[m + 1][n] <= WaterLevel)
    							{
    								qu[i][WaterLevel].push(temp);
    							}
    							else
    							{
    								qu[i][OriginalImage[m + 1][n]].push(temp);
    								SeedCounts[i][OriginalImage[m + 1][n]]++;
    							}
    						}
    					}
    
    					if (n < col - 1)
    					{
    						if (!LabelImage[m][n + 1])
    						{
    							temp.x = m;
    							temp.y = n + 1;
    							LabelImage[m][n + 1] = i + 1;
    
    							if (OriginalImage[m][n + 1] <= WaterLevel)
    							{
    								qu[i][WaterLevel].push(temp);
    							}
    							else
    							{
    								qu[i][OriginalImage[m][n + 1]].push(temp);
    								SeedCounts[i][OriginalImage[m][n + 1]]++;
    							}
    						}
    					}
    
    					if (n > 0)
    					{
    						if (!LabelImage[m][n - 1])
    						{
    							temp.x = m;
    							temp.y = n - 1;
    							LabelImage[m][n - 1] = i + 1;
    
    							if (OriginalImage[m][n - 1] <= WaterLevel)
    							{
    								qu[i][WaterLevel].push(temp);
    							}
    							else
    							{
    								qu[i][OriginalImage[m][n - 1]].push(temp);
    								SeedCounts[i][OriginalImage[m][n - 1]]++;
    							}
    						}
    					}
    				}
    				SeedCounts[i][WaterLevel] = (int)qu[i][WaterLevel].size();
    			}
    		}
    	}
    }
    
    while (!qu.empty())
    {
    	uu = qu.back();
    	delete[] uu;
    	qu.pop_back();
    }
    while (!SeedCounts.empty())
    {
    	array = SeedCounts.back();
    	delete[] array;
    	SeedCounts.pop_back();
    }
    return Num;
    }
    

    调用方法如下:

    int ROW = Img->height;
    int COL = Img->width;
    uchar **arrDist;
    uchar **arrSeed;
    uchar **arrLabel;
    arrDist = (uchar **)malloc(ROW * sizeof(uchar*));
    arrSeed = (uchar **)malloc(ROW * sizeof(uchar*));
    arrLabel = (uchar **)malloc(ROW * sizeof(uchar*));
    for (int i = 0; i < ROW; i++)
    {
    	arrDist[i] = (uchar*)malloc(COL * sizeof(uchar));
    
    	for (int i = 0; i < ROW; i++)
    	{
    		memcpy(arrDist[i], imgMat.ptr(i), COL* sizeof(uchar));
    		for (int j = 0; j < COL; j++)
    			arrDist[i][j] = 255 - arrDist[i][j];
    	}
    }
    	
    	for (int i = 0; i< ROW; i++)
    		memcpy(arrSeed[i], imgMat2.ptr(i), COL* sizeof(uchar));
    	
    int num = Watershed(arrDist, arrSeed, arrLabel, ROW, COL);

    参考:

    http://www.360doc.com/content/10/0512/21/1217721_27287483.shtml#

    http://blog.csdn.net/augusdi/article/details/9022509

    http://blog.csdn.net/nagao_kagetora/article/details/5995925


    展开全文
  • 传统分水岭算法基本原理 分水岭比较经典的计算方法是L.Vincent于1991年在PAMI上提出的[1]。传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学

    本次我们来看图像分割,同样也是OpenCV中较为重要的一个部分。图像分割是按照一定的原则,将一幅图像分为若干个互不相交的小局域的过程,它是图像处理中最为基础的研究领域之一。目前有很多图像分割方法,其中分水岭算法是一种基于区域的图像分割算法,分水岭算法因实现方便,已经在医疗图像,模式识别等领域得到了广泛的应用。

    传统分水岭算法基本原理

    分水岭比较经典的计算方法是L.Vincent于1991年在PAMI上提出的[1]。传统的分水岭分割方法,是一种基于拓扑理论的数学形态学的分割方法,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆地,而集水盆地的边界则形成分水岭。分水岭的概念和形成可以通过模拟浸入过程来说明。在每一个局部极小值表面,刺穿一个小孔,然后把整个模型慢慢浸人水中,随着浸入的加深,每一个局部极小值的影响域慢慢向外扩展,在两个集水盆汇合处构筑大坝如下图所示,即形成分水岭。我们来看传统分水岭算法示意图:
    在这里插入图片描述
    然而基于梯度图像的直接分水岭算法容易导致图像的过分割,产生这一现象的原因主要是由于输入的图像存在过多的极小区域而产生许多小的集水盆地,从而导致分割后的图像不能将图像中有意义的区域表示出来。所以必须对分割结果的相似区域进行合并。

    改进的分水岭算法基本原理

    因为传统的分水岭分割算法会由于图像中的噪声或其他不规则性而产生过度分割的结果。因此OpenCV实现了一个基于标记的分水岭算法,可以指定哪些是要合并的山谷点,哪些不是。这是一个交互式的图像分割。我们所做的是给我们知道的对象赋予不同的标签。用一种颜色(或强度)标记我们确定为前景或对象的区域,用另一种颜色标记我们确定为背景或非对象的区域,最后用0标记我们不确定的区域。这是我们的标记。然后应用分水岭算法。然后我们的标记将使用我们给出的标签进行更新,对象的边界值将为-1。传统的基于梯度的分水岭算法和改进后基于标记的分水岭算法示意图如下图所示:
    在这里插入图片描述

    从上图可以看出,传统基于梯度的分水岭算法由于局部最小值过多造成分割后的分水岭较多。而基于标记的分水岭算法,水淹过程从预先定义好的标记图像(像素)开始,较好的克服了过度分割的不足。本质上讲,基于标记点的改进算法是利用先验知识来帮助分割的一种方法。因此,改进算法的关键在于如何获得准确的标记图像,即如何将前景物体与背景准确的标记出来。

    OpenCV中的图像分割

    OpenCV提供了相关的函数API进行分水岭分割操作,我们来看函数原型:
    markers=cv.watershed(image, markers)

    image:输入8位3通道图像。

    markers:标记的输入/输出32位单通道图像(图)。 它的大小应与image相同。

    我们使用示例图像:
    在这里插入图片描述

    首先我们使用Otsu的二值化找到硬币的近似估计值:

    view plaincopy to clipboardprint?
    def watershed(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

    cv2.imshow('show', thresh)  
    cv2.waitKey(0)  
    

    在这里插入图片描述

    现在我们需要去除图像中的任何小的白噪声,因此我们要使用形态学开运算,为了去除物体上的小洞,我们要使用形态学闭运算,所以,现在我们可以确定,靠近物体中心的区域是前景,远离物体的区域是背景,只有硬币的边界区域是我们不确定的区域.

    我们需要提取出我们确信它们是硬币的区域,腐蚀边界像素,不管剩下的是什么,我们都可以确定它是硬币.如果它们不相互接触还可以继续,如果它们相互接触,另一个好的选择是找到距离变换并应用一个合适的阈值.

    为此,我们对结果进行了扩张,扩张将对象边界增加为背景,通过这种方法,我们可以确保背景中的任何区域都是真正的背景,因为边界区域被移除.

    剩下的区域是我们不知道的区域,无论是硬币还是背景.分水岭算法应该找到它, 这些区域通常围绕着前景和背景相遇的硬币边界(甚至两个不同的硬币相遇),它可以从sure_bg区域中减去sure_fg区域获得。

    来看代码:

    view plaincopy to clipboardprint?
    # noise removal  
    kernel = np.ones((3,3),np.uint8)  
    opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)  
       
    # sure background area  
    sure_bg = cv2.dilate(opening,kernel,iterations=3)  
       
    # Finding sure foreground area  
    dist_transform = cv2.distanceTransform(opening,cv.DIST_L2,5)  
    ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(),255,0)  
       
    # Finding unknown region  
    sure_fg = np.uint8(sure_fg)  
    unknown = cv2.subtract(sure_bg,sure_fg)  
    

    我们看处理之后的图像,sure_bg:
    在这里插入图片描述
    sure_fg:

    在这里插入图片描述

    看一下处理之后的图像:
    在这里插入图片描述

    现在我们可以确定哪些是硬币的区域,哪些是背景,哪些是背景.因此,我们创建标记(它是一个与原始图像相同大小的数组,但使用int32数据类型)并对其内部的区域进行标记。

    cv2.connectedComponents()

    将图像的背景标记为0,然后其他对象从1开始标记为整数。

    我们知道,如果背景是0,那么分水岭将会被认为是未知的区域, 所以我们用不同的整数来标记它,用0表示由未知定义的未知区域。

    view plaincopy to clipboardprint?
    # Marker labelling  
    ret, markers = cv2.connectedComponents(sure_fg)  
       
    # Add one to all labels so that sure background is not 0, but 1  
    markers = markers+1  
       
    # Now, mark the region of unknown with zero  
    markers[unknown==255] = 0  
    

    标记已经准备好了,现在是最后一步的时候了,应用分水岭,最终代码:

    view plaincopy to clipboardprint?
    def watershed(img):  
         gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  
         ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)  
       
         kernel = np.ones((3, 3), np.uint8)  
         opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)  #   
    形态开运算  
       
         sure_bg = cv2.dilate(opening, kernel, iterations=3)  
       
         dist_transform = cv2.distanceTransform(opening, cv2.DIST_L2, 5)  
         ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0)  
       
         sure_fg = np.uint8(sure_fg)  
         unknown = cv2.subtract(sure_bg, sure_fg)  
       
         ret, markers = cv2.connectedComponents(sure_fg)  
       
         markers = markers + 1  
       
         markers[unknown == 255] = 0  
       
         markers = cv2.watershed(img, markers)  
         img[markers == -1] = [255, 0, 0]  
       
         cv2.imshow('img', img)  
         cv2.waitKey(0)  
    

    在这里插入图片描述
    可以看到,最终结果显示完美分割。
    查看文章汇总页https://blog.csdn.net/weixin_44237705/article/details/107864965
    更多openvino技术信息可以入群交流~
    申请备注:CSDN
    在这里插入图片描述

    展开全文
  • 分水岭算法原理和c++实现

    千次阅读 2019-02-15 18:06:59
    算法原理 关于分水岭算法的介绍和原理,大家可以去看这篇博客,上面有很多动图,十分生动有趣:https://www.cnblogs.com/mikewolf2002/p/3304118.html。我们,先来看一下opencv提供的分水岭算法的函数接口 void ...

    算法原理

    关于分水岭算法的介绍和原理,大家可以去看这篇博客,上面有很多动图,十分生动有趣:https://www.cnblogs.com/mikewolf2002/p/3304118.html。我们,先来看一下opencv提供的分水岭算法的函数接口

    void watershed( InputArray image, InputOutputArray markers ); 
    

    输入图像必须是3通道的RGB图像,最为关键的是第二个参数markers,这个参数包含不同区域的轮廓,每个轮廓有一个自己唯一的编号,轮廓的定位可以通过Opencv中findContours方法实现。然后分水岭算法会根据markers传入的轮廓作为种子(也就是所谓的注水点),对图像上其他的像素点根据分水岭算法规则进行判断,并对每个像素点的区域归属进行划定,直到处理完图像上所有像素点。而区域与区域之间的分界处的值被置为“-1”,以做区分。

    算法步骤

    参考:https://blog.csdn.net/kakiebu/article/details/82965629

    • 将RGB图像灰度化
    • 使用大津法转为二值图,并做形态学闭合操作
    • 形态学闭操作
    • 距离变换
    • 将距离变换结果归一化到[0-1]之间
    • 将图像取值范围变为8位(0-255)
    • 再使用大津法转为二值图,并做形态学闭合操作
    • 使用findContours寻找marks
    • 对原图做形态学的腐蚀操作
    • 执行分水岭算法
    • 随机分配颜色和显示

    代码实现

    //分水岭算法
    Mat WaterSegment(Mat src) {
    	int row = src.rows;
    	int col = src.cols;
    	//1. 将RGB图像灰度化
    	Mat grayImage = speed_rgb2gray(src);
    	//2. 使用大津法转为二值图,并做形态学闭合操作
    	threshold(grayImage, grayImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
    	//3. 形态学闭操作
    	Mat kernel = getStructuringElement(MORPH_RECT, Size(9, 9), Point(-1, -1));
    	morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
    	//4. 距离变换
    	distanceTransform(grayImage, grayImage, DIST_L2, DIST_MASK_3, 5);
    	//5. 将图像归一化到[0, 1]范围
    	normalize(grayImage, grayImage, 0, 1, NORM_MINMAX);
    	//6. 将图像取值范围变为8位(0-255)
    	grayImage.convertTo(grayImage, CV_8UC1);
    	//7. 再使用大津法转为二值图,并做形态学闭合操作
    	threshold(grayImage, grayImage, 0, 255, THRESH_BINARY | THRESH_OTSU);
    	morphologyEx(grayImage, grayImage, MORPH_CLOSE, kernel);
    	//8. 使用findContours寻找marks
    	vector<vector<Point>> contours;
    	findContours(grayImage, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));
    	Mat marks = Mat::zeros(grayImage.size(), CV_32SC1);
    	for (size_t i = 0; i < contours.size(); i++)
    	{
    		//static_cast<int>(i+1)是为了分水岭的标记不同,区域1、2、3...这样才能分割
    		drawContours(marks, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i + 1)), 2);
    	}
    	//9. 对原图做形态学的腐蚀操作
    	Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    	morphologyEx(src, src, MORPH_ERODE, k);
    	//10. 调用opencv的分水岭算法
    	watershed(src, marks);
    	//11. 随机分配颜色
    	vector<Vec3b> colors;
    	for (size_t i = 0; i < contours.size(); i++) {
    		int r = theRNG().uniform(0, 255);
    		int g = theRNG().uniform(0, 255);
    		int b = theRNG().uniform(0, 255);
    		colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
    	}
    
    	// 12. 显示
    	Mat dst = Mat::zeros(marks.size(), CV_8UC3);
    	int index = 0;
    	for (int i = 0; i < row; i++) {
    		for (int j = 0; j < col; j++) {
    			index = marks.at<int>(i, j);
    			if (index > 0 && index <= contours.size()) {
    				dst.at<Vec3b>(i, j) = colors[index - 1];
    			}
    			else if (index == -1)
    			{
    				dst.at<Vec3b>(i, j) = Vec3b(255, 255, 255);
    			}
    			else {
    				dst.at<Vec3b>(i, j) = Vec3b(0, 0, 0);
    			}
    		}
    	}
    	return dst;
    }
    

    算法效果

    原图
    结果图

    参考博客

    https://www.cnblogs.com/wjy-lulu/p/7056466.html

    展开全文
  • 分水岭算法原理

    2012-11-25 21:17:21
    所谓分水岭算法有好多种实现算法,拓扑学,形态学,浸水...Watershed Algorithm(分水岭算法),顾名思义,就是根据分 水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那 么那一定是水绕山

    转载地址:http://hi.baidu.com/saminigod/item/4e0170cb582b6d0dac092f71

    所谓分水岭算法有好多种实现算法,拓扑学,形态学,浸水模拟和降水模拟等方式。
    要搞懂就不容易了。Watershed Algorithm(分水岭算法),顾名思义,就是根据分
    水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那
    么那一定是水绕山,山围水的情形。而区分高山(plateaus)与水的界线,以及湖
    与湖之间的间隔或都是连通的关系,就是我们可爱的分水岭(watershed)。为了得
    到一个相对集中的集水盆,那么让水涨到都接近周围的最高的山顶就可以了,再涨就
    要漏水到邻居了,而邻居,嘿嘿,水质不同诶,会混淆自我的。那么这样的话,我们
    就可以用来获取边界高度大,中间灰阶小的物体区域了,它就是集水盆。
    浸水法,就是先通过一个适当小的阈值得到起点,即集水盆的底;然后是向周围淹没
    也就是浸水的过程,直到得到分水岭。当然如果我们要一直淹没到山顶,即是一直处
    理到图像灰阶最高片,那么,当中就会出现筑坝的情况,不同的集水盆在这里想相遇
    了,我们要洁身自爱,到这里为止,因为都碰到边界了。不再上山。构筑属于自己的分水岭。
    在计算机图形学中,可利用灰度表征地貌高。图像中我们可以利用灰度高与地貌高
    的相似性来研究图像的灰度在空间上的变化。这是空域分析,比如还可以通过各种
    形式的梯度计算以得到算法的输入,进行浸水处理。分水岭具有很强的边缘检测能力,
    对微弱的边缘也有较好的效果。
    为会么这么说呢?为什么有很强的边缘检测能力,而又能得到相对集中的连通的
    集水盆?现实中很好办,我们在往凹地加水的时候,直到它涨到这一块紧凑的山岭
    边缘就不加了;但是如果有一条小山沟存在,那没办法,在初始阈值分割的时候,
    也就是山沟与集水盆有同样的极小值,而且它们之间是以这个高度一直连接的。
    那没关系,我们将它连通。在图像上呢?如何实现?
    看看算法,算法思想是这样的:
    首先准备好山和初始的水。这山就是我们的初始图像了,比如用自然获取的图像
    的梯度来表征山地的每一点的高度吧;而初始的水就是在阈值记为Thre底下,
    所有的低于这个高度的整个山地都加水,直到这个阈值Thre高度。从而有三个
    初始量:unsigned char** Ori_image、char** Seed_image和int** Label_image。
    最后一个是为最终的结果做准备的。当然要做好初始化,比如,Ora_image赋值为
    原图像(256色灰度图)的梯度值,Seed_image则是初始状态下有水的置位,无水的
    复位,而Label_image则全初始化为0,最终得到的是各点对应的区域号。
    接下来是考虑将已加的水进行记录,记录成连通的区域,也就是看看有多少个互
    不相关的集水盆,有五个,那么我们就涨出五个湖,而且尽可能的高,只要大家
    想到不溢出。在算法上,有多少个连通的区域就记录成多少个数据结构,工夫就
    在于如何将这些连通的区域连接成一块,并由一个数据结构来表达了。很好,
    我们准备用一个向量容器来实现初始保存,保存所有标记区域种子队列的数组,
    里面放的是种子队列的指针vector*> vque,而且这个队列是由一系列属于同
    一个区域的图像点组成,我们来自一个集水盆:);其保存方式是这样的:
    queue *pque=new queue[256];vque.push_back(pque),这样便将一个成员
    放进到这个区域来了,即容器--集水盆的保管都,容器中的每个指针,都指
    向一个集水盆,也就是我们要的连通区域;所以我们可以方便地由这个容器数
    据结构直接读值的方便性进行操作,一个脚标就可以得到一个区域(队列指针)
    的指针;而每个队列还不简单,并不是一列整形数那么易搞,所以说啊,这个
    算法,真头痛,这个队列的一个成员是一个点;而注意到vque里存放的一256
    个队列的的起始指针,真够残忍的。也就是说vque[i][j]就表达了一个队列,
    这个队列里可以存储操作一系列的点;显然容量取256是因为所有的初始或者是
    最终的区域中可能有0-256之间的不同的灰阶的点,那么我一个区域分用256个
    队列来记录这些成员点啦,很有可能,这里就只有一个集水盆,那么,256个灰
    阶的点都存在一个区域就有可能了。
    统计初始连通区域的方法是,八连通邻域法,即从逐一扫描输入的Seed_image
    的每个像素点,将所有的标记了的初始集水盆一一纳入各自的区域,这是整修
    图像的扫描,形成外循环。先创建一个临时队列quetem,用来处理当前初始集
    水盆的连通连接,将逐一扫描到的属于一个特定的初始集水盆区域的可生长点
    暂存,并形成一个内循环。对当前扫描点的处理是,首先判断该点是否为某个
    初始集水盆的点,如果不是跳过;接下来是,如果是初始集水盆的点,那么它
    的八连通域中是否存在不可生长的点(这里的不可生长是指Seed_image中没有
    标记的点),扫描的八连通邻域中的点是可生长的,即有标记的,则将之加入
    到临时队列中quetem;如果扫描到的连通邻域中有不可生长的至少一个点存在,
    那么加入到种子队列,记当前区域号为Num,当前扫描点为(m,n),从而当前的
    灰阶为Ori_image[m][n],将当前点添加到种子队列:vque[Num-1]
    [Ori_image[m][n]].push(POINT(m,n))。这里有两个循环,一个是quetem,
    另一个是Seed_image。直到两个循环完整结束,那么就得到了各个连通初始集
    水盆的记录,保存标记是区域号Num;而我们同时得到了初始的分水岭,那就放
    在了保存地点vque,这里面标识了它们对应的区域号,和区域里面对应的点的
    灰阶,即是特定区域特定灰阶对应的点的集合;我们可以获取这些分水岭的点
    所在的区域号,可以得到该区域的所有的灰阶的点信息。一句话,统计连通区
    域的功能有两个,一是标记初始区域,二是找分水岭。
    初始的区域标记好了,分岭也找到了,那么可以开始“水漫梁山”了。这就是
    淹没过程。淹没过程由也是由一个内嵌循环的循环来实现的:外循环是做水位
    上升(这里循环次数一定要256以内),waterlevel的上升,原来是已经做过
    了初始的水位分割,那么现在可以从Thre开始了,让水位慢慢上升,让它原本
    的湖慢慢扩张,尽量利用其应有的空间,而又不至于淹没到其它的邻居湖泊。
    内循环是扫描每个初始区域(当前Num,从而有Num个循环)的分水岭的点
    (在vque[][]中),按照给定的水位进行扩张。扩张过程是这样的:扫描到
    的分水岭的当前点,对其进行四连通邻域进行逐一检查,如果四连通域中有点
    没有标记的(那这一定是高度较高的点,较低的前面一定扫描过),那么先对
    该点以本区域号做标记Num(注意是当前的Num);再判断它在当前水位下是否
    可生长(灰阶是否小于等于waterlevel),如果可生长那么加入
    到vque[Num][waterlevel]种子队列中,将会再次进入内循环,否则如果在
    当前水位下不可生长,则加入到这个邻域点的分水岭集合中vque[Num][Ori_image[邻域点]]
    队列中。如此往复循环,直到对应区域完成,一个水位,扫描所有的区域的分
    水岭,这样各自同时在一个水位下扩张,保证了不出现跳跃的情况出现
    (就是一个水位一个区域全局扩张)。
    最终,所有的区域在每个水位都扩张完毕,得到了分割图,我们的大湖泊形成了。
    这是分水岭算法的一种实现方式。仔细考察不难发现这种实现方式不能产生新
    的集水盆,也就是说,由于初始集水盆的局限性,很可能会浪费大部分没有发
    掘出来的起始点较高的集水盆地。这样,我们就要对算法进行修改了。实现方
    式是:在淹没的过程中,我们是由阈值Thre的水位开始淹没的,那么我们可以
    对初始区域之外的没有标记的点(从Seed_image中考察),对之进行标记,条
    件是先把这一轮的内循环做好,然后在剩下的没标记区域中发掘新的集水盆,
    并加入到我们的种子队列中,下一个水位开始,就又多了一个新成员了,让它
    们不断膨胀,成长,拥有自己的小天的成员就会逐一的被分割出来。不过话说
    回来,我们这里是采用梯度图像,一般情况下,阈值初始分割能够满足我们的
    要求,把灰阶变化平滑的先截取出来,梯度信息已然足够强大;而如果采用了
    新盆地扩张,则比较适用于原始图像。
    分水岭算法主要的分割目的在于找到图像的连通区域。利用梯度信息作为输入
    图像,会有一个矛盾点,如果对原始图像进行梯度计算时不作滤波平滑处理,
    很容易将物体分割成多个物体,那是因为噪声的影响;而如果进行滤波处理,
    又容易造成将某些原本几个的物体合成一个物体。当然这里的物体主要还是指
    图像变化不大或者说是灰度值相近的目标区域。


    展开全文
  • OpenCV—图像分割中的分水岭算法原理与应用

    万次阅读 多人点赞 2015-10-18 09:58:56
    然而基于梯度图像的直接分水岭算法容易导致图像的过分割,本文介绍了传统分水岭算法以及基于标记图像的分水岭算法的基本原理,以OpenCV自带函数watershed()为例说明了基于标记图像的分水岭算法的应用。
  • 分水岭算法实现(C++、opencv) 1.作用:分割图像, 2.实现: #include <cmath> #include <iostream> #include <memory> #include <opencv2/core/core.hpp> #include <opencv2/...
  • 形态学分水岭算法原理及示例实现

    千次阅读 2019-05-15 11:20:10
    分水岭算法(watershed)是一种比较基本的数学形态学分割算法,其基本思想是将灰度图像转换为梯度图像,将梯度值看作高低起伏的山岭,将局部极小值及其邻域看作一个“集水盆”。设想一个个“集水盆”中存在积水,且...
  • 分水岭算法原理和c++源代码

    千次阅读 2017-07-02 22:48:40
    Watershed Algorithm(分水岭算法),顾名思义,就是根据分水岭的构成来考虑图像的分割。现实中我们可以或者说可以想象有山有湖的景象,那么那一定是水绕 山,山围水的情形。当然在需要的时候,要人工构筑分水岭,以...
  • 1. 分水岭原理 分水岭算法是一种基于拓扑理论的数学形态学分割理论,其基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域成为集水盆,而集水盆...
  • OpenCV中的watershed函数实现的分水岭算法是基于“标记”的分割算法,用于解决传统的分水岭算法过度分割的问题。试想,一副图片中肯定有N多个“山谷”,它们中的很多是我们不想要的。 对于标记...
  • 分水岭算法原理 分水岭算法是一种图像区域分割法,在分割的过程中,它会把跟临近像素间的相似性作为重要的参考依据,从而将在空间位置上相近并且灰度值相近的像素点互相连接起来构成一个封闭的轮廓,封闭性是...
  • 数学形态学以图像的形态特征为研究对象,用具有... 分水岭算法(Watershed)基本思想是把图像看作是测地学上的拓扑地貌,图像中每一点像素的灰度值表示该点的海拔高度,每一个局部极小值及其影响区域称为集水盆,而集水盆
  • 分 水 岭 算 法 WatershedAlgorithm( 分水岭算法 )顾名思义就是根据分水岭的构成来考虑图像的分 割现实中人们可以或者说可以想象有山有湖的景象 那么那一定是水绕山 山围水的情形 当然在需要的时候 要人工构筑分水岭...
  • 2.原理 第1步: 通过分水岭算法watersheds()获取图像的盆地。 第2步: 根据第一步分水岭算法分离结果,若盆地部分的灰度**< threshold**,则被合并到一起。设B1和B2分别为相邻盆地的最小灰度值,W为将盆地分割...
  • 文章目录分水岭算法原理OpenCV函数使用流程使用分水岭算法的通用步分割鸡蛋为例参考 分水岭算法原理 算法步骤: 1.构建图像梯度图像。 2.通过一定规则生成 n 个最初的注水区域(先验知识或局部梯度最小值,种子)。 ...
  • 分水岭算法是一种图像区域分割法,在分割的过程中...关于分水岭算法原理大家可以看我在2016年07月20日在CSDN上发表的一篇博文,链接如下: http://blog.csdn.net/wenhao_ir/article/details/51956025 OpenCV提供...
  • 整体等于其部分之和 ——欧几里得 整体大于其部分之和 ——Max Watheimer ...2.原理 第1步: 通过分水岭算法watersheds()获取图像的盆地。 第2步: 根据第一步分水岭算法分离结...
  • python opencv之之分分水水岭岭算算法法示示例例 这篇文章主要介绍了python opencv之分水岭...目目标 使用分水岭算法对基于标记的图像进行分割 使用函数cv2.watershed) 原原理理 灰度图像可以被看成拓扑平面,灰度值高的
  • 使用函数cv2.watershed() 原理: 灰度图像可以被看成拓扑平面,灰度值高的区域可以看出山峰,灰度值低的区域可以看成是山谷。向每一个山谷当中灌不同颜色的水。水位升高,不同山谷的水会汇合,为防止不同山谷的水...
  • 有关分水岭算法原理网上有很多解释,请自行查阅,这里不解释。下面三张图就是push和pop过程演示(不知道为何图片插进来就自动旋转了,尴尬,如果人懂,望一定告知解决方法,谢谢)下面是opencv中分水岭算法源码:...
  • 信号的截断产生了能量泄漏,而用FFT算法计算频谱又产生了栅栏效应,从原理上讲这两种误差都是不能消除的。在FFT分析中为了减少或消除频谱能量泄漏及栅栏效应,可采用不同的截取函数对信号进行截短,截短函数称为窗...
  • Watershed就是传说中的分水岭算法, 它将一幅图像看成是一块有湖泊和山川组成的地形。 图像灰度值大的像素对应海拔高的山地, 灰度值低的像素对应于海拔低的盆地。Watershed分割是模拟湖水上涨并在湖泊相遇处筑坝的...
  • OpenCV分水岭算法中标记图的妙用

    千次阅读 2017-04-12 22:06:38
    分水岭算法不是第一次看见了,因为项目中没用过,就一直... 下面进入正题,首先这个标记图作用是什么(分水岭算法原理这里就不说了),在一些分割算法如watershed和grabCut算法中,如果直接对原图进行处理,那么将图像
  • • 学习函数: cv2.watershed() 原理   任何一幅灰度图像都可以被看成拓扑平面,灰度值高的区域可以被看成是山峰,灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同颜色的水,随着水的位的升高,...
  • OpenCV---分水岭算法

    2018-07-09 13:32:00
    OpenCV学习(7) 分水岭算法(1)(原理简介简单明了) OpenCV-Python教程:31.分水岭算法对图像进行分割(步骤讲解不错) 使用分水岭算法进行图像分割 (一)获取灰度图像,二值化图像,进行形态学操作,消除噪点 ...
  • 本文介绍了python opencv之分水岭算法示例,分享给大家,具体如下:目标使用分水岭算法对基于标记的图像进行分割使用函数cv2.watershed()原理:灰度图像可以被看成拓扑平面,灰度值高的区域可以看出山峰,灰度值低的...
  • 分水岭算法图像分割(Image Segmentation with Watershed Algorithm) 目标 本节我们将要学习 • 使用基于掩模的分水岭算法进行图像分割 • 函数:cv2.connectedComponents()、cv2.watershed() 1. 原理 任何一副...
  • watershed() #原理: #灰度图像可以被看成拓扑平面,灰度值高的区域可以看出山峰,灰度值低的区域可以看成是山谷。 向每一个山谷当中灌不同颜色的水。水位升高,不同山谷的水会汇#合,为防止不同山谷的水汇合 ,小...
  • ⚠️由于自己的拖延症,3.4.3翻到一半,OpenCV发布了4.0.0了正式版,所以接下来是按照4.0.0翻译的。...我们会学到使用分水岭算法来做基于标记的图像分割。 我们会遇到:cv.watershed() 原理 任何灰度图像都可以看...

空空如也

空空如也

1 2 3
收藏数 41
精华内容 16
关键字:

watershed算法原理