OpenCV+python:Canny边缘检测算法
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;i<ColorImage->height;i++)
14 {
15 for (int j=0;j<ColorImage->width;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<nWindowSize;i++){for (int j=0;j<nWindowSize;j++){double n_Disx=i-nCenter;//水平方向距離中心像素距離double n_Disy=j-nCenter;pKernel_2[j*nWindowSize+i]=exp(-0.5*(n_Disx*n_Disx+n_Disy*n_Disy)/(nSigma*nSigma))/(2.0*3.1415926)*nSigma*nSigma; d_sum=d_sum+pKernel_2[j*nWindowSize+i];}}for(int i=0;i<nWindowSize;i++)//歸一化處理{for (int j=0;j<nWindowSize;j++){pKernel_2[j*nWindowSize+i]=pKernel_2[j*nWindowSize+i]/d_sum;}}//輸出模板for (int i=0;i<nWindowSize*nWindowSize;i++){if (i%(nWindowSize)==0){printf("\n");}printf("%.10f ",pKernel_2[i]);}//濾波處理for (int s=0;s<nWidth;s++){for (int t=0;t<nHeight;t++){double dFilter=0;double dSum=0;//當前坐標(s,t)//獲取8鄰域for (int x=-nCenter;x<=nCenter;x++){for (int y=-nCenter;y<=nCenter;y++){if ((x+s>=0)&&(x+s<nWidth)&&(y+t>=0)&&(y+t<nHeight))//判斷是否越界{double currentvalue=(double)OpenCvGrayImage->imageData[(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;iw<nWidth-1;iw++)
15 {
16 for (int jh=0;jh<nHeight-1;jh++)
17 {
18 P[jh*nWidth+iw]=(double)(pCanny->imageData[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<nWidth-1;iw++)
26 {
27 for (int jh=0;jh<nHeight-1;jh++)
28 {
29 M[jh*nWidth+iw]=(int)sqrt(P[jh*nWidth+iw]*P[jh*nWidth+iw]+Q[jh*nWidth+iw]*Q[jh*nWidth+iw]+0.5);
30 Theta[jh*nWidth+iw]=atan2(Q[jh*nWidth+iw],P[jh*nWidth+iw])*180/3.1415;
31 if (Theta[jh*nWidth+iw]<0)
32 {
33 Theta[jh*nWidth+iw]=360+Theta[jh*nWidth+iw];
34 }
35 }
36 }
(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;i<nWidth-1;i++)
13 {
14 for(int j=1;j<nHeight-1;j++)
15 {
16 //如果當前點梯度為0,該點就不是邊緣點
17 if (M[i+j*nWidth]==0)
18 {
19 N->imageData[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;wx<nWidth;wx++)
12 {
13 for (int hy=0;hy<nHeight;hy++)
14 {
15 if((uchar)N->imageData[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<nMaxMag-1))
41 {
42 count++;
43 nEdgeNum+=nHist[count];
44 }
45 nThrHigh=count;
46 nThrLow= (int)(nThrHigh*dRateLow+0.5);
47 printf("\n直方圖的長度 %d \n ",nMaxMag);
48 printf("\n梯度的閾值幅值大 %d 小閾值 %d \n ",nThrHigh,nThrLow);
(6)抑制孤立低閾值點
到目前為止,被劃分為強邊緣的像素點已經被確定為邊緣,因為它們是從圖像中的真實邊緣中提取出來的。然而,對于弱邊緣像素,將會有一些爭論,因為這些像素可以從真實邊緣提取也可以是因噪聲或顏色變化引起的。為了獲得準確的結果,應該抑制由后者引起的弱邊緣。通常,由真實邊緣引起的弱邊緣像素將連接到強邊緣像素,而噪聲響應未連接。為了跟蹤邊緣連接,通過查看弱邊緣像素及其8個鄰域像素,只要其中一個為強邊緣像素,則該弱邊緣點就可以保留為真實的邊緣。
抑制孤立邊緣點的偽代碼描述如下:
源代碼:
第六步:邊緣檢測3 /4 5 for(int is=1;is<nWidth-1;is++)6 {7 for (int jt=1;jt<nHeight-1;jt++)8 {9 //CvScalar s=cvGet2D(N,jt,is);
10 //int currentvalue=s.val[0];
11 int currentvalue=(uchar)(N->imageData[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;si<nWidth-1;si++)
22 {
23 for (int tj=1;tj<nHeight-1;tj++)
24 {
25 if ((uchar)N->imageData[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 npdef edge_demo(image):blurred = cv.GaussianBlur(image, (3, 3), 0)#高斯模糊gray = cv.cvtColor(blurred, cv.COLOR_BGR2GRAY) #灰度轉換# X Gradientxgrad = cv.Sobel(gray, cv.CV_16SC1, 1, 0)#計算梯度,(API要求不能為浮點數)# Y Gradientygrad = cv.Sobel(gray, cv.CV_16SC1, 0, 1) #edgeedge_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()
運行結果:
總結
以上是生活随笔為你收集整理的OpenCV+python:Canny边缘检测算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求一个恨自己的个性签名
- 下一篇: 求问图上的电影叫啥名字?