图像处理中的四邻域膨胀

2015-12-30 16:48:37 minushuang 阅读数 12036

原理:在特殊领域运算形式——结构元素(Sturcture Element),在每个像素位置上与二值图像对应的区域进行特定的逻辑运算。运算结构是输出图像的相应像素。运算效果取决于结构元素大小内容以及逻辑运算性质。

结构元素:膨胀和腐蚀操作的最基本组成部分,用于测试输出图像,通常要比待处理的图像小还很多。二维平面结构元素由一个数值为0或1的矩阵组成。结构元素的原点指定了图像中需要处理的像素范围,结构元素中数值为1的点决定结构元素的邻域像素在进行膨胀或腐蚀操作时是否需要参与计算。

先来定义一些基本符号和关系。

1.         元素

设有一幅图象X,若点aX的区域以内,则称aX的元素,记作aX,如图6.1所示。

2.         B包含于X

设有两幅图象BX。对于B中所有的元素ai,都有aiX,则称B包含于(included in)X,记作B  X,如图6.2所示。

3.         B击中X

设有两幅图象BX。若存在这样一个点,它即是B的元素,又是X的元素,则称B击中(hit)X,记作BX,如图6.3所示。

4.         B不击中X

设有两幅图象BX。若不存在任何一个点,它即是B的元素,又是X的元素,即BX的交集是空,则称B不击中(miss)X,记作BX=Ф;其中∩是集合运算相交的符号,Ф表示空集。如图6.4所示。

6.1     元素

6.2     包含

6.3     击中

6.4     不击中

5.         补集

设有一幅图象X,所有X区域以外的点构成的集合称为X的补集,记作Xc,如图6.5所示。显然,如果BX=Ф,则BX的补集内,即B  Xc

6.5     补集的示意图

6.         结构元素

设有两幅图象BX。若X是被处理的对象,而B是用来处理X的,则称B为结构元素(structure element),又被形象地称做刷子。结构元素通常都是一些比较小的图象。

7.         对称集

设有一幅图象B,将B中所有元素的坐标取反,即令(xy)变成(-x-y),所有这些点构成的新的集合称为B的对称集,记作Bv,如图6.6所示。

8.         平移

设有一幅图象B,有一个点a(x0,y0),将B平移a后的结果是,把B中所有元素的横坐标加x0,纵坐标加y0,即令(xy)变成(x+x0y+y0),所有这些点构成的新的集合称为B的平移,记作Ba,如图6.7所示。

6.6     对称集的示意图

6.7     平移的示意图

好了,介绍了这么多基本符号和关系,现在让我们应用这些符号和关系,看一下形态学的基本运算。

6.1 腐蚀

把结构元素B平移a后得到Ba,若Ba包含于X,我们记下这个a点,所有满足上述条件的a点组成的集合称做XB腐蚀(Erosion)的结果。用公式表示为:E(X)={a| Ba  X}=X  B,如图6.8所示。

6.8     腐蚀的示意图

6.8X是被处理的对象,B是结构元素。不难知道,对于任意一个在阴影部分的点aBa 包含于X,所以XB腐蚀的结果就是那个阴影部分。阴影部分在X的范围之内,且比X小,就象X被剥掉了一层似的,这就是为什么叫腐蚀的原因。

值得注意的是,上面的B是对称的,即B的对称集Bv=B,所以XB腐蚀的结果和X Bv腐蚀的结果是一样的。如果B不是对称的,让我们看看图6.9,就会发现XB腐蚀的结果和X Bv腐蚀的结果不同。

6.9     结构元素非对称时,腐蚀的结果不同

6.8和图6.9都是示意图,让我们来看看实际上是怎样进行腐蚀运算的。

在图6.10中,左边是被处理的图象X(二值图象,我们针对的是黑点),中间是结构元素B,那个标有origin的点是中心点,即当前处理元素的位置,我们在介绍模板操作时也有过类似的概念。腐蚀的方法是,拿B的中心点和X上的点一个一个地对比,如果B上的所有点都在X的范围内,则该点保留,否则将该点去掉;右边是腐蚀后的结果。可以看出,它仍在原来X的范围内,且比X包含的点要少,就象X被腐蚀掉了一层。

6.10   腐蚀运算

6.11为原图,图6.12为腐蚀后的结果图,能够很明显地看出腐蚀的效果。

6.11    原图

6.12   腐蚀后的结果图

下面的这段程序,实现了上述的腐蚀运算,针对的都是黑色点。参数中有一个BOOL变量,为真时,表示在水平方向进行腐蚀运算,即结构元素B  ;否则在垂直方向上进行腐蚀运算,即结构元素B  

腐蚀源码

BOOL Erosion(HWND hWnd,BOOL Hori)

{

       DWORD                             OffBits,BufSize;

LPBITMAPINFOHEADER    lpImgData;

       LPSTR                   lpPtr;

       HLOCAL                  hTempImgData;

       LPBITMAPINFOHEADER    lpTempImgData;

       LPSTR                            lpTempPtr;

       HDC                      hDc;

       HFILE                    hf;

       LONG                    x,y;

       unsigned char              num;

       int                        i;

//为了处理方便,仍采用256级灰度图,不过只用调色板中0和255两项

if( NumColors!=256){  

           MessageBox(hWnd,"Must be a mono bitmap with grayscale palette!",

"Error Message",MB_OK|MB_ICONEXCLAMATION);

return FALSE;

}

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

//BufSize为缓冲区大小

       BufSize=OffBits+bi.biHeight*LineBytes;

       //为新的缓冲区分配内存

       if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)

{

            MessageBox(hWnd,"Error alloc memory!","Error Message",

MB_OK|MB_ICONEXCLAMATION);

return FALSE;

    }

     lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);    

       lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);

       //拷贝头信息和位图数据     

       memcpy(lpTempImgData,lpImgData,BufSize);

       if(Hori)

       {   

//在水平方向进行腐蚀运算

              for(y=0;y<bi.biHeight;y++){

                     //lpPtr指向原图数据,lpTempPtr指向新图数据

                     lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes)+1;

                     lpTempPtr=(char*)lpTempImgData+

(BufSize-LineBytes-y*LineBytes)+1;

                     for(x=1;x<bi.biWidth-1;x++){ 

//注意为防止越界,x的范围从1到宽度-2

                            num=(unsigned char)*lpPtr;

                            if (num==0){  //因为腐蚀掉的是黑点,所以只对黑点处理

                                   *lpTempPtr=(unsigned char)0;  //先置成黑点

                                   for(i=0;i<3;i++){

                                          num=(unsigned char)*(lpPtr+i-1);

                                          if(num==255){ 

//自身及上下邻居中若有一个不是黑点,则将该点腐

//蚀成白点

                                                 *lpTempPtr=(unsigned char)255;

                                                 break;

                                          }

                                   }

                            }

//原图中就是白点的,新图中仍是白点

                            else *lpTempPtr=(unsigned char)255;  

                            //指向下一个象素

                            lpPtr++; 

                            lpTempPtr++;

                     }

              }

       }

else{ 

//在垂直方向进行腐蚀运算

              for(y=1;y<bi.biHeight-1;y++){ //注意为防止越界,y的范围从1到高度-2

                     //lpPtr指向原图数据,lpTempPtr指向新图数据

                     lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);

                     lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);

                     for(x=0;x<bi.biWidth;x++){

                            num=(unsigned char)*lpPtr;

                            if (num==0){ //因为腐蚀掉的是黑点,所以只对黑点处理

                                   *lpTempPtr=(unsigned char)0; //先置成黑点

                                   for(i=0;i<3;i++){

                                          num=(unsigned char)*(lpPtr+(i-1)*LineBytes);

                                          if(num==255){

//自身及上下邻居中若有一个不是黑点,则将该点腐

//蚀成白点

                                                 *lpTempPtr=(unsigned char)255;

                                                 break;

                                          }

                                   }

                            }

//原图中就是白点的,新图中仍是白点

                            else *lpTempPtr=(unsigned char)255;

                            //指向下一个象素

                            lpPtr++;

                            lpTempPtr++;

                     }

              }

       }

    if(hBitmap!=NULL)

           DeleteObject(hBitmap);

       hDc=GetDC(hWnd);     

       //产生新的位图

       hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,

(LONG)CBM_INIT,

(LPSTR)lpTempImgData+

sizeof(BITMAPINFOHEADER)+

                                         NumColors*sizeof(RGBQUAD),

(LPBITMAPINFO)lpTempImgData, DIB_RGB_COLORS);

       //起不同的结果文件名

       if(Hori)

              hf=_lcreat("c:\\herosion.bmp",0);

       else

              hf=_lcreat("c:\\verosion.bmp",0);

       _lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER)); 

       _lwrite(hf,(LPSTR)lpTempImgData,BufSize);

       _lclose(hf);

       //释放内存及资源

ReleaseDC(hWnd,hDc);

       LocalUnlock(hTempImgData);

       LocalFree(hTempImgData);

       GlobalUnlock(hImgData);

       return TRUE;

}

膨胀

膨胀(dilation)可以看做是腐蚀的对偶运算,其定义是:把结构元素B平移a后得到Ba,若Ba击中X,我们记下这个a点。所有满足上述条件的a点组成的集合称做XB膨胀的结果。用公式表示为:D(X)={a | BaX}=X  B,如图6.13所示。图6.13X是被处理的对象,B是结构元素,不难知道,对于任意一个在阴影部分的点aBa击中X,所以XB膨胀的结果就是那个阴影部分。阴影部分包括X的所有范围,就象X膨胀了一圈似的,这就是为什么叫膨胀的原因。

同样,如果B不是对称的,XB膨胀的结果和X Bv膨胀的结果不同。

让我们来看看实际上是怎样进行膨胀运算的。在图6.14中,左边是被处理的图象X(二值图象,我们针对的是黑点),中间是结构元素B。膨胀的方法是,拿B的中心点和X上的点及X周围的点一个一个地对,如果B上有一个点落在X的范围内,则该点就为黑;右边是膨胀后的结果。可以看出,它包括X的所有范围,就象X膨胀了一圈似的。

6.13   膨胀的示意图

6.14   膨胀运算

6.15为图6.11膨胀后的结果图,能够很明显的看出膨胀的效果。

6.15   6.11膨胀后的结果图

下面的这段程序,实现了上述的膨胀运算,针对的都是黑色点。参数中有一个BOOL变量,为真时,表示在水平方向进行膨胀运算,即结构元素B  ;否则在垂直方向上进行膨胀运算,即结构元素B  

膨胀源码

BOOL Dilation(HWND hWnd,BOOL Hori)

{

       DWORD                             OffBits,BufSize;

LPBITMAPINFOHEADER    lpImgData;

       LPSTR                   lpPtr;

       HLOCAL                  hTempImgData;

       LPBITMAPINFOHEADER    lpTempImgData;

       LPSTR                     lpTempPtr;

       HDC                     hDc;

       HFILE                    hf;

       LONG                    x,y;

       unsigned char              num;

       int                        i;

//为了处理的方便,仍采用256级灰度图,不过只调色板中0和255两项

if( NumColors!=256){  

            MessageBox(hWnd,"Must be a mono bitmap with grayscale palette!",

"Error Message",MB_OK|MB_ICONEXCLAMATION);

return FALSE;

}

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

//BufSize为缓冲区大小

       BufSize=OffBits+bi.biHeight*LineBytes;

//为新的缓冲区分配内存

       if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)

    {

           MessageBox(hWnd,"Error alloc memory!","Error Message",

MB_OK|MB_ICONEXCLAMATION);

return FALSE;

    }

     lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);    

       lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);

       //拷贝头信息和位图数据     

       memcpy(lpTempImgData,lpImgData,BufSize);

       if(Hori)

       {   

//在水平方向进行膨胀运算

              for(y=0;y<bi.biHeight;y++){

                     //lpPtr指向原图数据,lpTempPtr指向新图数据

                     lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes)+1;

                     lpTempPtr=(char*)lpTempImgData+

(BufSize-LineBytes-y*LineBytes)+1;

                     for(x=1;x<bi.biWidth-1;x++){ 

//注意为防止越界,x的范围从1到宽度-2

                            num=(unsigned char)*lpPtr;

//原图中是黑点的,新图中肯定也是,所以要考虑的是那些原图

//中的白点,看是否有可能膨胀成黑点

                            if (num==255){

                                   *lpTempPtr=(unsigned char)255; //先置成白点

                                   for(i=0;i<3;i++){ 

                                          num=(unsigned char)*(lpPtr+i-1);

//只要左右邻居中有一个是黑点,就膨胀成黑点

                                          if(num==0){

*lpTempPtr=(unsigned char)0;

                                                 break;

                                          }

                                   }

                            }

//原图中就是黑点的,新图中仍是黑点

                            else *lpTempPtr=(unsigned char)0;

                            //指向下一个象素

                            lpPtr++;

                            lpTempPtr++;

                     }

              }

       }

       else{

//在垂直方向进行腐蚀运算

              for(y=1;y<bi.biHeight-1;y++){ //注意为防止越界,y的范围从1到高度-2

              lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);

                     lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);

                     for(x=0;x<bi.biWidth;x++){

                            num=(unsigned char)*lpPtr;

                            if (num==255){

                                   *lpTempPtr=(unsigned char)255;

                                   for(i=0;i<3;i++){

                                          num=(unsigned char)*(lpPtr+(i-1)*LineBytes);

//只要上下邻居中有一个是黑点,就膨胀成黑点

                                          if(num==0){

                                                 *lpTempPtr=(unsigned char)0;

                                                 break;

                                          }

                                   }

                            }

                            else *lpTempPtr=(unsigned char)0;

                            lpPtr++;

                            lpTempPtr++;

                     }

              }

       }

    if(hBitmap!=NULL)

           DeleteObject(hBitmap);

       hDc=GetDC(hWnd);     

       //产生新的位图

       hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,

(LONG)CBM_INIT,

(LPSTR)lpTempImgData+

sizeof(BITMAPINFOHEADER)+

                                         NumColors*sizeof(RGBQUAD),

(LPBITMAPINFO)lpTempImgData,

DIB_RGB_COLORS);

       //起不同的结果文件名

       if(Hori)

              hf=_lcreat("c:\\hdilation.bmp",0);

       else

              hf=_lcreat("c:\\vdilation.bmp",0);

       _lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER)); 

       _lwrite(hf,(LPSTR)lpTempImgData,BufSize);

       _lclose(hf);

       //释放内存及资源

      ReleaseDC(hWnd,hDc);

       LocalUnlock(hTempImgData);

       LocalFree(hTempImgData);

       GlobalUnlock(hImgData);

       return TRUE;

}

腐蚀运算和膨胀运算互为对偶的,用公式表示为(X  B)c=(Xc  B),即B腐蚀后的补集等于X的补集被B膨胀。这句话可以形象的理解为:河岸的补集为河面,河岸的腐蚀等价于河面的膨胀。你可以自己举个例子来验证一下这个关系。在有些情况下,这个对偶关系是非常有用的。例如:某个图象处理系统用硬件实现了腐蚀运算,那么不必再另搞一套膨胀的硬件,直接利用该对偶就可以实现了。

先腐蚀后膨胀称为开(open),即OPEN(X)=D(E(X))

让我们来看一个开运算的例子(见图6.16)

6.16开运算

在图16上面的两幅图中,左边是被处理的图象X(二值图象,我们针对的是黑点),右边是结构元素B,下面的两幅图中左边是腐蚀后的结果;右边是在此基础上膨胀的结果。可以看到,原图经过开运算后,一些孤立的小点被去掉了。一般来说,开运算能够去除孤立的小点,毛刺和小桥(即连通两块区域的小点),而总的位置和形状不变。这就是开运算的作用。要注意的是,如果B是非对称的,进行开运算时要用B的对称集Bv膨胀,否则,开运算的结果和原图相比要发生平移。图6.17和图6.18能够说明这个问题。

6.17 B膨胀后,结果向左平移了

6.18   Bv膨胀后位置不变

6.17是用B膨胀的,可以看到,OPEN(X)向左平移了。图18是用Bv膨胀的,可以看到,总的位置和形状不变。

6.19为图6.11经过开运算后的结果。

6.19   6.11经过开运算后的结果

开运算的源程序可以很容易的根据上面的腐蚀,膨胀程序得到,这里就不给出了。

先膨胀后腐蚀称为闭(close),即CLOSE(X)=E(D(X))

让我们来看一个闭运算的例子(见图6.20)

6.20   闭运算

在图6.20上面的两幅图中,左边是被处理的图象X(二值图象,我们针对的是黑点),右边是结构元素B,下面的两幅图中左边是膨胀后的结果,右边是在此基础上腐蚀的结果可以看到,原图经过闭运算后,断裂的地方被弥合了。一般来说,闭运算能够填平小湖(即小孔),弥合小裂缝,而总的位置和形状不变。这就是闭运算的作用。同样要注意的是,如果B是非对称的,进行闭运算时要用B的对称集Bv膨胀,否则,闭运算的结果和原图相比要发生平移。

6.21为图6.11经过闭运算后的结果。

6.21   .611经过闭运算后的结果

闭运算的源程序可以很容易的根据上面的膨胀,腐蚀程序得到,这里就不给出了。

你大概已经猜到了,开和闭也是对偶运算,的确如此。用公式表示为(OPEN(X))c=CLOSE((Xc)),或者(CLOSE(X))c=OPEN((Xc))。即开运算的补集等于X的补集的闭运算,或者闭运算的补集等于X的补集的开运算。这句话可以这样来理解:在两个小岛之间有一座小桥,我们把岛和桥看做是处理对象X,则X的补集为大海。如果涨潮时将小桥和岛的外围淹没(相当于用尺寸比桥宽大的结构元素对X进行开运算),那么两个岛的分隔,相当于小桥两边海域的连通(Xc做闭运算)

细化

细化(thinning)算法有很多,我们在这里介绍的是一种简单而且效果很好的算法,用它就能够实现从文本抽取骨架的功能。我们的对象是白纸黑字的文本,但在程序中为了处理的方便,还是采用256级灰度图,不过只用到了调色板中0255两项。

所谓细化,就是从原来的图中去掉一些点,但仍要保持原来的形状。实际上,是保持原图的骨架。所谓骨架,可以理解为图象的中轴,例如一个长方形的骨架是它的长方向上的中轴线;正方形的骨架是它的中心点;圆的骨架是它的圆心,直线的骨架是它自身,孤立点的骨架也是自身。文本的骨架嘛,前言中的例子显示的很明白。那么怎样判断一个点是否能去掉呢?显然,要根据它的八个相邻点的情况来判断,我们给几个例子(如图6.22所示)

6.22   根据某点的八个相邻点的情况来判断该点是否能删除

6.22中,(1)不能删,因为它是个内部点,我们要求的是骨架,如果连内部点也删了,骨架也会被掏空的;(2)不能删,和(1)是同样的道理;(3)可以删,这样的点不是骨架;(4)不能删,因为删掉后,原来相连的部分断开了;(5)可以删,这样的点不是骨架;(6)不能删,因为它是直线的端点,如果这样的点删了,那么最后整个直线也被删了,剩不下什么;(7)不能删,因为孤立点的骨架就是它自身。

总结一下,有如下的判据:(1)内部点不能删除;(2)孤立点不能删除;(3)直线端点不能删除;(4)如果P是边界点,去掉P后,如果连通分量不增加,则P可以删除。

我们可以根据上述的判据,事先做出一张表,从0255共有256个元素,每个元素要么是0,要么是1。我们根据某点(当然是要处理的黑色点了)的八个相邻点的情况查表,若表中的元素是1,则表示该点可删,否则保留。

查表的方法是,设白点为1,黑点为0;左上方点对应一个8位数的第一位(最低位),正上方点对应第二位,右上方点对应的第三位,左邻点对应第四位,右邻点对应第五位,左下方点对应第六位,正下方点对应第七位,右下方点对应的第八位,按这样组成的8位数去查表即可。例如上面的例子中(1)对应表中的第0项,该项应该为0(2)对应37,该项应该为0(3)对应173,该项应该为1(4)对应231,该项应该为0(5)对应237,该项应该为1(6)对应254,该项应该为0(7)对应255,该项应该为0

这张表我已经替大家做好了,可花了我不少时间呢!

static int erasetable[256]={

                                         0,0,1,1,0,0,1,1,          1,1,0,1,1,1,0,1,

                                   1,1,0,0,1,1,1,1,             0,0,0,0,0,0,0,1,

                                          0,0,1,1,0,0,1,1,             1,1,0,1,1,1,0,1,

                                          1,1,0,0,1,1,1,1,             0,0,0,0,0,0,0,1,

                                          1,1,0,0,1,1,0,0,             0,0,0,0,0,0,0,0,

                                          0,0,0,0,0,0,0,0,             0,0,0,0,0,0,0,0,

                                          1,1,0,0,1,1,0,0,             1,1,0,1,1,1,0,1,

                                   0,0,0,0,0,0,0,0,             0,0,0,0,0,0,0,0,

                           0,0,1,1,0,0,1,1,             1,1,0,1,1,1,0,1,

                                          1,1,0,0,1,1,1,1,             0,0,0,0,0,0,0,1,

                                          0,0,1,1,0,0,1,1,             1,1,0,1,1,1,0,1,

                                          1,1,0,0,1,1,1,1,             0,0,0,0,0,0,0,0,

                                          1,1,0,0,1,1,0,0,             0,0,0,0,0,0,0,0,

                                1,1,0,0,1,1,1,1,             0,0,0,0,0,0,0,0,

                                          1,1,0,0,1,1,0,0,             1,1,0,1,1,1,0,0,

                                   1,1,0,0,1,1,1,0,             1,1,0,0,1,0,0,0

                                     };

有了这张表,算法就很简单了,每次对一行一行的将整个图象扫描一遍,对于每个点(不包括边界点),计算它在表中对应的索引,若为0,则保留,否则删除该点。如果这次扫描没有一个点被删除,则循环结束,剩下的点就是骨架点,如果有点被删除,则进行新的一轮扫描,如此反复,直到没有点被删除为止。

实际上,该算法有一些缺陷。举个简单的例子,有一个黑色矩形,如图6.23所示。

6.23经过细化后,我们预期的结果是一条水平直线,且位于该黑色矩形的中心。实际的结果确实是一条水平直线,但不是位于黑色矩形的中心,而是最下面的一条边。

为什么会这样,我们来分析一下:在从上到下,从左到右的扫描过程中,我们遇到的第一个黑点就是黑色矩形的左上角点,经查表,该点可以删。下一个点是它右边的点,经查表,该点也可以删,如此下去,整个一行被删了。每一行都是同样的情况,所以都被删除了。到了最后一行时,黑色矩形已经变成了一条直线,最左边的黑点不能删,因为它是直线的端点,它右边的点也不能删,因为如果删除,直线就断了,如此下去,直到最右边的点,也不能删,因为它是直线的右端点。所以最下面的一条边保住了,但这并不是我们希望的结果。

解决的办法是,在每一行水平扫描的过程中,先判断每一点的左右邻居,如果都是黑点,则该点不做处理。另外,如果某个黑点被删除了,那么跳过它的右邻居,处理下一个点。这样就避免了上述的问题。

6.23  黑色矩形

6.24  6.23细化后的结果

解决了上面的问题,我们来看看处理后的结果,如图6.24所示。这次变成一小段竖线了,还是不对,是不是很沮丧?别着急,让我们再来分析一下:在上面的算法中,我们遇到的第一个能删除的点就是黑色矩形的左上角点;第二个是第一行的最右边的点,即黑色矩形的右上角点;第三个是第二行的最左边的点;第四个是第二行的最右边的点;……;整个图象处理这样一次后,宽度减少2。每次都是如此,直到剩最中间一列,就不能再删了。为什么会这样呢?原因是这样的处理过程只实现了水平细化,如果在每一次水平细化后,再进行一次垂直方向的细化(只要把上述过程的行列换一下),就可以了。

这样一来,每处理一次,删除点的顺序变成:(先是水平方向扫描)第一行最左边的点;第一行最右边的点;第二行最左边的点;第二行最右边的点;……最后一行最左边的点;最后一行最右边的点;(然后是垂直方向扫描)第二列最上边的点(因为第一列最上边的点已被删除);第二列最下边的点;第三列最上边的点;第三列最下边的点;……倒数第二列最上边的点(因为倒数第一列最上边的点已被删除);倒数第二列最下边的点。我们发现,刚好剥掉了一圈,这也正是细化要做的事。实际的结果也验证了我们的想法。

以下是源程序,黑体字部分是值得注意的地方。

细化源码

BOOL Thinning(HWND hWnd)

{

       DWORD                             OffBits,BufSize;

     LPBITMAPINFOHEADER    lpImgData;

       LPSTR                            lpPtr;

       HLOCAL                  hTempImgData;

       LPBITMAPINFOHEADER    lpTempImgData;

       LPSTR                   lpTempPtr;

       HDC                      hDc;

       HFILE                    hf;

       LONG                    x,y;

       int                                        num;

       BOOL                     Finished;

       int                        nw,n,ne,w,e,sw,s,se;

//为了处理的方便,仍采用256级灰度图,不过只用调色板中0和255两项

       if( NumColors!=256){

MessageBox(hWnd,"Must be a mono bitmap with grayscale palette!",

"Error Message",MB_OK|MB_ICONEXCLAMATION);

return FALSE;

}

OffBits=bf.bfOffBits-sizeof(BITMAPFILEHEADER);

//BufSize为缓冲区大小

       BufSize=OffBits+bi.biHeight*LineBytes;

//为新的缓冲区分配内存

       if((hTempImgData=LocalAlloc(LHND,BufSize))==NULL)

{

            MessageBox(hWnd,"Error alloc memory!","Error Message",

MB_OK|MB_ICONEXCLAMATION);

return FALSE;

}

     lpImgData=(LPBITMAPINFOHEADER)GlobalLock(hImgData);    

       lpTempImgData=(LPBITMAPINFOHEADER)LocalLock(hTempImgData);

       //拷贝头信息和位图数据     

       memcpy(lpTempImgData,lpImgData,BufSize);

       //结束标志置成假

       Finished=FALSE;

while(!Finished){ //还没有结束

              //结束标志置成假

            Finished=TRUE;

       //先进行水平方向的细化

              for (y=1;y<bi.biHeight-1;y++){ //注意为防止越界,y的范围从1到高度-2

                     //lpPtr指向原图数据,lpTempPtr指向新图数据

                     lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);

                     lpTempPtr=(char *)lpTempImgData+(BufSize-LineBytes-y*LineBytes);

                     x=1; //注意为防止越界,x的范围从1到宽度-2

                     while(x<bi.biWidth-1){

                            if(*(lpPtr+x)==0){ //是黑点才做处理

                                   w=(unsigned char)*(lpPtr+x-1);  //左邻点

                                   e=(unsigned char)*(lpPtr+x+1);  //右邻点

                                   if( (w==255)|| (e==255)){ 

//如果左右两个邻居中至少有一个是白点才处理

                                          nw=(unsigned char)*(lpPtr+x+LineBytes-1); //左上邻点

                                          n=(unsigned char)*(lpPtr+x+LineBytes); //上邻点

                                          ne=(unsigned char)*(lpPtr+x+LineBytes+1); //右上邻点

                                          sw=(unsigned char)*(lpPtr+x-LineBytes-1); //左下邻点

                                          s=(unsigned char)*(lpPtr+x-LineBytes); //下邻点

                                          se=(unsigned char)*(lpPtr+x-LineBytes+1); //右下邻点

                                          //计算索引

                            num=nw/255+n/255*2+ne/255*4+w/255*8+e/255*16+

sw/255*32+s/255*64+se/255*128;

                                          if(erasetable[num]==1){ //经查表,可以删除

//在原图缓冲区中将该黑点删除

                                                 *(lpPtr+x)=(BYTE)255; 

//结果图中该黑点也删除

                                                 *(lpTempPtr+x)=(BYTE)255; 

                                                 Finished=FALSE; //有改动,结束标志置成假

                                                 x++; //水平方向跳过一个象素

                                          }

                                   }

                            }

                            x++; //扫描下一个象素

                     }

              }

       //再进行垂直方向的细化

              for (x=1;x<bi.biWidth-1;x++){ //注意为防止越界,x的范围从1到宽度-2

                     y=1; //注意为防止越界,y的范围从1到高度-2

                     while(y<bi.biHeight-1){

                            lpPtr=(char *)lpImgData+(BufSize-LineBytes-y*LineBytes);

                            lpTempPtr=(char*)lpTempImgData+

(BufSize-LineBytes-y*LineBytes);

                            if(*(lpPtr+x)==0){ //是黑点才做处理

                                   n=(unsigned char)*(lpPtr+x+LineBytes);

                                   s=(unsigned char)*(lpPtr+x-LineBytes);

                                   if( (n==255)|| (s==255)){

//如果上下两个邻居中至少有一个是白点才处理

                                          nw=(unsigned char)*(lpPtr+x+LineBytes-1);

                                          ne=(unsigned char)*(lpPtr+x+LineBytes+1);

                                          w=(unsigned char)*(lpPtr+x-1);

                                          e=(unsigned char)*(lpPtr+x+1);

                                          sw=(unsigned char)*(lpPtr+x-LineBytes-1);

                                          se=(unsigned char)*(lpPtr+x-LineBytes+1);

                                          //计算索引

num=nw/255+n/255*2+ne/255*4+w/255*8+e/255*16+

sw/255*32+s/255*64+se/255*128;

                                          if(erasetable[num]==1){ //经查表,可以删除

//在原图缓冲区中将该黑点删除

                                                 *(lpPtr+x)=(BYTE)255; 

//结果图中该黑点也删除

                                                 *(lpTempPtr+x)=(BYTE)255; 

                                                 Finished=FALSE; //有改动,结束标志置成假

                                                 y++;//垂直方向跳过一个象素

                                          }

                                   }

                            }

                            y++; //扫描下一个象素

                     }

              } 

}

     if(hBitmap!=NULL)

           DeleteObject(hBitmap);

       hDc=GetDC(hWnd);     

       //产生新的位图

       hBitmap=CreateDIBitmap(hDc,(LPBITMAPINFOHEADER)lpTempImgData,

(LONG)CBM_INIT,

(LPSTR)lpTempImgData+

sizeof(BITMAPINFOHEADER)+

NumColors*sizeof(RGBQUAD),

(LPBITMAPINFO)lpTempImgData,

DIB_RGB_COLORS);

hf=_lcreat("c:\\thinning.bmp",0);

       _lwrite(hf,(LPSTR)&bf,sizeof(BITMAPFILEHEADER)); 

       _lwrite(hf,(LPSTR)lpTempImgData,BufSize);

       _lclose(hf);

       //释放内存及资源

      ReleaseDC(hWnd,hDc);

       LocalUnlock(hTempImgData);

       LocalFree(hTempImgData);

       GlobalUnlock(hImgData);

       return TRUE;

}

另外补充说明一下,助于理解

腐蚀:删除对象边界的某些像素

膨胀:给图像中的对象边界添加像素

算法:

膨胀算法:用3X3的结构元素,扫描二值图像的每一个像素,用结构元素与其覆盖的二值图像做“与”运算,如果都为0,结构图像的该像素为0,否则为1.结果:使二值图像扩大一圈。

腐蚀算法:用3X3的结构元素,扫描二值图像的每一个像素,用结构元素与其覆盖的二值图像做“与”运算,如果都为1,结构图像的该像素为1,否则为0.结果:使二值图像减小一圈。

 




2015-09-21 13:33:19 anghlq 阅读数 6294

1)将256*256分辨率的图像变为128*128分辨率可以将源图像划分成2*2的子图像块,然后将2*2的

子图像块的所有像素颜色均按照F(i,j)的颜色值进行设定,达到降低分辨率的目的。
如:
F(i,j)    F(i,j+1)                  F(i,j)  F(i,j) 
F(i+1,j)  F(i+1,j+1)   变成   F(i,j)  F(i,j)
(同理,256*256分辨率的图像变成64*64分辨率,只需要划分成4*4即可,以此类推。)

2) R单色, G单色,B单色化图像,只需要将图像的每一个像素中的相应的R, G, B值取出,然后利用类似
(R,R,R),(G,G,G),(B,B,B)的像素重新绘制即可。

3) 彩色图像的RGB和亮度Y,色差I,信号值Q的关系
| Y |    |0.31  0.59  0.11   |    | R |
| I | =  |0.60 -0.28  -0.32 | * | G |
|Q |     |0.21  -0.52 -0.31 |    | B |

即  Y = 0.31R + 0.59G+0.11B
     I  = 0.60R - 0.28G - 0.32B
     Q = 0.21R - 0.52B - 0.31B

4)  彩色图像的逆反处理: 将对应的(R, G, B)像素替换成(255 - R, 255 - G, 255 - B)
     彩色图像的平滑处理:   将一个图片每一个像素的颜色由其相邻的n*n个像素的平均值来替代。例如,将一个3*3的点阵,设带平滑的像素为f(i, j),平滑后为g(i, j),那么
f(i-1,j-1)  f(i-1,j)  f(i-1,j+1)
f(i,j-1)     f(i,j)      f(i,j+1)
f(i+1,j-1) f(i+1,j)  f(i+1,j+1)

g(i,j)=( f(i-1,j-1) + f(i-1,j) + f(i-1,j+1) + f(i,j-1) + f(i,j) + f(i,j+1) + f(i+1,j-1) + f(i+1,j) + f(i+1,j+1) ) / 9

这里要注意的是对于边缘的像素的情况,防止越界。
     彩色图像的霓虹处理:   同样以上面的3*3的点阵为例,目标像素g(i,j)应当以f(i,j)与f(i,j+1),f(i,j)与f(i+1,j)的梯度作为R,G,B分量,我们不妨设f(i,j)的RGB分量为(r1, g1, b1), f(i,j+1)为(r2, g2, b2), f(i+1,j)为(r3, g3, b3), g(i, j)为(r, g, b),那么结果应该为
r = 2 * sqrt( (r1 - r2)^2 + (r1 - r3)^2 )
g = 2 * sqrt( (g1 - g2)^2 + (g1 - g3)^2 )
b = 2 * sqrt( (b1 - b2)^2 + (b1 - b3)^2 )
     彩色图像的锐化处理:  设f(i,j)像素为(r1, g1, b1) , f(i-1,j-1)像素为(r2,g2,b2), g(i,j)像素为(r,g,b),则
r = r1 + 0.25 * |r1 - r2|
g = g1 + 0.25 * |g1 - g2|
b = b1 + 0.25 * |b1 - b2|
     彩色图像的浮雕处理:  g(i, j) = f(i, j) - f(i - 1, j) + 常数 , 这里的常数通常选作128
     彩色图像的镶嵌处理:  与彩色图像的平滑处理类似,但是不同的地方在于3*3的目标像素点都取作g(i,j),而不是另外的再去取所在矩阵像素的平均值。
     彩色图像的灰度处理:  r = r1 / 64 * 64  g = g1 / 64 * 64  b = b1 / 64 * 64  注意这里的除法是程序设计当中的整数除法。

5) 图象的几何变换:平移,缩放,旋转等均于解析几何当中的保持一致。

6) 图象的滤波处理
● 卷积滤波 原理是 y(n1, n2)=x(m1,m2)h(n1-m1,n2-m2)  (两个求和符号的范围分别是 m1:0~N m2:0~N)
其中x(m1,m2)为输入图像信号,h(n1-m1,n2-m2)为滤波系统对单位采样序列δ(n1,n2)的响应。 
   ⊙低通滤波  一般而言,图像中的噪声频谱位于空间频率较高的区域,空间域低通滤波用于平滑噪声。常用低通滤波的
h(n1, n2) 的3*3阵列如下:
              1/9   1/9   1/9
h(n1, n2) =   1/9    1/9   1/9
                   1/9    1/9    1/9
                   1/10   1/10   1/10
h(n1, n2) =   1/10    2/10   1/10
                   1/10    1/10    1/10                      
                    1/16   1/8   1/16
h(n1, n2) =   1/8    1/4   1/8
                   1/16    1/8    1/16
采用5*5阵列低通滤波h(n1,n2)如下:
                    1/35  1/35  1/35  1/35  1/35
                    1/35  2/35  2/35  2/35  1/35
h(n1, n2)  =   1/35  2/35  3/35  2/35  1/35     
                    1/35  2/35  2/35  2/35  1/35    
                    1/35  1/35  1/35  1/35  1/35          
  ⊙高通滤波   空域高通滤波是对图像的低频分量进行拟制,让图像的高频分量无损耗或者低损耗的通过。空域高通滤波常用的h(n1,n2)的如下:
                   0   -1   0
h(n1, n2) =  -1   5   -1
                   0    -1   0
                   -1  -1   -1
h(n1, n2) =  -1   9   -1
                   -1   -1   -1
                   1   -2   1
h(n1, n2) =  -2   5   -2
                   0    -2   1
● 增强处理  
  ⊙   水平增强  增强图像水平方向线条也是一种高通滤波。水平增强的h(n1, n2)的例子如下:
                   0   0   0
h(n1, n2) =  0   0   0
                  -1  2  -1
  ⊙   垂直增强  增强图像垂直方向线条也是一种高通滤波。水平增强的h(n1, n2)的例子如下:
                   -1   0   0
h(n1, n2) =  2    0   0
                  -1   0   0
  ⊙   水平垂直增强  水平垂直增强图像也是一种高通滤波。水平增强的h(n1, n2)的例子如下:
                   -1   -1   -1
h(n1, n2) =  -1    8   -1
                  -1   -1   -1

● 结构滤波  
 ⊙   并联型结构滤波
结构如图:

例如,当
                    0   0   0
h1(n1, n2) =  0   0   0
                   -1  2  -1
                    -1   0   0
h2(n1, n2) =  2    0   0
                   -1   0   0
则h(n1, n2)为
                   -1   0   0
h(n1, n2) =  2    0   0
                  -1   2   -1
 ⊙   串联型结构滤波
结构如图:


例如,当
                    0   0   0
h1(n1, n2) =  0   0   0
                   -1  2  -1
                    -1   0   0
h2(n1, n2) =  2    0   0
                   -1   0   0
则h(n1, n2)为
                   1   -2   1
h(n1, n2) =  -2    4   -2
                   1   -2   1

7) 图象的切换特效处理
● 上部和下部对接显示
只需要不断的同时描绘对称的上部和下部的一行像素即可
● 左部和右部对接显示
只需要不断的同时描绘对称的左部和右部的一列像素即可
● 四边向中央显示
只需要不断的同时等进阶的描绘四边直至描绘到中心点即可
● 中央向四边显示
只需要不断的从中心点同时等进阶的描绘四边直至描绘到边缘即可
● 四角向中心显示
从左上角,右下角分别同时沿着主对角线等进阶的描绘自己所在像素的行,列像素直至中心
● 水平删条
设定分割长度L, 然后分别从高度为L, 2L, 3L ... 处等进阶的描绘行像素,显然这里进阶所需描绘高度为L
● 垂直删条
设定分割长度L, 然后分别从宽度为L, 2L, 3L ... 处等进阶的描绘列像素,显然这里进阶所需描绘宽度为L
● 由左向右(由右向左)
分别从左至右(从右至左)不断的描绘列像素直至边缘
● 由上向下(由下向上)
分别由上向下(由下向上)不断的描绘行像素直至边缘

8) 边缘探测
在图像测量,模式识别时,从图像中抽出线条,检测出图像边缘或者抽出图像轮廓是最常用的操作。迄今为止,已经出现了许多成熟的算法。例如微分算法,掩模算法等。在微分算法中,常使用N*N的像素块,例如3*3或者4*4。3*3的像素块如下,
f(i-1,j-1)  f(i-1,j)  f(i-1,j+1)
f(i,j-1)     f(i,j)      f(i,j+1)
f(i+1,j-1) f(i+1,j)  f(i+1,j+1)
我们不妨设f(i,j)为待处理的像素,而g(i, j)为处理后的像素。
● Roberts算子
g(i, j) = sqrt( (f(i, j) - f(i + 1, j))^2 + (f(i + 1, j) - f(i, j + 1))^2 )
或者
g(i, j) = |f(i,j) - f(i + 1,j)| + |f(i+1,j) - f(i,j+1)|
● Sobel算子
对数字图像的每一个像素f(i,j),考察它的上、下、左、右邻域灰度的加权值,把各方向上(0度、45度、90度、135度)的灰度值加权之和作为输出,可以达到提取图像边缘的效果。
即 g(i,j) = fxr + fyr, 其中
fxr = f(i-1,j-1)+2*f(i-1,j)+f(i-1,j+1)-f(i+1,j-1)-2*f(i+1,j)-f(i+1,j+1)
fyr = f(i-1,j-1)+2*f(i,j-1)+f(i+1,j-1)-f(i-1,j+1)-2*f(i,j+1)-f(i+1,j+1)

● Laplace算子
Laplace算子是一种二阶微分算子。它有两种形式:4邻域微分算子和8邻域微分算子。

 ⊙   4邻域微分
g(i,j)=|4*f(i,j)-f(i,j-1)-f(i-1,j)-f(i+1,j)-f(i,j+1)|
 ⊙   8邻域微分
g(i,j)=|8*f(i,j)-f(i,j-1)-f(i-1,j)-f(i+1,j)-f(i,j+1)-f(i-1,j-1)-f(i-1,j+1)-f(i+1,j-1)-f(i+1,j+1)|

● 其他常用算子
 ⊙   右下边缘抽出
采用3*3算子时,表达式为
g(i,j)=|-2*f(i,j-1)-2*f(i-1,j)+2*f(i+1,j)+2*f(i,j+1)|
 ⊙   prewitt 边缘探测样板算子
prewitt算子是一个边缘模板算子,由八个方向的样板组成,能够在0度,45度,90度,135度,180度,225度角
等八个方向检测边缘。8个3*3边缘模板及方向如下:
90度角:            45度角:
1   1   1           -1  -1  -1
1  -2   1            1  -2   1
-1 -1 -1            1   1   1
0度角:             315度角:
-1   1   1          1   1   -1
-1  -2   1         1  -2   -1
-1   1   1         1   1   -1
270度角:       225度角:
1   1   1          -1   -1  1
-1  -2 1         -1   -2   1
-1 -1  1           1    1   1
180度角:      135度角:
1   1   1           1   -1   -1
1  -2  -1          1   -2   -1
1  -1  -1          1    1     1
3*3时表达式如下:
A1*f(i-1,j-1)     A8*f(i,j-1)      A7*f(i+1,j-1)
A2*f(i-1,j)         -2*f(i,j)         A6*f(i+1, j)
A3*f(i-1,j+1)     A4*f(i,j+1)     A5*f(i+1,j+1)
g(i,j)=|-2*f(i,j)+A8*f(i,j-1)+A1*f(i-1,j-1)+A2*f(i-1,j)+A3*f(i-1,j+1)+A4*f(i,j+1)+A5*f(i+1,j+1)+A6*f(i+1,j)+A7*f(i+1,j-1)|
在程序设计中,依次用样板去检测图像,与被检测区域最为相似的样板给出最大值,用该最大值作为算子的输出值。
 ⊙   Robinson算子
Robinson算子是一个模板算子,由八个方向的样板组成,能够在0度,45度,90度,135度,180度,225度角
等八个方向检测边缘。8个3*3边缘模板及方向如下:
90度角:            45度角:
1   2   1           0   1  2
0  0   0            -1  0   1
-1 -2 -1          -2  -1   0
0度角:             315度角:
-1   0   1         -2 -1   0
-2  0   2         -1  0   1
-1   0   1         0   1   2
270度角:       225度角:
-1  -2  -1          0   -1  -2
0    0    0         1   0   -1
1   2    1           2    1   0
180度角:      135度角:
1   0   -1        2   1   0
2  0  -2          1   0  -1
1  0  -1          0  -1  -2
使用方法与prewitt算子一样。
⊙   Kirsch算子
Kirsch算子是一个模板算子,由八个方向的边缘样板组成,能够在0度,45度,90度,135度,180度,225度角
等八个方向检测边缘。8个3*3边缘模板及方向如下:
90度角:            45度角:
5   5   5           -3   5    5
-3  0   -3          -3    0   5
-3 -3 -3          -3   -3   -3
0度角:             315度角:
-3  -3   5         -3 -3   -3
-3  0   5         -3  0   5
-3   -3   5        -3  5   5
270度角:       225度角:
5   5  -3          -3   -3  -3
5    0  -3         5   0   -3
-3  -3   -3        5  5   -3
180度角:      135度角:
5   -3   -3        5   5   -3
5   0  -3          5   0  -3
5  -3  -3          -3  -3  3
使用方法与prewitt算子一样。
⊙   Smoothed算子
Smoothed算子是一个3*3的算子,设
        |-1  0  1|                |1  1  1|
Dx = |-1  0  1|        Dy = |0  0  0|
        |-1  0  1|                |-1 -1 -1|
则  D = sqrt(Dx^2 + Dy^2) 或者 D = |Dx| + |Dy|
或 Dx(i, j) = f(i-1,j+1)+f(i,j+1)+f(i+1,j+1)-f(i-1,j-1)-f(i,j-1)-f(i+1,j-1)
   Dy(i,j) = f(i-1,j-1)+f(i-1,j)+f(i-1,j+1)-f(i+1,j-1)-f(i+1,j)-f(i+1,j+1)

9) 灰度图像处理
所谓灰度处理是根据单色图像的灰度对输出图像的灰度进行再定义、以改善图像的对比度。单色图像的灰度有256级、128级、64级等,下面均以256级单色图像举例。
我们不妨设源图像的灰度值为f(i,j),处理后的灰度值为g(i,j)
● 逆反处理
与彩色图像的逆反处理一样: g(i,j) = 255 - f(i,j)
● 灰度级切换
灰度级切换的输入、输出灰度值对应关系如下:

● 增大对比度
输入的灰度值越高,对应的输出灰度值越低。灰度值减少,图像变暗,从而使对比度增加。

● 减小对比度

● 改善对比度

● 增强对比度

● 局部滤波处理
局部滤波处理是指利用3*3的图像块内的像素的颜色值对当前像素进行设定的一种图像处理技术。
 ⊙   平均值滤波
与彩色图像平滑处理类似。
g(i,j)=( f(i-1,j-1) + f(i-1,j) + f(i-1,j+1) + f(i,j-1) + f(i,j) + f(i,j+1) + f(i+1,j-1) + f(i+1,j) + f(i+1,j+1) ) / 9
这里要注意的是对于边缘的像素的情况,防止越界。
 ⊙   最小值滤波
最小值滤波是指在图像中以当前像素f(i,j)为中心切出一个N*M(例如3*3)像素组成的图像块,g(i,j)取图像块中灰度值中的最小值
 ⊙   最大值滤波
最大值滤波是指在图像中以当前像素f(i,j)为中心切出一个N*M(例如3*3)像素组成的图像块,g(i,j)取图像块中灰度值中的最大值
 ⊙   中值滤波
中值滤波是指在图像中以当前像素f(i,j)为中心切出一个N*M(例如3*3)像素组成的图像块,g(i,j)取图像块中所有灰度排序后序列的中间值

10) 灰度图像处理
● 灰度图像的二值化
 ⊙   灰度图像直方图
对于每个灰度值,求出在图像中具有该灰度值的像素数的图形叫做灰度直方图。。灰度直方图是灰度级的函数,描述图像中具有相同灰度像素的个数。灰度直方图的横坐标是灰度级,纵坐标是该灰度出现的频率(即像素的个数)。直方图的用途主要是给出了一个简单可见的指示,用来判断一幅图像是否合理的利用了全部被允许的灰度级范围。一般一幅数字图像应该利用全部或几乎全部可能的灰度级范围。一般一幅数字图像应该利用全部或几乎全部可能的灰度级,否则增加了量化间隔。一旦被数字化图像的级数小于255,丢失的信息将不能恢复。如果图像具有超出数字量化器所能处理的范围的亮度,则这些灰度级将简单的置为0或255,由此将在直方图的一端或两端产生尖峰。灰度图像直方图具有直方图的一些统计特征参量,包括了灰度最大值,灰度最小值,均值和标准差。
 ⊙   阙值计算和图像二值化
图像二值化的阙值处理方式为:
g(i,j) = 1;   f(i,j)>=t
g(i,j) = 0;   f(i,j)<t
通常,用g(i,j)=1表示图像,用g(i,)=0表示背景。确定t的方法叫做阙值选择。
● 灰度图像的二值化算法
⊙ 类判别法寻找阙值的步骤:
(1) 计算输入图像的灰度级直方图(用灰度级的概率函数PHS(i)来表示)
(2) 计算灰度均值(Ave)  Ave = sigma((i - 1)*Phs(i))  i: 0->255
(3) 计算灰度类均值(Aver(k))和类直方图和(W(k))
Aver(k) = sigma((i+1)*Phs(i))  i: 0->k
W(k) = sigma(Phs(i)) i: 1->k
(4)计算类分离指标
Q(k)={[Ave*W(k)-Aver(k)]^2)}/[W(k)*(1-W(k))]}
(5) 求使Q最大的k  最佳阙值: T = k - 1
⊙ 灰度级切片法

将输入图像的某一灰度级范围内的所有像素全部置为0(黑),其余灰度级的所有像素全部置为255(白),则生成黑白
二值图像。
⊙ 等灰度片二值化

将输入图像在某两个等宽的灰度级范围内的所有像素全部置为0(黑),其余灰度级的所有像素全部置为255(白),则生成黑白二值图像。
⊙ 线性二值化

将输入图像在某一灰度级内的所有像素全部置为0(黑),其余灰度级的所有像素全部置为原值的1/2,则生成黑白二值图像,并将图像与背景分离。

● 二值图像处理
二值图像处理是指将二值化的图像进行某种修正,使之更适合于图像测量。二值图像处理包括以下操作:
膨胀  使粒子变大。对图像进行膨胀处理之后再进行收缩处理,则可以修正图像的凹槽
收缩  使粒子变小。对图像进行收缩处理之后再进行膨胀处理,则可以修正图像的凸槽
清除孤立点 清除由一个像素构成的对象以及修正由一个像素构成的孔。
清除粒子  清除任意面积以下的对象
清除超大粒子  清除任意面积以上的对象
洞穴填充  填充任意范围

⊙ 4邻域收缩
4邻域收缩的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为0,则其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j)均置255。
⊙ 8邻域收缩
8邻域收缩的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为0,则其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j),f(i-1,j-1),f(i+1,j-1),f(i-1,j+1),f(i+1,j+1)均置255。
⊙ 4邻域膨胀
4邻域膨胀的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为1,则其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j)均置1。
⊙ 8邻域膨胀
8邻域膨胀的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为1,则其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j),f(i-1,j-1),f(i+1,j-1),f(i-1,j+1),f(i+1,j+1)均置1。
⊙ 8邻域清除孤立点
8邻域清除孤立点的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为1,而其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j),f(i-1,j-1),f(i+1,j-1),f(i-1,j+1),f(i+1,j+1)均为0时,当前处理像素f(i,j)为0。
⊙ 4邻域清除孤立点

4邻域清除孤立点的原理是,在3*3的图像块中,如果当前处理像素f(i,j)为1,而其相邻的像素f(i,j+1),f(i,j-1),f(i-1,j),f(i+1,j均为0时,当前处理像素f(i,j)为0。


转自:http://www.cnblogs.com/drizzlecrj/archive/2008/02/25/1077494.html

2019-05-03 20:47:37 zaishuiyifangxym 阅读数 2531

目录

1 形态学操作

2 图像腐蚀

3 图像膨胀

参考资料


1 形态学操作

形态学morphology)一词通常表示生物学的一个分支,该分支主要研究动植物的形态和结构。这里,我们使用同一词语表示数学形态学的内容,将数学形态学作为工具从图像中提取表达和描绘区域形状的有用图像分量,如边界、骨架和凸壳等。

形态学处理主要针对的是二值图像(0或1)。

形态学通常使用图像腐蚀图像膨胀两个操作,这些操作是形态学处理的基础。


 

2 图像腐蚀

作为{{Z}^{2}}中的集合AB,表示为A\ominus BBA的腐蚀定义为:

                                                                              A\ominus B=\left\{ z\left| {{(B)}_{z}}\subseteq A \right. \right\}

上式表示图像A用卷积模板B来进行腐蚀处理,通过模板B与图像A进行卷积计算,得出B覆盖区域的像素点最小值,并用这个最小值来替代参考点的像素值。如图所示,将左边的原始图像A腐蚀处理为右边的效果图A\ominus B

 

图像腐蚀的效果如下图所示:

 

图像腐蚀类似于“邻域被蚕食”,将图像中的高亮区域或白色部分进行缩减细化,其运行结果图比原图的高亮区域更小。其主要包括两个输入对象:

(1) 二值图像

(2 )卷积核

 

卷积核是腐蚀中的关键数组,采用numpy库可以生成。卷积核的中心点逐个像素扫描原始图像,腐蚀的过程如下图所示:

被扫描到的原始图像中的像素点,只有当卷积核对应的元素值均为1时,其值才为1,否则其值修改为0。换句话说,遍历到的黄色点位置,其周围全部是白色,保留白色,否则变为黑色,图像腐蚀变小。如下图所示:

 

图像腐蚀主要使用的函数为 erode(),其函数形式如下:

dst = cv2.erode(src, kernel, iterations)

其中,参数:

dst 表示处理的结果;

src 表示原图像;

kernel 表示卷积核;

iterations 表示迭代次数。

注:迭代次数默认是1,表示进行一次腐蚀,也可以根据需要进行多次迭代,进行多次腐蚀。

例如:下图表示5\times5的卷积核,可以采用函数 np.ones((5,5), np.uint8) 构建。

 

(1)卷积核大小为5\times5 ,迭代次数为1

代码如下所示:

#encoding:utf-8
import cv2
import numpy as np

#读取图片
src = cv2.imread('test1.bmp', cv2.IMREAD_UNCHANGED)

#设置卷积核
kernel = np.ones((5,5), np.uint8)

#图像腐蚀处理
erosion = cv2.erode(src, kernel)

#显示图像
cv2.imshow("src", src)
cv2.imshow("result", erosion)

#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()

 

运行结果如下图所示:

 

 

(2)卷积核大小为5\times5,迭代次数为9

代码如下所示:

#encoding:utf-8
import cv2
import numpy as np

#读取图片
src = cv2.imread('test1.bmp', cv2.IMREAD_UNCHANGED)

#设置卷积核
kernel = np.ones((5,5), np.uint8)

#图像腐蚀处理
erosion = cv2.erode(src, kernel,iterations=10)

#显示图像
cv2.imshow("src", src)
cv2.imshow("result", erosion)

#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()

 

运行结果如下图所示:

 

(2)卷积核大小为39\times39,迭代次数为1

代码如下所示:

#encoding:utf-8
import cv2
import numpy as np

#读取图片
src = cv2.imread('test1.bmp', cv2.IMREAD_UNCHANGED)

#设置卷积核
kernel = np.ones((39,39), np.uint8)

#图像腐蚀处理
erosion = cv2.erode(src, kernel)

#显示图像
cv2.imshow("src", src)
cv2.imshow("result", erosion)

#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()

 

运行结果如下图所示:

 


3 图像膨胀

作为{{Z}^{2}}中的集合AB,表示为A\oplus BBA膨胀定义为:

                                                                        A\oplus B=\left\{ z\left| {{(\widehat{B})}_{z}}\bigcap{A\ne \varnothing } \right. \right\}

图像膨胀是腐蚀操作的逆操作,类似于“领域扩张”,将图像中的高亮区域或白色部分进行扩张,其运行结果图比原图的高亮区域更大,线条变粗了,主要用于去噪。

(1) 图像被腐蚀后,去除了噪声,但是会压缩图像。

(2) 对腐蚀过的图像,进行膨胀处理,可以去除噪声,并且保持原有形状。

它也包括两个输入对象:

(1)二值图像或原始图像

(2)卷积核

图像膨胀的效果如下图所示:

 

卷积核是腐蚀中的关键数组,采用numpy库可以生成。卷积核的中心点逐个像素扫描原始图像,如下图所示:

 

 

被扫描到的原始图像中的像素点,当卷积核对应的元素值只要有一个为1时,其值就为1,否则为0

 

图像膨胀主要使用的函数为 dilate(),其函数用法如下所示:

dst = cv2.dilate(src, kernel, iterations)

其中,参数:

dst 表示处理的结果;

src 表示原始图像;

kernel 表示卷积核;

iterations 表示迭代次数。

注:迭代次数默认是1,表示进行一次膨胀,也可根据需要进行多次迭代,进行多次膨胀。通常进行1次膨胀即可。

例如,下图表示5\times5的卷积核,可以采用函数 np.ones((5,5), np.uint8) 构建。

 

(1)卷积核大小为3\times3 ,迭代次数为1

代码如下所示:

#encoding:utf-8
import cv2
import numpy as np

#读取图片
src = cv2.imread('test2.bmp', cv2.IMREAD_UNCHANGED)

#设置卷积核
kernel = np.ones((3,3), np.uint8)

#图像膨胀处理
erosion = cv2.dilate(src, kernel)

#显示图像
cv2.imshow("src", src)
cv2.imshow("result", erosion)

#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()

 

运行结果如下图所示:

 

(1)卷积核大小为5\times5 ,迭代次数为1

代码如下所示:

#encoding:utf-8
import cv2
import numpy as np

#读取图片
src = cv2.imread('test2.bmp', cv2.IMREAD_UNCHANGED)

#设置卷积核
kernel = np.ones((5,5), np.uint8)

#图像膨胀处理
erosion = cv2.dilate(src, kernel)

#显示图像
cv2.imshow("src", src)
cv2.imshow("result", erosion)

#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()

 

运行结果如下图所示:

 


 

参考资料

[1] https://blog.csdn.net/Eastmount/article/details/83581277

[2] Python+OpenCV图像处理

2019-05-11 08:31:12 Dujing2019 阅读数 3571

数字图像处理—形态学图像处理

同样的,暂时对书上已经写得很清楚的知识点不再重复赘述,主要做一些总结,思考以及知识点的梳理和扩展。

(一)预备知识

介绍一下形态学中的一些基本概念。

  1. 用数学形态学(也称图像代数)表示以形态为基础对图像进行分析的数学工具
  2. 基本思想是用具有一定形态的结构元素去度量和提取图像中的对应形状以达到对图像分析识别的目的
  3. 形态学图像处理的数学基础和所用语言是集合论
  4. 形态学图像处理的应用可以简化图像数据, 保持它们基本的形状特性,并除去不相干的结 构
  5. 形态学图像处理的基本运算有4个:膨胀、 腐蚀、开操作和闭操作

1.1 集合理论中的基本概念

介绍一下比较陌生的几个概念,其他的看书就好:

  1. 所有像素坐标的集合均不属于集合A,记为AcA^c,由下式给出:
    在这里插入图片描述
    这个集合称为集合A的补集

  2. 集合B的反射,定义为:

    即关于原集合原点对称 .

  3. 集合A平移到点z=(z1,z2),表示为(A)z,定义为:

1.2 二值图像、集合及逻辑算子

二值图像

二值图像(Binary Image),按名字来理解只有两个值,0和1,0代表黑,1代表白,或者说0表示背景,而1表示前景。其保存也相对简单,每个像素只需要1Bit就可以完整存储信息。如果把每个像素看成随机变量,一共有N个像素,那么二值图有2的N次方种变化,而8位灰度图有255的N次方种变化,8为三通道RGB图像有255255255的N次方种变化。也就是说同样尺寸的图像,二值图像保存的信息更少。二值图像(binary image),即图像上的每一个像素只有两种可能的取值或灰度等级状态,人们经常用黑白、B&W、单色图像表示二值图像。

二值图像集合

如果A和B是二值图像,那么C=A∪B仍是二值图像。这里,如 果 A 和B中相应的像素不是前景像素就是背景像素,那么 C中的这个像素就是前景像素。以第一种观点,函数 C由下式给出:
在这里插入图片描述
另一方面,运用集合的观点,C由下式给出:
在这里插入图片描述
集合运算

  1. A为图像集合,B为结构元素(集合)。
  2. 数学形态学运算时B对A进行操作。
  3. 结构元素要有1个原点(即结构元素参与形态学运算的参考点),可以是中心像素,原则上可选任何像素。
    注意:原点可以包含在结构元素中,也可以不包含在结构元素中,但运算的结果常不相同。

编码

f = imread('D:\数字图像处理\第九章学习\Fig0903(a).tif');
g = imread('D:\数字图像处理\第九章学习\Fig0903(b).tif');
subplot(2,3,1), imshow(f);title('(a)二值图像 A:');
subplot(2,3,2), imshow(g);title('(b)二值图像 B:');
subplot(2,3,3), imshow(~f);title('(c)A的补集~A:');
subplot(2,3,4), imshow(f|g);title('(d) A和B的并集 A|B:');
subplot(2,3,5), imshow(f&g);title('(e)A和B的交集 A & B:');
subplot(2,3,6), imshow(f&~g);title('(f)A和B的差集 A&~B');

代码运行效果如下
在这里插入图片描述
分析

图像(d)是 “ UTK”和 “ GT” 图像的并集,包括来自两幅图像的所有前景像素。相反,两幅图像的交集(图(e))显示了字母 “ UTK”和 “ GT”中重叠的像素。最后,集合的差集图像(图(f))显示了 “ UTK”中除去 “ GT” 像素后的字母。

(二)膨胀和腐蚀

2.1 膨胀

膨胀:膨胀是在二值图像中“加长”或“变粗”的操作。这种特殊的方式和变粗的程度由一个称为结构元素的集合控制。(实际就是将结构元素的原点与二值图像中的1重叠,将二值图像中重叠部分不是1的值变为1,完成膨胀)。

公式

A和B是两个集合,A被B膨胀定义为:

公式解释:

  1. B的反射进行平移与A的交集不为空。
  2. B的反射:相对于自身原点的映象。
  3. B的平移:对B的反射进行位移

图解

      

(a)集合A    (b)结构元素B (黑色为原点所在)

      

(c)结构元素B的映像    (d)图中两种阴影部分(深色为扩大的部分)合起来为A+B

注意

  1. 膨胀运算只要求结构元素的原点在目标图像的内部平移,换句话说,当结构元素在目标图像上平移时,允许结构元素中的非原点像素超出目标图像的范围
  2. 膨胀运算具有扩大图像和填充图像中比结果元素小的成分的作用,因此在实际应用中可以利用膨胀运算连接相邻物体和填充图像中的小孔和狭窄的缝隙

膨胀举例

膨胀函数

D = imdilate(A,B)

图像膨胀的应用:桥接文字裂缝

编码:

A = imread('D:\数字图像处理\第九章学习\Fig0906(a).tif');
B = [0 1 0; 1 1 1; 0 1 0];   %指定结构元素由0和1组成的矩阵
A2 = imdilate(A, B);    %二值图像
subplot(1,2,1), imshow(A);title('(a)包括断开文本的输入图像:');
subplot(1,2,2), imshow(A2);title('(b)膨胀后图像:');

在这里插入图片描述
图片中字体的加粗,且填充了字母中的小孔和狭窄的缝隙。

2.2 结构元的分解

公式
在这里插入图片描述
公式理解

B膨胀A等同于B1先膨胀A,再用B2膨胀之前的结果。

举例

下面是由1组成的5x5数组的膨胀:
在这里插入图片描述
这个结构元能够分解为值为 1 的 5 元素行矩阵和值为 1 的 5 元素列矩阵:

在这里插入图片描述
分析

在原结构元中,元素个数为 25; 但在行列分解后,总元素数目仅为 10。这意味着首先用 行结构元膨胀,再用列结构元膨胀,能够比 5x5 的数组膨胀快 2.5 倍。在实践中,速度的增长稍微慢一些,因为在每个膨胀运算中总有些其他开销。然而,由分解执行获得的速度方面的增 长仍然有很大意义。

2.3 strel函数

工具箱函数 strel 用于构造各种形状和大小的结构元。

基本语法

se = strel(shape, parameters)

shape用于指定希望形状的字符串,parameters是描述形状信息的参数列表。

具体例子参考课本,是基础语法。

2.4 腐蚀

腐蚀:与膨胀相反,对二值图像中的对象进行“收缩”或“细化”。(实际上将结构元素的原点覆盖在每一个二值图像的1上,只要二值图像上有0和结构元素的1重叠,那么与原点重叠的值为0)同样由集合与结构元素完成。

公式

A和B是两个集合,A被B腐蚀定义为:

公式解释:

  1. A被 B 腐蚀是包含在A中的B由z平移的所有点z的集合。
  2. B包含在A中的声明相当于B不共享A背景的任何元素。

图解
     

(a)集合A(阴影部分)   (b)结构元素B(阴影部分,深色部分为原点)(c)阴影部分合起来为A-B

注意

  1. 当结构元素中原点位置不为1(也即原点不属于结构元素时),也要把它看作是1,也就是说,当在目标图像中找与结构元素B相同的子图像时,也要求子图像中与结构元素B的原点对应的那个位置的像素的值是1。
  2. 腐蚀运算要求结构元素必须完全包括在被腐蚀图像内部:换句话说,当结构元素在目标图像上平移时,结构元素中的任何元素不能超过目标图像范围。
  3. 腐蚀运算的结果不仅与结构元素的形状选取有关,而且还与原点位置的选取有关
  4. 腐蚀运算具有缩小图像和消除图像中比结构元素小的成分的作用,因此在实际应用中,可以利用腐蚀运算去除物体之间的粘连,消除图像中的小颗粒噪声

腐蚀举例

腐蚀函数

A2 = imerode(A, se)

图像腐蚀应用:消除图像细节部分

编码:

f = imread('D:\数字图像处理\第九章学习\Fig0908(a).tif');
se = strel('disk', 10);
g = imerode(f, se);
se = strel('disk', 5);
g1 = imerode(f, se);
g2 = imerode(f, strel('disk', 20));
subplot(2,2,1), imshow(f);title('(a)原始图像的尺寸为480x480像素:');
subplot(2,2,2), imshow(g);title('(b)用半径为10的圆形腐蚀:');
subplot(2,2,3), imshow(g1);title('(c)用半径为5的圆形腐蚀:');
subplot(2,2,4), imshow(g2);title('(d)用半径为20的圆形腐蚀');

分析

假设要除去图a中的细线,但想保留其他结构,可以选取足够小的结构元来匹配中心方块,但较粗的边缘线因太大而无法匹配全部线。图b几乎成功去掉了模板中的细线,图c中一些引线还没有去掉,图d中引线都被去掉了,但是边缘引线也丢失了,所以选取合适的结构元很重要。

(三) 膨胀与腐蚀的结合

3.1 开操作和闭操作

开操作

  1. 使图像的轮廓变得光滑,断开狭窄的间断和消除细的突出物。
  2. 使用结构元素B对集合A进行开操作,定义为:

    先用B对A腐蚀,然后用B对结果膨胀。
  3. 与开操作等价的数学表达式为:
  4. A o B 的边界通过B中的点完成。
  5. B在A的边界内转动时,B中的点所能到达的A的边界的最远点。
  6. A o B 是 A的子集合。
  7. 如果C是D的子集,则 C o B是 D o B的子集。
  8. (A o B) o B = A o B

闭操作

  1. 同样使图像的轮廓变得光滑,但与开操作相反,它能消除狭窄的间断和长细的鸿沟,消除小的孔洞,并填补轮廓线中的裂痕。
  2. 使用结构元素B对集合A进行闭操作,定 义为:

    先用B对A膨胀,然后用B对结果腐蚀。
  3. A . B的边界通过B中的点完成 。
  4. B在A的边界外部转动 :
  5. A 是 A . B的子集合。
  6. 如果C 是 D 的子集 , 则C . B 是 D . B的子集。
  7. (A . B) . B = A . B

工具箱函数

开操作:

C = imopen(A, B)

闭操作:

C = imclose(A, B)

A为二值图像,B为0,1矩阵组成,并且是指定结构元素。

函数imopen 和 imclose 的应用

编码:

f = imread('D:\数字图像处理\第九章学习\Fig0910(a).tif');
se = strel('square', 40);
fo = imopen(f, se);
fc = imclose(f, se);
foc = imclose(fo, se);
subplot(2,2,1), imshow(f), title('(a)原图');
subplot(2,2,2), imshow(fo), title('(b)开操作');
subplot(2,2,3), imshow(fc), title('(c)闭操作');
subplot(2,2,4), imshow(foc), title('(d) (b)的闭操作结果');

分析

  1. 图(a)中的图像设计了一些用于演示开操作和闭操作的特征,比如细小突起、细的桥接点、几个弯口、孤立的小洞、 小的孤立物和齿状边缘。
  2. 图 (b)显示了结果。注意,从图中可以看出,细的突出和外部点的边缘的不规则部分被去除掉了,细的桥接和小的孤立物也被去除了。
  3. 图 ©中的结果: 这里,细的弯口、内部的不规则边缘和小洞都被去除了。先做开操作的闭操作的结果有平滑效果.
  4. 图 (d)显示了平滑过的物体。

噪声滤波器

先开操作再闭操作,构成噪声滤波器。

编码:

f = imread('D:\数字图像处理\第九章学习\Fig0911(a).tif');
se = strel('square', 6);
fo = imopen(f, se);
foc = imclose(fo, se);
subplot(1,3,1), imshow(f), title('(a)带噪声的指纹图像');
subplot(1,3,2), imshow(fo), title('(b)图像的开操作');
subplot(1,3,3), imshow(foc), title('(c)先用开操作,再用闭操作');

在这里插入图片描述
分析

  1. 图(a)是受噪声污染的指纹二值图像,噪声为黑色背景上的亮元素和亮指纹部分的暗元素。
  2. 图(b)所示的图像。发现,对图像进行开操作可以去除噪声点,但是这种处理在指纹的纹脊上又引入一些缺口
  3. 图( c )显示了最终结果。在这个结果中,大多数噪声被消除了,开运算的闭运算可以给指纹填充缺口,但是指纹纹路并没有完全恢复 。

3.2 击中或击不中变换

击中击不中变换(HMT),HMT变换可以同时探测图像的内部和外部。研究解决目标图像识别模式识别等领域,在处理目标图像和背景的关系上能够取得更好的效果。

作用:形状检测的基本工具。

公式

A中对B进行的匹配(击中)表示为:

B1是由与一个对象相联系的B元素构成的集合,B1是由与一个对象相联系的B元素构成的集合。

图解

工具箱函数

C = bwhitmiss(A, B1, B2)

其中的 C为结果,A为输入图像,B1、B2表示结构元素。

定位图像中物体左上角的像素

编码:

f = imread('D:\数字图像处理\第九章学习\Fig0913(a).tif');
B1 = strel([0 0 0;0 1 1; 0 1 0]);
B2 = strel([1 1 1;1 0 0;1 0 0]);
g = bwhitmiss(f,B1,B2);
subplot(1,2,1), imshow(f), title('(a)原始图像');
subplot(1,2,2), imshow(g), title('(b)击中、击不中变换的结果');

分析

  1. 图(a)显示了包括各种尺寸的正方形图像。我们要定位有东、南相邻像素(这些 “击中”)和没有东北、北、西北、西和西南相邻像素(这些 “击不中”)的前景像素。这些要求导致以下B1,B2两个结构元。这两个结构元都不包括东南邻域像素,这称为不关心像素。用函数 bwhitmiss 来计算变换。
  2. 图 (b)中的每个单像素点都是图 (a)中物体左上角的像素。图 (b)中是放大后的像素,以便更清晰。bwhitmiss的替代语法可以把Bl 和 B2 组合成间隔矩阵。只要 B1等于 1 或-1,B2 等于 1, 间隔矩阵就等于 1。对于不关心像素,间隔矩阵等于 0。

3.3 bwmorph函数

工具箱函数 bwmorph 执行许多以膨胀、腐蚀和查找表运算相结合为基础的形态学操作, 调用语法为:

g = bwmorph(f, operation, n);

f 是输入的二值图像,operation 是指定所希望运算的字符串,n 是指定重复次数的正整数。

细化

f = imread('D:\数字图像处理\第九章学习\Fig0911(a).tif');
g1 = bwmorph(f, 'thin',1);
g2 = bwmorph(f, 'thin',2);
ginf = bwmorph(f,'thin', Inf);
subplot(1,4,1),imshow(f);title('(a)指纹图像:');
subplot(1,4,2),imshow(g1);title('(b)细化一次后的指纹图像:');
subplot(1,4,3),imshow(g2);title('(c)细化两次后的图像:');
subplot(1,4,4),imshow(ginf);title('(d)一直细化到稳定状态的图像:');

在这里插入图片描述
骨骼化

f = imread('D:\数字图像处理\第九章学习\Fig0916(a).tif');
fs = bwmorph(f,'skel',Inf);
for k = 1:5
    fa = fs & ~endpoints(fs);
end
subplot(1,3,1),imshow(f);title('(a)骨头图像:');
subplot(1,3,2),imshow(fs);title('(b)使用函数 bwmorph 得到的骨豁:');
subplot(1,3,3),imshow(fa);title('(c)使用函数 endpoint 裁剪后的骨豁:');

在这里插入图片描述
分析:骨骼化(Gonzalez和 Woods[2008])是另一种减少二值图像中的物体为一组细“笔画”的方法, 这些细骨豁仍保留原始物体形状的重要信息。当 operation 置为 'skel '时,函数 bwmorph 执行骨骼化。令 f 代表图(a)中类似骨头的图像,为了计算骨骼,调用 bwmorph, 令 n=Inf,图(b)显示了骨骼化的结果,与物体的基本形状相似。骨骼化和细化经常产生短的无关的“毛刺” ,有时这被叫做寄生成分。清除(或除去)这些“毛刺”的处理称为裁剪。方法是反复确认并去除端点。通过 5 次去除端点的迭代,得以后处理骨骼化图像 fs,图(c )显示了结果。

(四)标记连通分量

工具箱函数

[L, num] = bwlabel (f, conn)

f 是输入二值图像,coon指定希望的连接方式(不是4连接就是8连接),输出L叫做标记矩阵,函数num则给出找到的连通分量总数。

计算和显示连通分量的质心:

f = imread('D:\数字图像处理\第九章学习\Fig0917(a).tif');
imshow(f);title('(a)标注连通分量原始图像:');
[L,n]=bwlabel(f);        %L为标记矩阵,n为找到连接分量的总数
[r,c]=find(L==3);        %返回第3个对象所有像素的行索引和列索引 
rbar=mean(r);
cbar=mean(c);
figure,imshow(f);title('(b)标记所有对象质心后的图像:');
hold on            %保持当前图像使其不被刷新
for k=1:n
   [r,c]=find(L==k);
   rbar=mean(r);
   cbar=mean(c);
   plot(cbar,rbar,'Marker','o','MarkerEdgeColor','k',...
        'MarkerFaceColor','k','MarkerSize',10);
   plot(cbar,rbar,'Marker','*','MarkerFaceColor','w'); %其中的marker为标记
end

(五)形态学重建

概述:重构是一种涉及到两幅图像和一个结构元素的形态学变换。一幅图像,即标记,是变换的开始点。另一幅图像是掩膜,用来约束变换过程。结构元素用于定义连接性。

定义:若G是掩膜,f为标记,则从f重构g可以记为RgR_g(f),由下列的迭代过程定义:

  1. 将h1初始化为标记图像f。
  2. 创建结构元素 :B = ones(3)。
  3. 重复

    直到

    其中,标记f必须是g的一个子集。

函数

out = imreconstruct(marker,mask)

masker是标记,mask是掩膜。

5.1 通过重建进行开操作

在形态学开操作中,腐蚀典型地去除小的物体,且随后的膨胀趋向于恢复保留的物体形状。 然而,这种恢复的精确度取决于形状和结构元之间的相似性。本节讨论的方法,通过重建进行开操作能准确地恢复腐蚀之后的物体形状。用结构元B对图像 G通过重建进行开操作可定义为 :
在这里插入图片描述

f = imread('D:\数字图像处理\第九章学习\Fig0917(a).tif');
subplot(3,2,1),imshow(f);title('(a)重构原始图像');
fe=imerode(f,ones(51,1));%竖线腐蚀
subplot(3,2,2),imshow(fe);title('(b)使用竖线腐蚀后的结果');
fo=imopen(f,ones(51,1));%竖线做开运算
subplot(3,2,3),imshow(fo);title('(c)使用竖线做开运算结果');
fobr=imreconstruct(fe,f);%fe做标记
subplot(3,2,4),imshow(fobr);title('(d)使用竖线做重构开运算');
ff=imfill(f,'holes');%对f进行孔洞填充
subplot(3,2,5),imshow(ff);title('(e)对f填充孔洞后的图像');
fc=imclearborder(f,8);%清除边界,2维8邻接
subplot(3,2,6),imshow(fc);title('(f)对f清除边界后的图像');

在这里插入图片描述
分析

  1. 传统开运算中,腐蚀去除掉小对象,随后的膨胀恢复原始对象形状,但受元素结构影响,恢复的往往不是很精确。
  2. 重构则能精确恢复原始图像。

5.2 填充孔洞

令I表示二值图像,假设我们选择标记图像F,除了图像边缘外,其余部分都为 0, 边缘部分设值为 1-I:
在这里插入图片描述
函数

g = imfill(f,‘holes’);

5.3 清除边界物体

定义标记图像F为:
在这里插入图片描述
其中,/是原始图像,然后以/作为模板图像,重建
在这里插入图片描述
得到一幅图像H, 其中仅包含与边界接触的物体。

函数

g = imclearborder(f,conn)

f 是输入图像,g 是结果。conn 的值不是 4 就是 8(默认)。 物体更亮且与图像边界相连接的结构。

(六)灰度级形态学

6.1 膨胀和腐蚀

灰度图像的形态学梯度定义为膨胀运算与腐蚀运算的结果之间的差值。

膨胀定义

  1. 使用结构元素b对f的灰度膨胀定义为:

    其中,DfD_fDbD_b分别是f和b的定义域,f和b是函数而不是二值形态学情况中的集合。

  2. 当结构元素b是平坦的,即b(x,y)在其定义域内都为0时:
    在这里插入图片描述

腐蚀定义

  1. 使用结构元素b对f的灰度腐蚀定义为:
    在这里插入图片描述
    其中,DfD_fDbD_b分别是f和b的定义域。

  2. 当结构元素b是平坦的,即b(x,y)在其定义域内都为0时:
    在这里插入图片描述

膨胀和腐蚀操作

编写代码:

f = imread('D:\数字图像处理\第九章学习\Fig0923(a).tif');
se=strel('square',3);  %构造了一个平坦的3x3的结构元素
gd=imdilate(f,se);    %对原图像进行膨胀操作
ge=imerode(f,se);     %对原图像进行腐蚀操作
morph_grad=imsubtract(gd,ge); %从膨胀的图像中减去腐蚀过得图像产生一个形态学梯度。
subplot(3,2,1);imshow(f,[]);title('(a)原始图像');
subplot(3,2,2),imshow(gd,[]);title('(b)膨胀的图像');
subplot(3,2,3),imshow(ge,[]);title('(c)腐蚀的图像');
subplot(3,2,4),imshow(morph_grad,[]);title('(d)形态学梯度');

在这里插入图片描述
分析

  1. 膨胀得到的图像比原图像更明亮,并且减弱或消除小的,暗的细节部分。即比原图像模糊。
  2. 腐蚀得到的图像更暗,并且尺寸小,明亮的部分被削弱 。

6.2 开操作和闭操作

图像开运算

  1. 在灰度图像中,开操作的表达式与二值图像拥有相同的形式。
  2. 把一幅图像看做是一个三维表明,其亮度值代表xy平面上的高度值,则当结构元素b在f下面活动时,结构元素的任何部分的最高值构成了开运算的结果。
  3. 先进行腐蚀操作可以除去小的亮的图像细节,但这样会使图像变暗,接下来进行膨胀操作增强图像的整体亮度。

图像闭运算

  1. 在灰度图像中,闭操作的表达式与二值图像拥有相同的形式。
  2. 当结构元素b在f的上面活动时,结构元素的任何部分的最低值构成了闭运算的结果 。
  3. 先通过膨胀操作除去图像中的暗细节,同时增加图像的亮度,接下来对图像进行腐蚀,而不会将膨胀操作除去的部分重新引入图像中。

用开操作和闭操作做形态学平滑

f = imread('D:\数字图像处理\第九章学习\Fig0925(a).tif');
subplot(3,2,1),imshow(f);  
title('(a)木钉图像原图');   
se=strel('disk',5);     %disk其实就是一个八边形  
fo=imopen(f,se);        %经过开运算  
subplot(3,2,2),imshow(f);  
title('(b)使用半径5的disk开运算后的图像');   
foc=imclose(fo,se);  
subplot(3,2,3),imshow(foc);  
title('(c)先开后闭的图像'); 
focd=imclose(f,se);  
subplot(3,2,4),imshow(focd);  
title('(d)原始图像的闭操作'); 
foce=imopen(focd,se);  
subplot(3,2,5),imshow(foce);  
title('(e)先闭后开的图像'); 
fasf=f;  
for i=2:5  
    se=strel('disk',i);  
    fasf=imclose(imopen(fasf,se),se);  
end  
subplot(3,2,6),imshow(fasf);  
title('(f)使用开闭交替滤波后图像'); 


在这里插入图片描述
分析

  1. 图 (b)显示了开操作的图像 fo, 在这里,我们看到,亮区域己经被调低了(平滑),木钉上的暗条文几乎没有受影响。
  2. 图 (c )显示了开操作的闭操作 foe。现在我们注意到,暗区域已经被平滑得很好了,结果是整个图像得到全部平滑。这种过程通常叫做开-闭滤波。先开运算后闭运算构成噪声滤波器,用来平滑图像并去除噪声。
  3. 图 (d)显示了原始图像的闭操作结果。木钉上的暗条文已经被平滑掉了,主要留下了亮的细节(注意背景中的亮条文)。
  4. 图 (e)显示了这些条文的平滑和木钉表面的进一步平滑效果。最终结果是原始图像得到全部平滑。
  5. 图(f)是交替顺序滤波,交替顺序滤波的一种形式是用不断增大的一系列结构元执行开-闭滤波,刚开始用小的结构元,增加大小,直到与图 (b)和©中结构元的大小相同为止。交替顺序滤波与单个开-闭滤波相比,处理图像更平滑一些。

非均匀背景的补偿

f = imread('D:\数字图像处理\第九章学习\Fig0926(a).tif');
g = f>=(255*graythresh(f));
se=strel('disk',100);
fo=imopen(f,se);
f2=imsubtract(f,fo); 
g1 = f2>=(255*graythresh(f2));
subplot(2,3,1),imshow(f);  
title('(a)原始图像');  
subplot(2,3,2),imshow(g);  
title('(b)经过阈值处理后的图像');   
subplot(2,3,3),imshow(f);  
title('(c)原图开运算后的图像');  
subplot(2,3,4),imshow(f2);  
title('(d)原图减去开运算');  
subplot(2,3,5),imshow(g1);  
title('(e)最终结果');  

在这里插入图片描述
分析

  1. 图 (a) :显示了一幅米粒的图像f,图像下部的背景比上部的黑。这样的话,对不平坦的亮度进行阈值处理会很困难。
  2. 图 (b) "是阈值处理方案,图像顶端的米粒被很好地从背景中分离开来,但是图像底部的米粒没有从背景中正确地提取出来。
  3. 图(c ):对图像进行开操作,可以产生对整个图像背景的合理估计。
  4. 图(d) :把图(c )从原始图像中减去,生成一幅拥有合适的均勾背景的米粒图像.
  5. 图(e):显示了新的经阈值处理后的图像。注意,改进效果超过了图 (b)。

粒度测定 :

颗粒分析:形态学技术可以用与间接地度量颗粒的大小分布,但不能准确地识别每一个颗粒。对于形状规则且亮于背景大的颗粒,基本方法是应用不断增大尺寸的形态学开运算。

f = imread('D:\数字图像处理\第九章学习\Fig0926(a).tif');
sumpixels=zeros(1,36);  
for k=0:35  
    se=strel('disk',k);  
    fo=imopen(f,se);  
    sumpixels(k+1)=sum(fo(:));  
end    
%可以看到,连续开运算之间的表面积会减少  
plot(0:35,sumpixels),xlabel('k'),ylabel('surface area');  
title('(a)表面积和结构元素半径之间的关系');  
figure,plot(-diff(sumpixels));%diff()函数为差分或者近似倒数,即相邻2个之间的差值  
xlabel('k'),ylabel('surface area reduction');  
title('(b)减少的表面积和结构元素半径之间的关系'); 

分析

  1. (a)连续开运算之间的表面积会减小。
  2. (b)图峰值表明出现了大量的有着这种半径的对象。

6.3 重建

重建

  1. h极小值变换:标记图像是由掩膜挑选ing减去常量所得。
  2. 开运算重建:先腐蚀后重建。
  3. 闭运算重建:对图像求补、计算其开操作重建并对结果求补。

重建移去复杂的背景

f = imread('D:\数字图像处理\第九章学习\Fig0930(a).tif');
subplot(3,3,1),imshow(f);  
title('(a)原图像');    
f_obr=imreconstruct(imerode(f,ones(1,71)),f);  
subplot(3,3,2),imshow(f_obr);  
title('(b)重建的开操作');   
f_o=imopen(f,ones(1,71));    
subplot(3,3,3),imshow(f_o);  
title('(c)开操作');    
f_thr=imsubtract(f,f_obr);    %顶帽重构
subplot(3,3,4),imshow(f_thr);  
title('(d)重建的顶帽操作');  
f_th=imsubtract(f,f_o)    %标准顶帽运算,方便比较
subplot(3,3,5),imshow(f_th);  
title('(e)顶帽操作');  
g_obr=imreconstruct(imerode(f_thr,ones(1,11)),f_thr);  
subplot(3,3,6),imshow(g_obr);  
title('(f)用水平线对(b)经开运算后重建图');   
g_obrd=imdilate(g_obr,ones(1,2));  
subplot(3,3,7),imshow(g_obrd);  
title('(g)使用水平线对(f)进行膨胀');  
f2=imreconstruct(min(g_obrd,f_thr),f_thr);  
subplot(3,3,8),imshow(f2);  
title('(h)最后的重建结果');  

在这里插入图片描述
分析

为了消除每个键盘上方的水平反射光,利用这些反射比图像中任何文本字符都要宽的这个事实。用长水平线的结构元执行重建的开操作,重建的开操作(f_obr) 显示于图(b)中。为了进行对比,图(c )显示了标准的开操作 (f_o) 。重建的开操作在提取水平的相邻键之间的背景方面的确较好。从原始图像中减去重建的开操作被称为顶帽重建 , 结果示于图 (d)中。消除图 (d)中键右边的垂直反射光。这可以通过用短的水平线执行重建的开操作来完成,在这个结果中(见图 (f)),垂直的反射光不见了。但是,包括字母的垂直的细笔画也不见了。我们利用了那些已被错误消除的字母非常接近第一次膨胀(见图 (g))后还存在的其他字符这一事实,以 f_thr 作为模板,以 min(g_obrd,f_thr) 作为标记,图 (h)显示了最后的结果。注意,背景上键盘的阴影和反射光都成功去除了。

2014-04-14 23:12:24 zhmxy555 阅读数 140215


本系列文章由@浅墨_毛星云 出品,转载请注明出处。  

文章链接: http://blog.csdn.net/poem_qianmo/article/details/23710721

作者:毛星云(浅墨)    邮箱: happylifemxy@163.com 

写作当前博文时配套使用的OpenCV版本: 2.4.8



本篇文章中,我们一起探究了图像处理中,最基本的形态学运算——膨胀与腐蚀。浅墨在文章开头友情提醒,用人物照片做腐蚀和膨胀的素材图片得到的效果会比较惊悚,毁三观的,不建议尝试。。。。。。。。。。


OK,开始吧,依然是先放一张截图:





一、理论与概念讲解——从现象到本质



1.1 形态学概述

 

形态学(morphology)一词通常表示生物学的一个分支,该分支主要研究动植物的形态和结构。而我们图像处理中指的形态学,往往表示的是数学形态学。下面一起来了解数学形态学的概念。

数学形态学(Mathematical morphology) 是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二值腐蚀和膨胀、二值开闭运算、骨架抽取、极限腐蚀、击中击不中变换、形态学梯度、Top-hat变换、颗粒分析、流域变换、灰值腐蚀和膨胀、灰值开闭运算、灰值形态学梯度等。

 

简单来讲,形态学操作就是基于形状的一系列图像处理操作。OpenCV为进行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有二种,他们是:膨胀与腐蚀(Dilation与Erosion)。

膨胀与腐蚀能实现多种多样的功能,主要如下:

  • 消除噪声
  • 分割(isolate)出独立的图像元素,在图像中连接(join)相邻的元素。
  • 寻找图像中的明显的极大值区域或极小值区域
  • 求出图像的梯度

 


我们在这里给出下文会用到的,用于对比膨胀与腐蚀运算的“浅墨”字样毛笔字原图:

 

在进行腐蚀和膨胀的讲解之前,首先需要注意,腐蚀和膨胀是对白色部分(高亮部分)而言的,不是黑色部分。膨胀就是图像中的高亮部分进行膨胀,“领域扩张”,效果图拥有比原图更大的高亮区域。腐蚀就是原图中的高亮部分被腐蚀,“领域被蚕食”,效果图拥有比原图更小的高亮区域。

 





1.2 膨胀

 

其实,膨胀就是求局部最大值的操作。

按数学方面来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,我们称之为A)与核(我们称之为B)进行卷积。

核可以是任何的形状和大小,它拥有一个单独定义出来的参考点,我们称其为锚点(anchorpoint)。多数情况下,核是一个小的中间带有参考点和实心正方形或者圆盘,其实,我们可以把核视为模板或者掩码。

 

而膨胀就是求局部最大值的操作,核B与图形卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长。如下图所示,这就是膨胀操作的初衷。



膨胀的数学表达式:


膨胀效果图(毛笔字):

 

照片膨胀效果图:


 



1.3 腐蚀


再来看一下腐蚀,大家应该知道,膨胀和腐蚀是一对好基友,是相反的一对操作,所以腐蚀就是求局部最小值的操作。

我们一般都会把腐蚀和膨胀对应起来理解和学习。下文就可以看到,两者的函数原型也是基本上一样的。

 

原理图:

 

腐蚀的数学表达式:

 

腐蚀效果图(毛笔字):


照片腐蚀效果图:

 

 浅墨表示这张狗狗超可爱:D

 

 



二、深入——OpenCV源码分析溯源

 


直接上源码吧,在…\opencv\sources\modules\imgproc\src\ morph.cpp路径中 的第1353行开始就为erode(腐蚀)函数的源码,1361行为dilate(膨胀)函数的源码。

//-----------------------------------【erode()函数中文注释版源代码】---------------------------- 
//    说明:以下代码为来自于计算机开源视觉库OpenCV的官方源代码 
//    OpenCV源代码版本:2.4.8 
//    源码路径:…\opencv\sources\modules\imgproc\src\ morph.cpp 
//    源文件中如下代码的起始行数:1353行 
//    中文注释by浅墨 
//--------------------------------------------------------------------------------------------------------  
void cv::erode( InputArray src, OutputArraydst, InputArray kernel,
                Point anchor, int iterations,
                int borderType, constScalar& borderValue )
{
//调用morphOp函数,并设定标识符为MORPH_ERODE
   morphOp( MORPH_ERODE, src, dst, kernel, anchor, iterations, borderType,borderValue );
}

//-----------------------------------【dilate()函数中文注释版源代码】---------------------------- 
//    说明:以下代码为来自于计算机开源视觉库OpenCV的官方源代码 
//    OpenCV源代码版本:2.4.8 
//    源码路径:…\opencv\sources\modules\imgproc\src\ morph.cpp 
//    源文件中如下代码的起始行数:1361行 
//    中文注释by浅墨 
//-------------------------------------------------------------------------------------------------------- 
void cv::dilate( InputArray src,OutputArray dst, InputArray kernel,
                 Point anchor, int iterations,
                 int borderType, constScalar& borderValue )
{
//调用morphOp函数,并设定标识符为MORPH_DILATE
   morphOp( MORPH_DILATE, src, dst, kernel, anchor, iterations, borderType,borderValue );
}


可以发现erode和dilate这两个函数内部就是调用了一下morphOp,只是他们调用morphOp时,第一个参数标识符不同,一个为MORPH_ERODE(腐蚀),一个为MORPH_DILATE(膨胀)。

morphOp函数的源码在…\opencv\sources\modules\imgproc\src\morph.cpp中的第1286行,有兴趣的朋友们可以研究研究,这里就不费时费力花篇幅展开分析了。

 

 

 

三、浅出——API函数快速上手

 



3.1  形态学膨胀——dilate函数

 


erode函数,使用像素邻域内的局部极大运算符来膨胀一张图片,从src输入,由dst输出。支持就地(in-place)操作。

函数原型:

C++: void dilate(
	InputArray src,
	OutputArray dst,
	InputArray kernel,
	Point anchor=Point(-1,-1),
	int iterations=1,
	int borderType=BORDER_CONSTANT,
	const Scalar& borderValue=morphologyDefaultBorderValue() 
);

参数详解:

  • 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。
  • 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
  • 第三个参数,InputArray类型的kernel,膨胀操作的核。若为NULL时,表示的是使用参考点位于中心3x3的核。

我们一般使用函数 getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。

其中,getStructuringElement函数的第一个参数表示内核的形状,我们可以选择如下三种形状之一:

    • 矩形: MORPH_RECT
    • 交叉形: MORPH_CROSS
    • 椭圆形: MORPH_ELLIPSE

而getStructuringElement函数的第二和第三个参数分别是内核的尺寸以及锚点的位置。

我们一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得getStructuringElement函数的返回值。对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心。且需要注意,十字形的element形状唯一依赖于锚点的位置。而在其他情况下,锚点只是影响了形态学运算结果的偏移。

getStructuringElement函数相关的调用示例代码如下:

 int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
 
//获取自定义核
Mat element = getStructuringElement(MORPH_RECT,
	Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),
	Point( g_nStructElementSize, g_nStructElementSize ));


调用这样之后,我们便可以在接下来调用erode或dilate函数时,第三个参数填保存了getStructuringElement返回值的Mat类型变量。对应于我们上面的示例,就是填element变量。


  • 第四个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于中心。
  • 第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1。
  • 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
  • 第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般我们不用去管他。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释。
  •  

使用erode函数,一般我们只需要填前面的三个参数,后面的四个参数都有默认值。而且往往结合getStructuringElement一起使用。

调用范例:

       	//载入原图 
       	Mat image = imread("1.jpg");
	//获取自定义核
       	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       	Mat out;
       	//进行膨胀操作
       	dilate(image, out, element);

用上面核心代码架起来的完整程序代码:

 

//-----------------------------------【头文件包含部分】---------------------------------------
//     描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【命名空间声明部分】---------------------------------------
//     描述:包含程序所使用的命名空间
//----------------------------------------------------------------------------------------------- 
using namespace std;
using namespace cv;
 
//-----------------------------------【main( )函数】--------------------------------------------
//     描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(  )
{
 
       //载入原图 
       Mat image = imread("1.jpg");
 
       //创建窗口 
       namedWindow("【原图】膨胀操作");
       namedWindow("【效果图】膨胀操作");
 
       //显示原图
       imshow("【原图】膨胀操作", image);
 
	//获取自定义核
       Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       Mat out;
	//进行膨胀操作
       dilate(image,out, element);
 
       //显示效果图
       imshow("【效果图】膨胀操作", out);
 
       waitKey(0);
 
       return 0;
}

 运行截图:



 

 

 

3.2 形态学腐蚀——erode函数



erode函数,使用像素邻域内的局部极小运算符来腐蚀一张图片,从src输入,由dst输出。支持就地(in-place)操作。

 

看一下函数原型:

C++: void erode(
	InputArray src,
	OutputArray dst,
	InputArray kernel,
	Point anchor=Point(-1,-1),
	int iterations=1,
	int borderType=BORDER_CONSTANT,
	const Scalar& borderValue=morphologyDefaultBorderValue()
 );

参数详解:

  • 第一个参数,InputArray类型的src,输入图像,即源图像,填Mat类的对象即可。图像通道的数量可以是任意的,但图像深度应为CV_8U,CV_16U,CV_16S,CV_32F或 CV_64F其中之一。
  • 第二个参数,OutputArray类型的dst,即目标图像,需要和源图片有一样的尺寸和类型。
  • 第三个参数,InputArray类型的kernel,腐蚀操作的内核。若为NULL时,表示的是使用参考点位于中心3x3的核。我们一般使用函数 getStructuringElement配合这个参数的使用。getStructuringElement函数会返回指定形状和尺寸的结构元素(内核矩阵)。(具体看上文中浅出部分dilate函数的第三个参数讲解部分)
  • 第四个参数,Point类型的anchor,锚的位置,其有默认值(-1,-1),表示锚位于单位(element)的中心,我们一般不用管它。
  • 第五个参数,int类型的iterations,迭代使用erode()函数的次数,默认值为1。
  • 第六个参数,int类型的borderType,用于推断图像外部像素的某种边界模式。注意它有默认值BORDER_DEFAULT。
  • 第七个参数,const Scalar&类型的borderValue,当边界为常数时的边界值,有默认值morphologyDefaultBorderValue(),一般我们不用去管他。需要用到它时,可以看官方文档中的createMorphologyFilter()函数得到更详细的解释。

同样的,使用erode函数,一般我们只需要填前面的三个参数,后面的四个参数都有默认值。而且往往结合getStructuringElement一起使用。

调用范例:

       	//载入原图 
       	Mat image = imread("1.jpg");
	//获取自定义核
       	Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       	Mat out;
       	//进行腐蚀操作
       	erode(image,out, element);

用上面核心代码架起来的完整程序代码:

 

//-----------------------------------【头文件包含部分】---------------------------------------
//     描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include<opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【命名空间声明部分】---------------------------------------
//     描述:包含程序所使用的命名空间
//----------------------------------------------------------------------------------------------- 
using namespace std;
using namespace cv;
 
//-----------------------------------【main( )函数】--------------------------------------------
//     描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main(  )
{
       //载入原图 
       Matimage = imread("1.jpg");
 
        //创建窗口 
       namedWindow("【原图】腐蚀操作");
       namedWindow("【效果图】腐蚀操作");
 
       //显示原图
       imshow("【原图】腐蚀操作", image);
 
        
//获取自定义核
       Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
       Mat out;
 
//进行腐蚀操作
       erode(image,out, element);
 
       //显示效果图
       imshow("【效果图】腐蚀操作", out);
 
       waitKey(0);
 
       return 0;
}


运行结果:

 

 

 

 

四、综合示例——在实战中熟稔

 

 

依然是每篇文章都会配给大家的一个详细注释的博文配套示例程序,把这篇文章中介绍的知识点以代码为载体,展现给大家。

这个示例程序中的效果图窗口有两个滚动条,顾名思义,第一个滚动条“腐蚀/膨胀”用于在腐蚀/膨胀之间进行切换;第二个滚动条”内核尺寸”用于调节形态学操作时的内核尺寸,以得到效果不同的图像,有一定的可玩性。废话不多说,上代码吧:

 
//-----------------------------------【程序说明】----------------------------------------------
//            程序名称::《【OpenCV入门教程之十】形态学图像处理(一):膨胀与腐蚀  》 博文配套源码
//            开发所用IDE版本:Visual Studio 2010
//          开发所用OpenCV版本: 2.4.8
//            2014年4月14日 Create by 浅墨
//            浅墨的微博:@浅墨_毛星云
//------------------------------------------------------------------------------------------------
 
//-----------------------------------【头文件包含部分】---------------------------------------
//            描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
#include<opencv2/imgproc/imgproc.hpp>
#include <iostream>
 
//-----------------------------------【命名空间声明部分】---------------------------------------
//            描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace std;
using namespace cv;
 
 
//-----------------------------------【全局变量声明部分】--------------------------------------
//            描述:全局变量声明
//-----------------------------------------------------------------------------------------------
Mat g_srcImage, g_dstImage;//原始图和效果图
int g_nTrackbarNumer = 0;//0表示腐蚀erode, 1表示膨胀dilate
int g_nStructElementSize = 3; //结构元素(内核矩阵)的尺寸
 
 
//-----------------------------------【全局函数声明部分】--------------------------------------
//            描述:全局函数声明
//-----------------------------------------------------------------------------------------------
void Process();//膨胀和腐蚀的处理函数
void on_TrackbarNumChange(int, void *);//回调函数
void on_ElementSizeChange(int, void *);//回调函数
 
 
//-----------------------------------【main( )函数】--------------------------------------------
//            描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
       //改变console字体颜色
       system("color5E"); 
 
       //载入原图
       g_srcImage= imread("1.jpg");
       if(!g_srcImage.data ) { printf("Oh,no,读取srcImage错误~!\n"); return false; }
      
       //显示原始图
       namedWindow("【原始图】");
       imshow("【原始图】", g_srcImage);
      
       //进行初次腐蚀操作并显示效果图
       namedWindow("【效果图】");
       //获取自定义核
       Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));
       erode(g_srcImage,g_dstImage, element);
       imshow("【效果图】", g_dstImage);
 
       //创建轨迹条
       createTrackbar("腐蚀/膨胀", "【效果图】", &g_nTrackbarNumer, 1, on_TrackbarNumChange);
       createTrackbar("内核尺寸", "【效果图】",&g_nStructElementSize, 21, on_ElementSizeChange);
 
       //输出一些帮助信息
       cout<<endl<<"\t嗯。运行成功,请调整滚动条观察图像效果~\n\n"
              <<"\t按下“q”键时,程序退出~!\n"
              <<"\n\n\t\t\t\tby浅墨";
 
       //轮询获取按键信息,若下q键,程序退出
       while(char(waitKey(1))!= 'q') {}
 
       return 0;
}
 
//-----------------------------【Process( )函数】------------------------------------
//            描述:进行自定义的腐蚀和膨胀操作
//-----------------------------------------------------------------------------------------
void Process()
{
       //获取自定义核
       Mat element = getStructuringElement(MORPH_RECT, Size(2*g_nStructElementSize+1,2*g_nStructElementSize+1),Point( g_nStructElementSize, g_nStructElementSize ));
 
       //进行腐蚀或膨胀操作
       if(g_nTrackbarNumer== 0) {   
              erode(g_srcImage,g_dstImage, element);
       }
       else{
              dilate(g_srcImage,g_dstImage, element);
       }
 
       //显示效果图
       imshow("【效果图】", g_dstImage);
}
 
 
//-----------------------------【on_TrackbarNumChange( )函数】------------------------------------
//            描述:腐蚀和膨胀之间切换开关的回调函数
//-----------------------------------------------------------------------------------------------------
void on_TrackbarNumChange(int, void *)
{
       //腐蚀和膨胀之间效果已经切换,回调函数体内需调用一次Process函数,使改变后的效果立即生效并显示出来
       Process();
}
 
 
//-----------------------------【on_ElementSizeChange( )函数】-------------------------------------
//            描述:腐蚀和膨胀操作内核改变时的回调函数
//-----------------------------------------------------------------------------------------------------
void on_ElementSizeChange(int, void *)
{
       //内核尺寸已改变,回调函数体内需调用一次Process函数,使改变后的效果立即生效并显示出来
       Process();
}


 

放出一些效果图吧。原始图:

 


膨胀效果图:

 






腐蚀效果图:







腐蚀和膨胀得到的图,都特有喜感,但千变万变,还是原图好看:



OK,就放出这些吧,具体更多的运行效果大家就自己下载示例程序回去玩吧。


本篇文章到这里就基本结束了,最后放出文章配套示例程序的打包下载地址。

 

本篇文章的配套源代码请点击这里下载:


【浅墨OpenCV入门教程之十】配套源代码下载

 


OK,今天的内容大概就是这些,我们下篇文章见:)