二值图像是一种简单的图像格式,它只有两个灰度级,即"0"表示黑色的像素点,"255"表示白色的像素点,至于如何从一幅普通的图像获得二值图像,请参考我近期在天极网上发表的《Visual C++编程实现图像的分割》一文。二值图像处理在图像处理领域占据很重要的位置,在具体的图像处理应用系统中,往往需要对于获得的二值图像再进一步进行处理,以有利于后期的识别工作。二值图像处理运算是从数学形态学下的集合论方法发展起来的,尽管它的基本运算很简单,但是却可以产生复杂的效果。常用的二值图像处理操作有许多方法,如腐蚀、膨胀、细化、开运算和闭运算等等。本文对这些内容作些研究探讨, 希望对爱好图像处理的朋友有所帮助。
一、腐蚀和膨胀
形态学是一门新兴科学,它的用途主要是获取物体拓扑和结果信息,它通过物体和结构元素相互作用的某些运算,得到物体更本质的形态。它在图像处理中的应用主要是:
1.利用形态学的基本运算,对图像进行观察和处理,从而达到改善图像质量的目的;
2.描述和定义图像的各种几何参数和特征,如面积,周长,连通度,颗粒度,骨架和方向性。
限于篇幅,我们只介绍简单二值图像的形态学运算,对于灰度图像的形态学运算,有兴趣的读者可以看有关的参考书。二值图像基本的形态学运算是腐蚀和膨胀,简单的腐蚀是消除物体的所有边界点的一种过程,其结果是使剩下的物体沿其周边比原物体小一个像素的面积。如果物体是圆的,它的直径在每次腐蚀后将减少两个像素,如果物体在某一点处任意方向上连通的像素小于三个,那么该物体经过一次腐蚀后将在该点处分裂为二个物体。简单的膨胀运算是将与某物体接触的所有背景点合并到该物体中的过程。过程的结果是使物体的面积增大了相应数量的点,如果物体是圆的,它的直径在每次膨胀后将增大两个像素。如果两个物体在某一点的任意方向相隔少于三个像素,它们将在该点连通起来。
下面给出具体的实现腐蚀和膨胀的函数代码:
////////////////////////////////二值图像腐蚀操作函数 BOOL ImageErosion(BYTE *pData,int Width,int Height) {//pData为图像数据的指针,Width和Height为图像的宽和高; BYTE* pData1; int m,n,i,j,sum,k,sum1; BOOL bErosion; if(pData==NULL) { AfxMessageBox("图像数据为空,请读取图像数据"); return FALSE; } //申请空间,pData1存放处理后的数据; pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height]; if(pData1==NULL) { AfxMessageBox("图像缓冲数据区申请失败,请重新申请图像数据缓冲区"); return FALSE ; } memcpy(pData1,pData,WIDTHBYTES(Width*8)*Height); for(i=10;i<Height-10;i++) for(j=32;j<Width-32;j++) } if(bErosion) } } } memcpy(pData,pData1,WIDTHBYTES(Width*8)*Height); return TRUE; } ////////////////////////////////////二值图像的膨胀操作 BOOL ImageDilation(BYTE *pData,int Width,int Height) { BYTE* pData1; int m,n,i,j,sum,k,sum1; BOOL bDilation; if(pData==NULL) { AfxMessageBox("图像数据为空,请读取图像数据"); return FALSE; } //申请空间,pData1存放处理后的数据; pData1=(BYTE*)new char[WIDTHBYTES(Width*8)*Height]; if(pData1==NULL) { AfxMessageBox("图像缓冲数据区申请失败,请重新申请图像数据缓冲区"); return FALSE ; } memcpy(pData1,pData,WIDTHBYTES(Width*8)*Height); for(i=10;i<Height-10;i++) for(j=32;j<Width-32;j++) } if(bDilation) } } } memcpy(pData,pData1,WIDTHBYTES(Width*8)*Height); return TRUE; } |
从上面的说明可以看出,腐蚀可以消除图像中小的噪声区域,膨胀可以填补物体中的空洞。对一个图像先进行腐蚀运算然后再膨胀的操作过程称为开运算,它可以消除细小的物体、在纤细点处分离物体、平滑较大物体的边界时不明显的改变其面积。如果对一个图像先膨胀然后再收缩,我们称之为闭运算,它具有填充物体内细小的空洞、连接邻近物体、在不明显改变物体面积的情况下平滑其边界的作用。通常情况下,当有噪声的图像用阈值二值化后,所得到的边界是很不平滑的,物体区域具有一些错判的孔洞,背景区域散布着一些小的噪声物体,连续的开和闭运算可以显著的改善这种情况,这时候需要在连接几次腐蚀迭代之后,再加上相同次数的膨胀,才可以产生所期望的效果。为了更好的显示出二值图像的处理效果,我们仍旧以图像采集卡获取的汽车图像为处理源图像,下图为处理后的效果:
(a)噪声图 |
(b)开运算处理 |
图一 开运算效果图
上图中,a图为包含噪声的图像,b图为经过腐蚀膨胀处理后的图像,可以看出,经过上述处理,成功的消除了图像中的噪声点,同时又起到了平滑边缘的
作用。
二、细化
图像处理中物体的形状信息是十分重要的,为了便于描述和抽取图像特定区域的特征,对那些表示物体的区域通常需要采用细化算法处理,得到与原来物体区域形状近似的由简单的弧或曲线组成的图形,这些细线处于物体的中轴附近,这就是所谓的图像的细化。通俗的说图像细化就是从原来的图像中去掉一些点,但仍要保持目标区域的原来形状,通过细化操作可以将一个物体细化为一条单像素宽的线,从而图形化的显示出其拓补性质。实际上,图像细化就是保持原图的骨架。所谓骨架,可以理解为图象的中轴,例如一个长方形的骨架是它的长方向上的中轴线;正方形的骨架是它的中心点;圆的骨架是它的圆心,直线的骨架是它自身,孤立点的骨架也是自身。对于任意形状的区域,细化实质上是腐蚀操作的变体,细化过程中要根据每个像素点的八个相邻点的情况来判断该点是否可以剔除或保留。下面我们给几个例子来说明如何判断当前像素点是否该保留或剔除。
图二 根据某点的八个相邻点的情况来判断该点是否能删除
上图给出了当前需要处理的像素点在不同的八邻域条件下的情况,可以看出:(1)不能删,因为它是个内部点,我们要求的是骨架,如果连内部点也删了,骨架也会被掏空的;(2)不能删,和(1)是同样的道理;(3)可以删,这样的点不是骨架;(4)不能删,因为删掉后,原来相连的部分断开了;(5)可以删,这样的点不是骨架;(6)不能删,因为它是直线的端点,如果这样的点删了,那么最后整个直线也被删了,剩不下什么;(7)不能删,因为孤立点的骨架就是它自身。 总结一下,有如下的判据:1.内部点不能删除;2.孤立点不能删除;3.直线端点不能删除;4.如果P是边界点,去掉P后,如果连通分量不增加,则P可以删除。我们可以根据上述的判据,事先做出一张表,从0到255共有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。仔细考虑当前像素点的各种八邻域的情况,我们可以得到一个细化操作查找表,该表在下面的细化算法中详细介绍。
为了避免分裂物体,细化的过程分为两个步骤,第一步是正常的腐蚀操作,但是它是有条件的,也就是说那些被标记的可除去的像素点并不立即消去;在第二步中,只将那些消除后并不破坏连通性的点消除,否则的话保留这些边界点。以上的步骤是在一个3x3邻域内运算,可以通过查表实现细化的操作。算法的实现步骤如下:
1) 定义一个3x3模板和一个查找表,模板和查找表分别如图二和表一所示:
1 | 2 | 4 |
128 | 256 | 8 |
64 | 32 | 16 |
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 }; |
BOOL SeneBorderThinning(BYTE *pData,int Width,int Height) { //pData为指向图像数据的指针,Width和Height为图像的宽度和高度; int i,j; int num; //细化结束标志; BOOL Finished; //各个变量用来存储(i,j)位置的八邻域像素点的灰度; int nw,n,ne,w,e,sw,s,se; //细化表; static int erasetable[256]=; if(pData==NULL) { AfxMessageBox("图像数据为空,请读取图像数据"); return FALSE; } Finished=FALSE; //开始细化; while(!Finished) { Finished=TRUE; //水平扫描; for (i=10;i<Height-10;i++) { for(j=30;j<Width-30;j++) { if(*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)==0) { w=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j-1); e=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j+1); //判断(i,j)是否是边界点,如是,求该点的八邻域灰度值(0/255),根据各点的权重,计算对应查找表的索引; if( (w==255)|| (e==255)) { nw=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j-1); n=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j); ne=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j+1); sw=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j-1); s=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j); se=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j+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) { //查表,如果符合条件,将边界点修改为图像的背景; *(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)=(BYTE)255; Finished=FALSE;//再次进行扫描; j++; } } } } } //垂直扫描; for (j=30;j<Width-30;j++) { for(i=10;i<Height-10;i++) { if(*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)==0) { n=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j); s=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j); if( (n==255)|| (s==255)) { nw=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j-1); ne=*(pData+WIDTHBYTES(Width*8)*(Height-i)+j+1); w=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j-1); e=*(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j+1); sw=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j-1); se=*(pData+WIDTHBYTES(Width*8)*(Height-i-2)+j+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) { //查表,如果符合条件,将边界点修改为图像的背景; *(pData+WIDTHBYTES(Width*8)*(Height-i-1)+j)=(BYTE)255; Finished=FALSE;//再次进行扫描; i++; } } } } } } return TRUE; } |
(a) |
(b) |
|
|
|
|