不同图像灰度不同,边界处一般会有明显的边缘,利用此特征可以分割图像。需要说明的是:边缘和物体间的边界并不等同,边缘指的是图像中像素的值有突变的地方,而物体间的边界指的是现实场景中的存在于物体之间的边界。有可能有边缘的地方并非边界,也有可能边界的地方并无边缘,因为现实世界中的物体是三维的,而图像只具有二维信息,从三维到二维的投影成像不可避免的会丢失一部分信息;另外,成像过程中的光照和噪声也是不可避免的重要因素。正是因为这些原因,基于边缘的图像分割仍然是当前图像研究中的世界级难题,目前研究者正在试图在边缘提取中加入高层的语义信息。
在实际的图像分割中,往往只用到一阶和二阶导数,虽然,原理上,可以用更高阶的导数,但是,因为噪声的影响,在纯粹二阶的导数操作中就会出现对噪声的敏感现象,三阶以上的导数信息往往失去了应用价值。二阶导数还可以说明灰度突变的类型。在有些情况下,如灰度变化均匀的图像,只利用一阶导数可能找不到边界,此时二阶导数就能提供很有用的信息。二阶导数对噪声也比较敏感,解决的方法是先对图像进行平滑滤波,消除部分噪声,再进行边缘检测。不过,利用二阶导数信息的算法是基于过零检测的,因此得到的边缘点数比较少,有利于后继的处理和识别工作。
各种算子的存在就是对这种导数分割原理进行的实例化计算,是为了在计算过程中直接使用的一种计算单位。
Roberts算子:边缘定位准,但是对噪声敏感。适用于边缘明显且噪声较少的图像分割。Roberts边缘检测算子是一种利用局部差分算子寻找边缘的算子,Robert算子图像处理后结果边缘不是很平滑。经分析,由于Robert算子通常会在图像边缘附近的区域内产生较宽的响应,故采用上述算子检测的边缘图像常需做细化处理,边缘定位的精度不是很高。
Prewitt算子:对噪声有抑制作用,抑制噪声的原理是通过像素平均,但是像素平均相当于对图像的低通滤波,所以Prewitt算子对边缘的定位不如Roberts算子。
Sobel算子:Sobel算子和Prewitt算子都是加权平均,但是Sobel算子认为,邻域的像素对当前像素产生的影响不是等价的,所以距离不同的像素具有不同的权值,对算子结果产生的影响也不同。一般来说,距离越远,产生的影响越小。
Isotropic Sobel算子:加权平均算子,权值反比于邻点与中心点的距离,当沿不同方向检测边缘时梯度幅度一致,就是通常所说的各向同性。
在边沿检测中,常用的一种模板是Sobel 算子。Sobel 算子有两个,一个是检测水平边沿的;另一个是检测垂直平边沿的 。Sobel算子另一种形式是各向同性Sobel(Isotropic Sobel)算子,也有两个,一个是检测水平边沿的 ,另一个是检测垂直平边沿的 。各向同性Sobel算子和普通Sobel算子相比,它的位置加权系数更为准确,在检测不同方向的边沿时梯度的幅度一致。由于建筑物图像的特殊性,我们可以发现,处理该类型图像轮廓时,并不需要对梯度方向进行运算,所以程序并没有给出各向同性Sobel算子的处理方法。
由于Sobel算子是滤波算子的形式,用于提取边缘,可以利用快速卷积函数,简单有效,因此应用广泛。美中不足的是,Sobel算子并没有将图像的主体与背景严格地区分开来,换言之就是Sobel算子没有基于图像灰度进行处理,由于Sobel算子没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人满意。在观测一幅图像的时候,我们往往首先注意的是图像与背景不同的部分,正是这个部分将主体突出显示,基于该理论,我们可以给出阈值化轮廓提取算法,该算法已在数学上证明当像素点满足正态分布时所求解是最优的。
上面的算子是利用一阶导数的信息,属于梯度算子范畴。
Laplacian算子:这是二阶微分算子。其具有各向同性,即与坐标轴方向无关,坐标轴旋转后梯度结果不变。但是,其对噪声比较敏感,所以,图像一般先经过平滑处理,因为平滑处理也是用模板进行的,所以,通常的分割算法都是把Laplacian算子和平滑算子结合起来生成一个新的模板。
Laplacian算子一般不以其原始形式用于边缘检测,因为其作为一个二阶导数,Laplacian算子对噪声具有无法接受的敏感性;同时其幅值产生算边缘,这是复杂的分割不希望有的结果;最后Laplacian算子不能检测边缘的方向;所以Laplacian在分割中所起的作用包括:(1)利用它的零交叉性质进行边缘定位;(2)确定一个像素是在一条边缘暗的一面还是亮的一面;一般使用的是高斯型拉普拉斯算子(Laplacian of a Gaussian,LoG),由于二阶导数是线性运算,利用LoG卷积一幅图像与首先使用高斯型平滑函数卷积改图像,然后计算所得结果的拉普拉斯是一样的。所以在LoG公式中使用高斯函数的目的就是对图像进行平滑处理,使用Laplacian算子的目的是提供一幅用零交叉确定边缘位置的图像;图像的平滑处理减少了噪声的影响并且它的主要作用还是抵消由Laplacian算子的二阶导数引起的逐渐增加的噪声影响。
微分算子在图像处理中扮演重要的角色,其算法实现简单,而且边缘检测的效果又较好,因此这些基本的微分算子是学习图像处理过程中的必备方法,下面着重讨论几种常见的微分算子。
1.Sobel
其主要用于边缘检测,在技术上它是以离散型的差分算子,用来运算图像亮度函数的梯度的近似值,缺点是Sobel算子并没有将图像的主题与背景严格地区分开来,换言之就是Sobel算子并没有基于图像灰度进行处理,由于Sobel算子并没有严格地模拟人的视觉生理特征,所以提取的图像轮廓有时并不能令人满意,算法具体实现很简单,就是3*3的两个不同方向上的模板运算,这里不再写出。
2.Robert算子
根据任一相互垂直方向上的差分都用来估计梯度,Robert算子采用对角方向相邻像素之差
3.Prewitt算子
该算子与Sobel算子类似,只是权值有所变化,但两者实现起来功能还是有差距的,据经验得知Sobel要比Prewitt更能准确检测图像边缘。
4.Laplacian算子
拉普拉斯算子是一种二阶微分算子,若只考虑边缘点的位置而不考虑周围的灰度差时可用该算子进行检测。对于阶跃状边缘,其二阶导数在边缘点出现零交叉,并且边缘点两旁的像素的二阶导数异号。
5.Canny算子
该算子功能比前面几种都要好,但是它实现起来较为麻烦,Canny算子是一个具有滤波,增强,检测的多阶段的优化算子,在进行处理前,Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny分割算法采用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中,Canny算子还将经过一个非极大值抑制的过程,最后Canny算子还采用两个阈值来连接边缘。
下面算法是基于的算法不可能直接运行,只是把Canny的具体实现步骤写了出来,若需用还要自己写。
该算子具体实现方法:
// anny.cpp: implementation of the Canny class. // ////////////////////////////////////////////////////////////////////// #include "anny.h" #include "math.h" //#include "algorithms.h" //#include "algorithm.h" #include "stdlib.h" //#include "maths.h" //using namespace std; ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// Canny::Canny(int PicHeight,int PicWidth,double ** PicData,double PicSigma,double PicRatioLow,double PicRatioHigh) { iHeight=PicHeight; iWidth=PicWidth; iData=PicData; sigma=PicSigma; dRatioLow=PicRatioLow; dRatioHigh=PicRatioHigh; } Canny::~Canny() { } void Canny::CannyArith(int **iEdgePoint) { int i; int **iGradX ; // 指向x方向导数的指针 int **iGradY ; // 指向y方向导数的指针 int **iExtent ; // 梯度的幅度 iGradX=new int *[iHeight]; for(i=0;i iGradX[i]=new int[iWidth]; iGradY=new int *[iHeight]; for(i=0;i iGradY[i]=new int[iWidth]; iExtent=new int *[iHeight]; for(i=0;i iExtent[i]=new int[iWidth]; // 对原图象进行滤波 GaussionSmooth(); // 计算X,Y方向上的方向导数 DirGrad(iGradX,iGradY); // 计算梯度的幅度 GradExtent(iGradX,iGradY,iExtent); // 应用non-maximum 抑制 NonMaxSuppress(iExtent,iGradX,iGradY,iEdgePoint); // 应用Hysteresis,找到所有的边界 Hysteresis(iExtent,iEdgePoint); // 释放内存 for(i=0;i delete []*(iGradX+i); delete iGradX; for(i=0;i delete []*(iGradY+i); delete iGradY; for(i=0;i delete []*(iExtent+i); delete iExtent; } void Canny::GaussionSmooth() { int i,j,k; //循环变量 int iWindowSize; //记录模板大小的变量 int iHalfLen; //模板大小的一半 double *pdKernel; //模板各点的权值 double dDotMul; //模板与对应像素点的卷积和 double dWeightSum; //模板的权值累加和 double **dTemp; //记录图像数据的中间变量 //开辟空间 dTemp=new double *[iHeight]; for(i=0;i dTemp[i]=new double[iWidth]; //获得模板长度和模板的各个权值 MakeGauss(&pdKernel,&iWindowSize); //得到模板的一半长度 iHalfLen=iWindowSize/2; //对图像对水方向根据模板进行平滑 for(i=0;i { for(j=0;j { dDotMul=0; dWeightSum=0; for(k=(-iHalfLen);k<=iHalfLen;k++) { if((k+j>=0)&&(k+j { dDotMul+=iData[i][j+k]*pdKernel[k+iHalfLen]; dWeightSum+=pdKernel[k+iHalfLen]; } } dTemp[i][j]=dDotMul/dWeightSum; } } //对图像垂直方向上根据模板的转置进行平滑(注意图像数据是在水平平滑之后进行的) for(i=0;i { for(j=0;j { dDotMul=0; dWeightSum=0; for(k=(-iHalfLen);k<=iHalfLen;k++) { if((k+j>=0)&&(k+j { dDotMul+=dTemp[j+k][i]*pdKernel[k+iHalfLen]; dWeightSum+=pdKernel[k+iHalfLen]; } } iData[j][i]=dDotMul/dWeightSum; } } //空间释放 delete []pdKernel; pdKernel=NULL; for(i=0;i delete []*(dTemp+i); delete dTemp; } void Canny::MakeGauss(double **pdKernel,int *iWindowSize) { int i; //循环变量 int nCenter; //确定高斯模板的一半长度 double dDistance; //一维高斯模板各点离中心点的距离 double PI=3.1415926; //圆周率 double dValue; //中间变量,记录高斯模板各点的权值(未经归一化) double dSum=0; //中间变量,记录高斯模板各点权值的总和 *iWindowSize=int(1+2*int(3*sigma+0.5)); //确定一维高斯模板长度,根据概率论的知识,选取[-3*sigma, 3*sigma]以内的数据。 nCenter=(*iWindowSize)/2; //得到一半长度 *pdKernel=new double[*iWindowSize];//开辟记录各点权值的空间 //利用高斯分布函数(正太分布)确定各点的权值,主要是根据高斯分布离中心点的距离越远,所取的值就越小,这与图像有些 //相似,离中心点越远,对中心点的影响就越小。 for(i=0;i<(*iWindowSize);i++) { dDistance=double(i-nCenter); //高斯分布函数求值 dValue=exp((-1/2)*dDistance*dDistance/(sigma*sigma))/(sqrt(2*PI)*sigma); (*pdKernel)[i]=dValue; dSum+=dValue; } //归一化(因为要不改变原图像的灰度区域,就必须保证各权值之和为1 for(i=0;i<(*iWindowSize);i++) { (*pdKernel)[i] /= dSum; } } void Canny::DirGrad(int **iGradX,int **iGradY) { int i,j,temp1,temp2; //水平方向的方向导数(下面都是用min和max对边界值做了相应的处理) for(i=0;i { for(j=0;j { if(iWidth-1 temp1=iWidth-1; else temp1=j+1; if(0 temp2=j-1; else temp2=0; iGradX[i][j]=int(iData[i][temp1]-iData[i][temp2]); } } //垂直方向的方向导数 for(i=0;i { for(j=0;j { if(iHeight-1 temp1=iHeight-1; else temp1=j+1; if(0 temp2=j-1; else temp2=0; iGradY[j][i]=int(iData[temp1][i]-iData[temp2][i]); } } } void Canny::GradExtent(int **iGradX,int **iGradY,int **iExtent) { int i,j; double iTemp1,iTemp2; for(i=0;i { for(j=0;j { iTemp1=iGradX[i][j]*iGradX[i][j]; iTemp2=iGradY[i][j]*iGradY[i][j]; iExtent[i][j]=int(sqrt(iTemp1+iTemp2)+0.5); } } } void Canny::NonMaxSuppress(int **iExtent,int **iGradX,int **iGradY,int **dUnchRst) { int i,j; int gx,gy; //记录像素点X,Y 方向的方向导数值 int g1,g2,g3,g4; //各个领域的梯度值 double weight; //比重 double dTemp1,dTemp2,dTemp; //中间变量 //处理边缘值(边缘点不可能是边界点 for(i=0;i { dUnchRst[i][0]=0; dUnchRst[i][iWidth-1]=0; } for(j=0;j { dUnchRst[0][j]=0; dUnchRst[iHeight-1][j]=0; } //标记有可能是边界点的像素点 for(i=1;i { for(j=1;j { //梯度值是0的像素点不可能是边界点 if(iExtent[i][j]==0) dUnchRst[i][j]=0; else { dTemp=iExtent[i][j]; gx=iGradX[i][j]; gy=iGradY[i][j]; //下面都是判断当前像素点的梯度值和其领域像素点的梯度值,如大于就有可能是边界点,如小于就不可能是边界点 if(abs(gy)>abs(gx)) { weight=double(abs(gx)/abs(gy)); g2=iExtent[i-1][j]; g4=iExtent[i+1][j]; if(gx*gy>0) { g1=iExtent[i-1][j-1]; g3=iExtent[i+1][j+1]; } else { g1=iExtent[i-1][j+1]; g3=iExtent[i+1][j-1]; } } else { weight=double(abs(gy)/abs(gx)); g2=iExtent[i][j+1]; g4=iExtent[i][j-1]; if(gx*gy>0) { g1=iExtent[i+1][j+1]; g3=iExtent[i-1][j-1]; } else { g1=iExtent[i-1][j+1]; g3=iExtent[i+1][j-1]; } } dTemp1=weight*g1+(1-weight)*g2; dTemp2=weight*g3+(1-weight)*g4; //当大于的时候就有可能是边界点 if(dTemp>=dTemp1&&dTemp>=dTemp2) { dUnchRst[i][j] = 128 ; } else { dUnchRst[i][j]=0 ; } } } } } void Canny::Hysteresis(int **iExtent,int **iEdgePoint) { int i,j; int iThreHigh; int iThreLow; SetThreshold(iExtent,&iThreHigh,&iThreLow,iEdgePoint); for(i=0;i { for(j=0;j { if((iEdgePoint[i][j]==128)&&(iExtent[i][j]>=iThreHigh)) { iEdgePoint[i][j]=255; TraceEdge(i,j,iThreLow,iEdgePoint,iExtent); } } } // 那些还没有被设置为边界点的象素已经不可能成为边界点 for(i=0;i { for(j=0;j { if(iEdgePoint[i][j]!=255) { // 设置为非边界点 iEdgePoint[i][j] = 0 ; } } } } void Canny::SetThreshold(int **iExtent,int *iThreHigh,int *iThreLow,int **iEdgePoint) { int i,j,k; int GradHist[1024]; //统计梯度直方图的数据,梯度最大值不可能超过1024 int iEdgeNum; //边界点的数量 int iGradMax=0; //边界点的梯度最大值 int iHighCount; //根据iRatioHigh小于高阈值像素的个数 //初始化 for(i=0;i<1024;i++) GradHist[i]=0; //梯度直方图统计 for(i=0;i { for(j=0;j { if(iEdgePoint[i][j]==128) { GradHist[iExtent[i][j]]++; } } } iEdgeNum=0; //找出最大梯度和统计边界点的个数 for(i=0;i<1024;i++) { if(GradHist[i]!=0) iGradMax=i; iEdgeNum+=GradHist[i]; } //获得小于高阈值的个数 iHighCount=int(iEdgeNum*dRatioHigh+0.5); k=1; iEdgeNum=GradHist[1]; //求出高阈值 while((k<=(iGradMax-1))&&(iEdgeNum { k++; iEdgeNum+=GradHist[k]; } *iThreHigh=k; //根据高阈值和比例关系求得低阈值 *iThreLow=int((*iThreHigh)*dRatioLow+0.5); } void Canny::TraceEdge(int y,int x,int iThreLow,int **iEdgePoint,int **iExtent) { // 对8邻域象素进行查询 int xNb[8] = {1, 1, 0,-1,-1,-1, 0, 1} ; int yNb[8] = {0, 1, 1, 1,0 ,-1,-1,-1} ; int yy ; int xx ; int k ; for(k=0;k<8;k++) { yy=y+yNb[k] ; xx=x+xNb[k] ; // 如果该象素为可能的边界点,又没有处理过, 并且梯度大于阈值 if(iEdgePoint[yy][xx]==128&&iExtent[yy][xx]>=iThreLow) { // 把该点设置成为边界点 iEdgePoint[yy][xx]=255 ; // 以该点为中心进行跟踪 //TraceEdge(yy,xx,iThreLow,iEdgePoint,iExtent); } } }