1,边缘处理
图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波。我们知道微分运算是求信号的变化率,具有加强高频分量的作用。
在空域运算中来说,对图像的锐化就是计算微分。由于数字图像的离散信号,微分运算就变成计算差分或梯度。
图像处理中有多种边缘检测(梯度)算子,常用的包括普通一阶差分,Robert算子(交叉差分),Sobel算子等等,是基于寻找梯度强度。拉普拉斯算子(二阶差分)是基于过零点检测。通过计算梯度,设置阀值,得到边缘图像。
2,Canny边缘检测算法简介
Canny边缘检测算子是一种多级检测算法。1986年由John F. Canny提出,同时提出了边缘检测的三大准则:
(1) 低错误率的边缘检测:检测算法应该精确地找到图像中的尽可能多的边缘,尽可能的减少漏检和误检。
(2) 最优定位:检测的边缘点应该精确地定位于边缘的中心。
(3)图像中的任意边缘应该只被标记一次,同时图像噪声不应产生伪边缘。
为了满足这些要求,Canny使用了变分法。Canny检测器中的最优函数使用四个指数项的和来描述,它可以由高斯函数的一阶导数来近似。
3,Canny边缘检测算法的处理流程
(1)灰度转换:
该部分是按照Canny算法通常处理的图像为灰度图,如果获取的彩色图像,那首先就得进行灰度化。以RGB格式的彩图为例,通常灰度化采用的公式是:
Gray=0.299R+0.587G+0.114B
在OpenCV中也提供相关的API(cvtColor)
源代码:
///第一步:灰度化
2 IplImage * ColorImage=cvLoadImage("c:\\photo.bmp",1);
3 if (ColorImage==NULL)
4 {
5 printf("image read error");
6 return 0;
7 }
8 cvNamedWindow("Sourceimg",0);
9 cvShowImage("Sourceimg",ColorImage); //
10 IplImage * OpenCvGrayImage;
11 OpenCvGrayImage=cvCreateImage(cvGetSize(ColorImage),ColorImage->depth,1);
12 float data1,data2,data3;
13 for (int i=0;iheight;i++)
14 {
15 for (int j=0;jwidth;j++)
16 {
17 data1=(uchar)(ColorImage->imageData[i*ColorImage->widthStep+j*3+0]);
18 data2=(uchar)(ColorImage->imageData[i*ColorImage->widthStep+j*3+1]);
19 data3=(uchar)(ColorImage->imageData[i*ColorImage->widthStep+j*3+2]);
20 OpenCvGrayImage->imageData[i*OpenCvGrayImage->widthStep+j]=(uchar)(0.07*data1 + 0.71*data2 + 0.21*data3);
21 }
22 }
23 cvNamedWindow("GrayImage",0);
24 cvShowImage("GrayImage",OpenCvGrayImage); //显示灰度图
25 cvWaitKey(0);
26 cvDestroyWindow("GrayImage");
(2) 使用高斯滤波器,以平滑图像,滤除噪声(高斯模糊):
了尽可能减少噪声对边缘检测结果的影响,所以必须滤除噪声以防止由噪声引起的错误检测。为了平滑图像,使用高斯滤波器与图像进行卷积,该步骤将平滑图像,以减少边缘检测器上明显的噪声影响。大小为(2k+1)x(2k+1)的高斯滤波器核的生成方程式由下式给出:
下面是一个sigma = 1.4,尺寸为3x3的高斯卷积核的例子(需要注意归一化):
若图像中一个3x3的窗口为A,要滤波的像素点为e,则经过高斯滤波之后,像素点e的亮度值为:
其中*为卷积符号,sum表示矩阵中所有元素相加求和。重要的是需要理解,高斯卷积核大小的选择将影响Canny检测器的性能。尺寸越大,检测器对噪声的敏感度越低,但是边缘检测的定位误差也将略有增加。一般5x5是一个比较不错的trade off。
源代码:
///////第二步:高斯滤波
///////
double nSigma=0.2;
int nWindowSize=1+2*ceil(3*nSigma);//通过sigma得到窗口大小
int nCenter=nWindowSize/2;
int nWidth=OpenCvGrayImage->width;
int nHeight=OpenCvGrayImage->height;
IplImage * pCanny;
pCanny=cvCreateImage(cvGetSize(ColorImage),ColorImage->depth,1);
//生成二维滤波核
double *pKernel_2=new double[nWindowSize*nWindowSize];
double d_sum=0.0;
for(int i=0;i=0)&&(x+s=0)&&(y+timageData[(y+t)*OpenCvGrayImage->widthStep+x+s];
dFilter+=currentvalue*pKernel_2[x+nCenter+(y+nCenter)*nCenter];
dSum+=pKernel_2[x+nCenter+(y+nCenter)*nCenter];
}
}
}
pCanny->imageData[t*pCanny->widthStep+s]=(uchar)(dFilter/dSum);
}
}
cvNamedWindow("GaussImage",0);
cvShowImage("GaussImage",pCanny); //显示高斯图
cvWaitKey(0);
cvDestroyWindow("GaussImage");
(3)计算梯度强度和方向:
图像中的边缘可以指向各个方向,因此Canny算法使用四个算子来检测图像中的水平、垂直和对角边缘。边缘检测的算子(如Roberts,Prewitt,Sobel等)返回水平Gx和垂直Gy方向的一阶导数值,由此便可以确定像素点的梯度G和方向theta 。
其中G为梯度强度, theta表示梯度方向,arctan为反正切函数。具体参见《OpenCV+python:图像梯度》一文。
源代码:
第三步:计算梯度值和方向
3 //////////////////同样可以用不同的检测器////////////////加上把图像放到0-255之间//////
4 ///// P[i,j]=(S[i+1,j]-S[i,j]+S[i+1,j+1]-S[i,j+1])/2 /////
5 ///// Q[i,j]=(S[i,j]-S[i,j+1]+S[i+1,j]-S[i+1,j+1])/2 /////
6 /////////////////////////////////////////////////////////////////
7 double *P=new double[nWidth*nHeight];
8 double *Q=new double[nWidth*nHeight];
9 int *M=new int[nWidth*nHeight];
10 //IplImage * M;//梯度结果
11 //M=cvCreateImage(cvGetSize(ColorImage),ColorImage->depth,1);
12 double *Theta=new double[nWidth*nHeight];
13 int nwidthstep=pCanny->widthStep;
14 for(int iw=0;iwimageData[min(iw+1,nWidth-1)+jh*nwidthstep]-pCanny->imageData[iw+jh*nwidthstep]+
19 pCanny->imageData[min(iw+1,nWidth-1)+min(jh+1,nHeight-1)*nwidthstep]-pCanny->imageData[iw+min(jh+1,nHeight-1)*nwidthstep])/2;
20 Q[jh*nWidth+iw]=(double)(pCanny->imageData[iw+jh*nwidthstep]-pCanny->imageData[iw+min(jh+1,nHeight-1)*nwidthstep]+
21 pCanny->imageData[min(iw+1,nWidth-1)+jh*nwidthstep]-pCanny->imageData[min(iw+1,nWidth-1)+min(jh+1,nHeight-1)*nwidthstep])/2;
22 }
23 }
24 //计算幅值和方向
25 for(int iw=0;iw
(4) 非极大值抑制:
非极大值抑制是一种边缘稀疏技术,通俗意义上是指寻找像素点局部最大值,非极大值抑制的作用在于“瘦”边。对图像进行梯度计算后,仅仅基于梯度值提取的边缘仍然很模糊。对于标准3,对边缘有且应当只有一个准确的响应。而非极大值抑制则可以帮助将局部最大值之外的所有梯度值抑制为0,对梯度图像中每个像素进行非极大值抑制的算法是:
上图中左右图:g1、g2、g3、g4都代表像素点,很明显它们是c的八领域中的4个,左图中c点是我们需要判断的点,蓝色的直线是它的梯度方向,也就是说c要是局部极大值,它的梯度幅值M需要大于直线与g1g2和g2g3的交点,dtmp1和dtmp2处的梯度幅值。
但是dtmp1和dtmp2不是整像素,而是亚像素,也就是坐标是浮点的,那怎么求它们的梯度幅值呢?线性插值,例如dtmp1在g1、g2之间,g1、g2的幅值都知道,我们只要知道dtmp1在g1、g2之间的比例,就能得到它的梯度幅值,而比例是可以靠夹角计算出来的,夹角又是梯度的方向。
写个线性插值的公式:设g1的幅值M(g1),g2的幅值M(g2),则dtmp1可以很得到:
M(dtmp1)=w*M(g2)+(1-w)*M(g1)
其中w=distance(dtmp1,g2)/distance(g1,g2)
distance(g1,g2) 表示两点之间的距离。实际上w是一个比例系数,这个比例系数可以通过梯度方向(幅角的正切和余切)得到。
右边图中的4个直线就是4个不同的情况,情况不同,g1、g2、g3、g4代表c的八领域中的4个坐标会有所差异,但是线性插值的原理都是一致的。
需要注意的是,如何标志方向并不重要,重要的是梯度方向的计算要和梯度算子的选取保持一致。
源代码:
////////第四步:非极大值抑制
2 //注意事项 权重的选取,也就是离得近权重大
3 /////////////////////////////////////////////////////////////////
4 IplImage * N;//非极大值抑制结果
5 N=cvCreateImage(cvGetSize(ColorImage),ColorImage->depth,1);
6 IplImage * OpencvCannyimg;//非极大值抑制结果
7 OpencvCannyimg=cvCreateImage(cvGetSize(ColorImage),ColorImage->depth,1);
8 int g1=0, g2=0, g3=0, g4=0; //用于进行插值,得到亚像素点坐标值
9 double dTmp1=0.0, dTmp2=0.0; //保存两个亚像素点插值得到的灰度数据
10 double dWeight=0.0; //插值的权重
11
12 for(int i=1;iimageData[i+j*nwidthstep]=0;
20 }else
21 {
22 ////////首先判断属于那种情况,然后根据情况插值///////
23 ////////////////////第一种情况///////////////////////
24 ///////// g1 g2 /////////////
25 ///////// C /////////////
26 ///////// g3 g4 /////////////
27 /////////////////////////////////////////////////////
28 if((Theta[i+j*nWidth]>=90&&Theta[i+j*nWidth]<135)||(Theta[i+j*nWidth]>=270&&Theta[i+j*nWidth]<315))
29 {
30 g1=M[i-1+(j-1)*nWidth];
31 g2=M[i+(j-1)*nWidth];
32 g3=M[i+(j+1)*nWidth];
33 g4=M[i+1+(j+1)*nWidth];
34 dWeight=fabs(P[i+j*nWidth])/fabs(Q[i+j*nWidth]);
35 dTmp1=g1*dWeight+(1-dWeight)*g2;
36 dTmp2=g4*dWeight+(1-dWeight)*g3;
37 ////////////////////第二种情况///////////////////////
38 ///////// g1 /////////////
39 ///////// g2 C g3 /////////////
40 ///////// g4 /////////////
41 /////////////////////////////////////////////////////
42 }else if((Theta[i+j*nWidth]>=135&&Theta[i+j*nWidth]<180)||(Theta[i+j*nWidth]>=315&&Theta[i+j*nWidth]<360))
43 {
44 g1=M[i-1+(j-1)*nWidth];
45 g2=M[i-1+(j)*nWidth];
46 g3=M[i+1+(j)*nWidth];
47 g4=M[i+1+(j+1)*nWidth];
48 dWeight=fabs(Q[i+j*nWidth])/fabs(P[i+j*nWidth]);
49 dTmp1=g1*dWeight+(1-dWeight)*g2;
50 dTmp2=g4*dWeight+(1-dWeight)*g3;
51 ////////////////////第三种情况///////////////////////
52 ///////// g1 g2 /////////////
53 ///////// C /////////////
54 ///////// g4 g3 /////////////
55 /////////////////////////////////////////////////////
56 }else if((Theta[i+j*nWidth]>=45&&Theta[i+j*nWidth]<90)||(Theta[i+j*nWidth]>=225&&Theta[i+j*nWidth]<270))
57 {
58 g1=M[i+1+(j-1)*nWidth];
59 g2=M[i+(j-1)*nWidth];
60 g3=M[i+(j+1)*nWidth];
61 g4=M[i-1+(j+1)*nWidth];
62 dWeight=fabs(P[i+j*nWidth])/fabs(Q[i+j*nWidth]);
63 dTmp1=g1*dWeight+(1-dWeight)*g2;
64 dTmp2=g4*dWeight+(1-dWeight)*g3;
65 ////////////////////第四种情况///////////////////////
66 ///////// g1 /////////////
67 ///////// g4 C g2 /////////////
68 ///////// g3 /////////////
69 /////////////////////////////////////////////////////
70 }else if((Theta[i+j*nWidth]>=0&&Theta[i+j*nWidth]<45)||(Theta[i+j*nWidth]>=180&&Theta[i+j*nWidth]<225))
71 {
72 g1=M[i+1+(j-1)*nWidth];
73 g2=M[i+1+(j)*nWidth];
74 g3=M[i-1+(j)*nWidth];
75 g4=M[i-1+(j+1)*nWidth];
76 dWeight=fabs(Q[i+j*nWidth])/fabs(P[i+j*nWidth]);
77 dTmp1=g1*dWeight+(1-dWeight)*g2;
78 dTmp2=g4*dWeight+(1-dWeight)*g3;
79
80 }
81
82 }
83
84 if ((M[i+j*nWidth]>=dTmp1)&&(M[i+j*nWidth]>=dTmp2))
85 {
86 N->imageData[i+j*nwidthstep]=200;
87
88 }else N->imageData[i+j*nwidthstep]=0;
89
90 }
91 }
92
93
94 //cvNamedWindow("Limteimg",0);
95 //cvShowImage("Limteimg",N); //显示非抑制
96 //cvWaitKey(0);
97 //cvDestroyWindow("Limteimg");
(5) 双阈值检测
在施加非极大值抑制之后,剩余的像素可以更准确地表示图像中的实际边缘。然而,仍然存在由于噪声和颜色变化引起的一些边缘像素。为了解决这些杂散响应,必须用弱梯度值过滤边缘像素,并保留具有高梯度值的边缘像素,可以通过选择高低阈值来实现。如果边缘像素的梯度值高于高阈值,则将其标记为强边缘像素;如果边缘像素的梯度值小于高阈值并且大于低阈值,则将其标记为弱边缘像素;如果边缘像素的梯度值小于低阈值,则会被抑制。阈值的选择取决于给定输入图像的内容。
双阈值检测的伪代码描写如下:
源代码:
///////第五步:双阈值的选取
2 //注意事项 注意是梯度幅值的阈值
3 /////////////////////////////////////////////////////////////////
4 int nHist[1024];//直方图
5 int nEdgeNum;//所有边缘点的数目
6 int nMaxMag=0;//最大梯度的幅值
7 for(int k=0;k<1024;k++)
8 {
9 nHist[k]=0;
10 }
11 for (int wx=0;wximageData[wx+hy*N->widthStep]==200)
16 {
17 int Mindex=M[wx+hy*nWidth];
18 nHist[M[wx+hy*nWidth]]++;//获取了梯度直方图
19
20 }
21 }
22 }
23 nEdgeNum=0;
24 for (int index=1;index<1024;index++)
25 {
26 if (nHist[index]!=0)
27 {
28 nMaxMag=index;
29 }
30 nEdgeNum+=nHist[index];//经过non-maximum suppression后有多少边缘点像素
31 }
32 //计算两个阈值 注意是梯度的阈值
33 int nThrHigh;
34 int nThrLow;
35 double dRateHigh=0.7;
36 double dRateLow=0.5;
37 int nHightcount=(int)(dRateHigh*nEdgeNum+0.5);
38 int count=1;
39 nEdgeNum=nHist[1];
40 while((nEdgeNum<=nHightcount)&&(count
(6)抑制孤立低阈值点
到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。
抑制孤立边缘点的伪代码描述如下:
////////第六步:边缘检测
3 /////////////////////////////////////////////////////////////////
4
5 for(int is=1;isimageData[is+jt*N->widthStep]);
12 if ((currentvalue==200)&&(M[is+jt*nWidth]>=nThrHigh))
13 //是非最大抑制后的点且 梯度幅值要大于高阈值
14 {
15 N->imageData[is+jt*nwidthstep]=255;
16 //邻域点判断
17 TraceEdge(is, jt, nThrLow, N, M);
18 }
19 }
20 }
21 for (int si=1;siimageData[si+tj*nwidthstep]!=255)
26 {
27 N->imageData[si+tj*nwidthstep]=0;
28 }
29 }
30
31 }
32
33 cvNamedWindow("Cannyimg",0);
34 cvShowImage("Cannyimg",N);
其中,邻域跟踪算法给出了两个,一种是递归,一种是非递归。 递归算法。解决了堆栈溢出问题。
int TraceEdge(int w, int h, int nThrLow, IplImage* pResult, int *pMag)
2 {
3 int n,m;
4 char flag = 0;
5 int currentvalue=(uchar)pResult->imageData[w+h*pResult->widthStep];
6 if ( currentvalue== 0)
7 {
8 pResult->imageData[w+h*pResult->widthStep]= 255;
9 flag=0;
10 for (n= -1; n<=1; n++)
11 {
12 for(m= -1; m<=1; m++)
13 {
14 if (n==0 && m==0) continue;
15 int curgrayvalue=(uchar)pResult->imageData[w+n+(h+m)*pResult->widthStep];
16 int curgrdvalue=pMag[w+n+(h+m)*pResult->width];
17 if (curgrayvalue==200&&curgrdvalue>nThrLow)
18 if (TraceEdge(w+n, h+m, nThrLow, pResult, pMag))
19 {
20 flag=1;
21 break;
22 }
23 }
24 if (flag) break;
25 }
26 return(1);
27 }
28 return(0);
29 }
非递归算法。如下:
void TraceEdge(int w, int h, int nThrLow, IplImage* pResult, int *pMag)
2 {
3 //对8邻域像素进行查询
4 int xNum[8] = {1,1,0,-1,-1,-1,0,1};
5 int yNum[8] = {0,1,1,1,0,-1,-1,-1};
6 int xx=0;
7 int yy=0;
8 bool change=true;
9 while(change)
10 {
11 change=false;
12 for(int k=0;k<8;k++)
13 {
14 xx=w+xNum[k];
15 yy=h+yNum[k];
16 // 如果该象素为可能的边界点,又没有处理过
17 // 并且梯度大于阈值
18 int curgrayvalue=(uchar)pResult->imageData[xx+yy*pResult->widthStep];
19 int curgrdvalue=pMag[xx+yy*pResult->width];
20 if(curgrayvalue==200&&curgrdvalue>nThrLow)
21 {
22 change=true;
23 // 把该点设置成为边界点
24 pResult->imageData[xx+yy*pResult->widthStep]=255;
25 h=yy;
26 w=xx;
27 break;
28 }
29 }
30 }
31 }
到此,整个算法写完了,利用OpenCV的相关API进行处理的源代码如下:
import cv2 as cv
import numpy as np
def edge_demo(image):
blurred = cv.GaussianBlur(image, (3, 3), 0)#高斯模糊
gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) #灰度转换
# X Gradient
xgrad = cv.Sobel(gray, cv.CV_16SC1, 1, 0)#计算梯度,(API要求不能为浮点数)
# Y Gradient
ygrad = cv.Sobel(gray, cv.CV_16SC1, 0, 1)
#edge
edge_output = cv.Canny(xgrad, ygrad, 50, 150) #调用cv.Canny,利用高低阈值求出图像边缘
#edge_output = cv.Canny(gray, 50, 150)
cv.imshow("Canny Edge", edge_output)
dst = cv.bitwise_and(image, image, mask=edge_output)#得到彩色图像的边缘检测图像
cv.imshow("Color Edge", dst)
src = cv.imread("F:/images/lena.png")
cv.namedWindow("input image", cv.WINDOW_AUTOSIZE)
cv.imshow("input image", src)
edge_demo(src)
cv.waitKey(0)
cv.destroyAllWindows()