Canny邊緣檢測算法原理及其VC實現詳解(一)
http://blog.csdn.net/likezhaobin/article/details/6892176
圖象的邊緣是指圖象局部區域亮度變化顯著的部分,該區域的灰度剖面一般可以看作是一個階躍,既從一個灰度值在很小的緩衝區域內急劇變化到另一個灰度相差較大的灰度值。圖象的邊緣部分集中了圖象的大部分信息,圖象邊緣的確定與提取對於整個圖象場景的識別與理解是非常重要的,同時也是圖象分割所依賴的重要特徵,邊緣檢測主要是圖象的灰度變化的度量、檢測和定位,自從1959提出邊緣檢測以來,經過五十多年的發展,已有許多中不同的邊緣檢測方法。根據作者的理解和實踐,本文對邊緣檢測的原理進行了描述,在此基礎上着重對Canny檢測算法的實現進行詳述。
本文所述內容均由編程驗證而來,在實現過程中,有任何錯誤或者不足之處大家共同討論(本文不講述枯燥的理論證明和數學推導,僅僅從算法的實現以及改進上進行原理性和工程化的描述)。
1、邊緣檢測原理及步驟
在之前的博文中,作者從一維函數的躍變檢測開始,循序漸進的對二維圖像邊緣檢測的基本原理進行了通俗化的描述。結論是:實現圖像的邊緣檢測,就是要用離散化梯度逼近函數根據二維灰度矩陣梯度向量來尋找圖像灰度矩陣的灰度躍變位置,然後在圖像中將這些位置的點連起來就構成了所謂的圖像邊緣(圖像邊緣在這裏是一個統稱,包括了二維圖像上的邊緣、角點、紋理等基元圖)。
在實際情況中理想的灰度階躍及其線條邊緣圖像是很少見到的,同時大多數的傳感器件具有低頻濾波特性,這樣會使得階躍邊緣變爲斜坡性邊緣,看起來其中的強度變化不是瞬間的,而是跨越了一定的距離。這就使得在邊緣檢測中首先要進行的工作是濾波。
1)濾波:邊緣檢測的算法主要是基於圖像強度的一階和二階導數,但導數通常對噪聲很敏感,因此必須採用濾波器來改善與噪聲有關的邊緣檢測器的性能。常見的濾波方法主要有高斯濾波,即採用離散化的高斯函數產生一組歸一化的高斯核(具體見“高斯濾波原理及其編程離散化實現方法”一文),然後基於高斯核函數對圖像灰度矩陣的每一點進行加權求和(具體程序實現見下文)。
2)增強:增強邊緣的基礎是確定圖像各點鄰域強度的變化值。增強算法可以將圖像灰度點鄰域強度值有顯著變化的點凸顯出來。在具體編程實現時,可通過計算梯度幅值來確定。
3)檢測:經過增強的圖像,往往鄰域中有很多點的梯度值比較大,而在特定的應用中,這些點並不是我們要找的邊緣點,所以應該採用某種方法來對這些點進行取捨。實際工程中,常用的方法是通過閾值化方法來檢測。
2、Canny邊緣檢測算法原理
JohnCanny於1986年提出Canny算子,它與Marr(LoG)邊緣檢測方法類似,也屬於是先平滑後求導數的方法。本節對根據上述的邊緣檢測過程對Canny檢測算法的原理進行介紹。
2.1 對原始圖像進行灰度化
Canny算法通常處理的圖像爲灰度圖,因此如果攝像機獲取的是彩色圖像,那首先就得進行灰度化。對一幅彩色圖進行灰度化,就是根據圖像各個通道的採樣值進行加權平均。以RGB格式的彩圖爲例,通常灰度化採用的方法主要有:
方法1:Gray=(R+G+B)/3;
方法2:Gray=0.299R+0.587G+0.114B;(這種參數考慮到了人眼的生理特點)
注意1:至於其他格式的彩色圖像,可以根據相應的轉換關係轉爲RGB然後再進行灰度化;
注意2:在編程時要注意圖像格式中RGB的順序通常爲BGR。
2.2 對圖像進行高斯濾波
圖像高斯濾波的實現可以用兩個一維高斯核分別兩次加權實現,也可以通過一個二維高斯核一次卷積實現。
1)高斯覈實現
上式爲離散化的一維高斯函數,確定參數就可以得到一維核向量。
上式爲離散化的二維高斯函數,確定參數就可以得到二維核向量。
注意1:關於參數Sigma的取值詳見上篇博文。
注意2:在求的高斯核後,要對整個核進行歸一化處理。
2)圖像高斯濾波
對圖像進行高斯濾波,聽起來很玄乎,其實就是根據待濾波的像素點及其鄰域點的灰度值按照一定的參數規則進行加權平均。這樣可以有效濾去理想圖像中疊加的高頻噪聲。
通常濾波和邊緣檢測是矛盾的概念,抑制了噪聲會使得圖像邊緣模糊,這回增加邊緣定位的不確定性;而如果要提高邊緣檢測的靈敏度,同時對噪聲也提高了靈敏度。實際工程經驗表明,高斯函數確定的核可以在抗噪聲干擾和邊緣檢測精確定位之間提供較好的折衷方案。這就是所謂的高斯圖像濾波,具體實現代碼見下文。
2.3 用一階偏導的有限差分來計算梯度的幅值和方向
關於圖像灰度值得梯度可使用一階有限差分來進行近似,這樣就可以得圖像在x和y方向上偏導數的兩個矩陣。常用的梯度算子有如下幾種:
1)Roberts算子
上式爲其x和y方向偏導數計算模板,可用數學公式表達其每個點的梯度幅值爲:
2)Sobel算子
上式三個矩陣分別爲該算子的x向卷積模板、y向卷積模板以及待處理點的鄰域點標記矩陣,據此可用數學公式表達其每個點的梯度幅值爲:
3)Prewitt算子
和Sobel算子原理一樣,在此僅給出其卷積模板。
4)Canny算法所採用的方法
在本文實現的Canny算法中所採用的卷積算子比較簡單,表達如下: 其x向、y向的一階偏導數矩陣,梯度幅值以及梯度方向的數學表達式爲:
求出這幾個矩陣後,就可以進行下一步的檢測過程。
2.4 對梯度幅值進行非極大值抑制
圖像梯度幅值矩陣中的元素值越大,說明圖像中該點的梯度值越大,但這不不能說明該點就是邊緣(這僅僅是屬於圖像增強的過程)。在Canny算法中,非極大值抑制是進行邊緣檢測的重要步驟,通俗意義上是指尋找像素點局部最大值,將非極大值點所對應的灰度值置爲0,這樣可以剔除掉一大部分非邊緣的點(這是本人的理解)。圖1 非極大值抑制原理
根據圖1 可知,要進行非極大值抑制,就首先要確定像素點C的灰度值在其8值鄰域內是否爲最大。圖1中藍色的線條方向爲C點的梯度方向,這樣就可以確定其局部的最大值肯定分佈在這條線上,也即出了C點外,梯度方向的交點dTmp1和dTmp2這兩個點的值也可能會是局部最大值。因此,判斷C點灰度與這兩個點灰度大小即可判斷C點是否爲其鄰域內的局部最大灰度點。如果經過判斷,C點灰度值小於這兩個點中的任一個,那就說明C點不是局部極大值,那麼則可以排除C點爲邊緣。這就是非極大值抑制的工作原理。
作者認爲,在理解的過程中需要注意以下兩點:
1)中非最大抑制是回答這樣一個問題:“當前的梯度值在梯度方向上是一個局部最大值嗎?” 所以,要把當前位置的梯度值與梯度方向上兩側的梯度值進行比較;
2)梯度方向垂直於邊緣方向。
但實際上,我們只能得到C點鄰域的8個點的值,而dTmp1和dTmp2並不在其中,要得到這兩個值就需要對該兩個點兩端的已知灰度進行線性插值,也即根據圖1中的g1和g2對dTmp1進行插值,根據g3和g4對dTmp2進行插值,這要用到其梯度方向,這是上文Canny算法中要求解梯度方向矩陣Thita的原因。
完成非極大值抑制後,會得到一個二值圖像,非邊緣的點灰度值均爲0,可能爲邊緣的局部灰度極大值點可設置其灰度爲128。根據下文的具體測試圖像可以看出,這樣一個檢測結果還是包含了很多由噪聲及其他原因造成的假邊緣。因此還需要進一步的處理。2.5 用雙閾值算法檢測和連接邊緣
Canny算法中減少假邊緣數量的方法是採用雙閾值法。選擇兩個閾值(關於閾值的選取方法在擴展中進行討論),根據高閾值得到一個邊緣圖像,這樣一個圖像含有很少的假邊緣,但是由於閾值較高,產生的圖像邊緣可能不閉合,未解決這樣一個問題採用了另外一個低閾值。
在高閾值圖像中把邊緣鏈接成輪廓,當到達輪廓的端點時,該算法會在斷點的8鄰域點中尋找滿足低閾值的點,再根據此點收集新的邊緣,直到整個圖像邊緣閉合。
以上即爲整個Canny邊緣檢測算法的原理分析,接下來我們進行VC下的算法實現和效果分析。
3、 Canny算法的實現流程
由於本文主要目的在於學習和實現算法,而對於圖像讀取、視頻獲取等內容不進行闡述。因此選用OpenCV算法庫作爲其他功能的實現途徑(關於OpenCV的使用,作者將另文表述)。首先展現本文將要處理的彩色圖片。
圖2 待處理的圖像
3.1 圖像讀取和灰度化
編程時採用上文所描述的第二種方法來實現圖像的灰度化。其中ptr數組中保存的灰度化後的圖像數據。具體的灰度化後的效果如圖3所示。
- IplImage* ColorImage = cvLoadImage( "12.jpg", -1 ); //讀入圖像,獲取彩圖指針
- IplImage* OpenCvGrayImage; //定義變換後的灰度圖指針
- unsigned char* ptr; //指向圖像的數據首地址
- if (ColorImage == NULL)
- return;
- int i = ColorImage->width * ColorImage->height;
- BYTE data1; //中間過程變量
- BYTE data2;
- BYTE data3;
- ptr = new unsigned char[i];
- for(intj=0; j<ColorImage->height; j++) //對RGB加權平均,權值參考OpenCV
- {
- for(intx=0; x<ColorImage->width; x++)
- {
- data1 = (BYTE)ColorImage->imageData[j*ColorImage->widthStep + i*3]; //B分量
- data2 = (BYTE)ColorImage->imageData[j*ColorImage->widthStep + i*3 + 1]; //G分量
- data3 = (BYTE)ColorImage->imageData[j*ColorImage->widthStep + i*3 + 2]; //R分量
- ptr[j*ColorImage->width+x]=(BYTE)(0.072169*data1 + 0.715160*data2 + 0.212671*data3);
- }
- }
- OpenCvGrayImage=cvCreateImageHeader(cvGetSize(ColorImage), ColorImage->depth, 1);
- cvSetData(GrayImage,ptr, GrayImage->widthStep); //根據數據生成灰度圖
- cvNamedWindow("GrayImage",CV_WINDOW_AUTOSIZE);
- cvShowImage("GrayImage",OpenCvGrayImage); //顯示灰度圖
- cvWaitKey(0);
- cvDestroyWindow("GrayImage");
圖3 灰度化後的圖像
3.2 圖像的高斯濾波
根據上面所講的邊緣檢測過程,下一個步驟就是對圖像進行高斯濾波。可根據之前博文描述的方法獲取一維或者二維的高斯濾波核。因此進行圖像高斯濾波可有兩種實現方式,以下具體進行介紹。
首先定義該部分的通用變量:- double nSigma = 0.4; //定義高斯函數的標準差
- int nWidowSize = 1+2*ceil(3*nSigma); //定義濾波窗口的大小
- int nCenter = (nWidowSize)/2; //定義濾波窗口中心的索引
- int nWidth = OpenCvGrayImage->width; //獲取圖像的像素寬度
- int nHeight = OpenCvGrayImage->height; //獲取圖像的像素高度
- unsigned char* nImageData = new unsigned char[nWidth*nHeight]; //暫時保存圖像中的數據
- unsigned char*pCanny = new unsigned char[nWidth*nHeight]; //爲平滑後的圖像數據分配內存
- double* nData = new double[nWidth*nHeight]; //兩次平滑的中間數據
- for(int j=0; j<nHeight; j++) //獲取數據
- {
- for(i=0; i<nWidth; i++)
- nImageData[j*nWidth+i] = (unsigned char)OpenCvGrayImage->imageData[j*nWidth+i];
- }
3.2.1 根據一維高斯核進行兩次濾波
1)生成一維高斯濾波係數
- //////////////////////生成一維高斯濾波係數/////////////////////////////
- double* pdKernal_1 = new double[nWidowSize]; //定義一維高斯核數組
- double dSum_1 = 0.0; //求和,用於進行歸一化
- ////////////////////////一維高斯函數公式//////////////////////////////
- //// x*x /////////////////
- //// -1*---------------- /////////////////
- //// 1 2*Sigma*Sigma /////////////////
- //// ------------ e /////////////////
- //// /////////////////
- //// \/2*pi*Sigma /////////////////
- //////////////////////////////////////////////////////////////////////
- for(int i=0; i<nWidowSize; i++)
- {
- double nDis = (double)(i-nCenter);
- pdKernal_1[i] = exp(-(0.5)*nDis*nDis/(nSigma*nSigma))/(sqrt(2*3.14159)*nSigma);
- dSum_1 += pdKernal_1[i];
- }
- for(i=0; i<nWidowSize; i++)
- {
- pdKernal_1[i] /= dSum_1; //進行歸一化
- }
2)分別進行x向和y向的一維加權濾波,濾波後的數據保存在矩陣pCanny中
- for(i=0; i<nHeight; i++) //進行x向的高斯濾波(加權平均)
- {
- for(j=0; j<nWidth; j++)
- {
- double dSum = 0;
- double dFilter=0; //濾波中間值
- for(int nLimit=(-nCenter); nLimit<=nCenter; nLimit++)
- {
- if((j+nLimit)>=0 && (j+nLimit) < nWidth ) //圖像不能超出邊界
- {
- dFilter += (double)nImageData[i*nWidth+j+nLimit] * pdKernal_1[nCenter+nLimit];
- dSum += pdKernal_1[nCenter+nLimit];
- }
- }
- nData[i*nWidth+j] = dFilter/dSum;
- }
- }
- for(i=0; i<nWidth; i++) //進行y向的高斯濾波(加權平均)
- {
- for(j=0; j<nHeight; j++)
- {
- double dSum = 0.0;
- double dFilter=0;
- for(int nLimit=(-nCenter); nLimit<=nCenter; nLimit++)
- {
- if((j+nLimit)>=0 && (j+nLimit) < nHeight) //圖像不能超出邊界
- {
- dFilter += (double)nData[(j+nLimit)*nWidth+i] * pdKernal_1[nCenter+nLimit];
- dSum += pdKernal_1[nCenter+nLimit];
- }
- }
- pCanny[j*nWidth+i] = (unsigned char)(int)dFilter/dSum;
- }
- }
3.2.2 根據二維高斯核進行濾波
1)生成二維高斯濾波係數
- //////////////////////生成一維高斯濾波係數//////////////////////////////////
- double* pdKernal_2 = new double[nWidowSize*nWidowSize]; //定義一維高斯核數組
- double dSum_2 = 0.0; //求和,進行歸一化
- ///////////////////////二維高斯函數公式////////////////////////////////////
- //// x*x+y*y ///////////////
- //// -1*-------------- ///////////////
- //// 1 2*Sigma*Sigma ///////////////
- //// ---------------- e ///////////////
- //// 2*pi*Sigma*Sigma ///////////////
- ///////////////////////////////////////////////////////////////////////////
- for(i=0; i<nWidowSize; i++)
- {
- for(int j=0; j<nWidowSize; j++)
- {
- int nDis_x = i-nCenter;
- int nDis_y = j-nCenter;
- pdKernal_2[i+j*nWidowSize]=exp(-(1/2)*(nDis_x*nDis_x+nDis_y*nDis_y)
- /(nSigma*nSigma))/(2*3.1415926*nSigma*nSigma);
- dSum_2 += pdKernal_2[i+j*nWidowSize];
- }
- }
- for(i=0; i<nWidowSize; i++)
- {
- for(int j=0; j<nWidowSize; j++) //進行歸一化
- {
- pdKernal_2[i+j*nWidowSize] /= dSum_2;
- }
- }
2)採用高斯核進行高斯濾波,濾波後的數據保存在矩陣pCanny中
- int x;
- int y;
- for(i=0; i<nHeight; i++)
- {
- for(j=0; j<nWidth; j++)
- {
- double dFilter=0.0;
- double dSum = 0.0;
- for(x=(-nCenter); x<=nCenter; x++) //行
- {
- for(y=(-nCenter); y<=nCenter; y++) //列
- {
- if( (j+x)>=0 && (j+x)<nWidth && (i+y)>=0 && (i+y)<nHeight) //判斷邊緣
- {
- dFilter += (double)nImageData [(i+y)*nWidth + (j+x)]
- * pdKernal_2[(y+nCenter)*nWidowSize+(x+nCenter)];
- dSum += pdKernal_2[(y+nCenter)*nWidowSize+(x+nCenter)];
- }
- }
- }
- pCanny[i*nWidth+j] = (unsigned char)dFilter/dSum;
- }
- }
3.3 圖像增強——計算圖像梯度及其方向
- //////////////////同樣可以用不同的檢測器/////////////////////////
- ///// P[i,j]=(S[i,j+1]-S[i,j]+S[i+1,j+1]-S[i+1,j])/2 /////
- ///// Q[i,j]=(S[i,j]-S[i+1,j]+S[i,j+1]-S[i+1,j+1])/2 /////
- /////////////////////////////////////////////////////////////////
- double* P = new double[nWidth*nHeight]; //x向偏導數
- double* Q = new double[nWidth*nHeight]; //y向偏導數
- int* M = new int[nWidth*nHeight]; //梯度幅值
- double* Theta = new double[nWidth*nHeight]; //梯度方向
- //計算x,y方向的偏導數
- for(i=0; i<(nHeight-1); i++)
- {
- for(j=0; j<(nWidth-1); j++)
- {
- P[i*nWidth+j] = (double)(pCanny[i*nWidth + min(j+1, nWidth-1)] - pCanny[i*nWidth+j] + pCanny[min(i+1, nHeight-1)*nWidth+min(j+1, nWidth-1)] - pCanny[min(i+1, nHeight-1)*nWidth+j])/2;
- Q[i*nWidth+j] = (double)(pCanny[i*nWidth+j] - pCanny[min(i+1, nHeight-1)*nWidth+j] + pCanny[i*nWidth+min(j+1, nWidth-1)] - pCanny[min(i+1, nHeight-1)*nWidth+min(j+1, nWidth-1)])/2;
- }
- }
- //計算梯度幅值和梯度的方向
- for(i=0; i<nHeight; i++)
- {
- for(j=0; j<nWidth; j++)
- {
- M[i*nWidth+j] = (int)(sqrt(P[i*nWidth+j]*P[i*nWidth+j] + Q[i*nWidth+j]*Q[i*nWidth+j])+0.5);
- Theta[i*nWidth+j] = atan2(Q[i*nWidth+j], P[i*nWidth+j]) * 57.3;
- if(Theta[i*nWidth+j] < 0)
- Theta[i*nWidth+j] += 360; //將這個角度轉換到0~360範圍
- }
- }
3.4 非極大值抑制
根據上文所述的工作原理,這部分首先需要求解每個像素點在其鄰域內的梯度方向的兩個灰度值,然後判斷是否爲潛在的邊緣,如果不是則將該點灰度值設置爲0.
首先定義相關的參數如下:
- unsigned char* N = new unsigned char[nWidth*nHeight]; //非極大值抑制結果
- int g1=0, g2=0, g3=0, g4=0; //用於進行插值,得到亞像素點座標值
- double dTmp1=0.0, dTmp2=0.0; //保存兩個亞像素點插值得到的灰度數據
- double dWeight=0.0; //插值的權重
- for(i=0; i<nWidth; i++)
- {
- N[i] = 0;
- N[(nHeight-1)*nWidth+i] = 0;
- }
- for(j=0; j<nHeight; j++)
- {
- N[j*nWidth] = 0;
- N[j*nWidth+(nWidth-1)] = 0;
- }
- for(i=1; i<(nWidth-1); i++)
- {
- for(j=1; j<(nHeight-1); j++)
- {
- int nPointIdx = i+j*nWidth; //當前點在圖像數組中的索引值
- if(M[nPointIdx] == 0)
- N[nPointIdx] = 0; //如果當前梯度幅值爲0,則不是局部最大對該點賦爲0
- else
- {
- ////////首先判斷屬於那種情況,然後根據情況插值///////
- ////////////////////第一種情況///////////////////////
- ///////// g1 g2 /////////////
- ///////// C /////////////
- ///////// g3 g4 /////////////
- /////////////////////////////////////////////////////
- if( ((Theta[nPointIdx]>=90)&&(Theta[nPointIdx]<135)) ||
- ((Theta[nPointIdx]>=270)&&(Theta[nPointIdx]<315)))
- {
- //////根據斜率和四個中間值進行插值求解
- g1 = M[nPointIdx-nWidth-1];
- g2 = M[nPointIdx-nWidth];
- g3 = M[nPointIdx+nWidth];
- g4 = M[nPointIdx+nWidth+1];
- dWeight = fabs(P[nPointIdx])/fabs(Q[nPointIdx]); //反正切
- dTmp1 = g1*dWeight+g2*(1-dWeight);
- dTmp2 = g4*dWeight+g3*(1-dWeight);
- }
- ////////////////////第二種情況///////////////////////
- ///////// g1 /////////////
- ///////// g2 C g3 /////////////
- ///////// g4 /////////////
- /////////////////////////////////////////////////////
- else if( ((Theta[nPointIdx]>=135)&&(Theta[nPointIdx]<180)) ||
- ((Theta[nPointIdx]>=315)&&(Theta[nPointIdx]<360)))
- {
- g1 = M[nPointIdx-nWidth-1];
- g2 = M[nPointIdx-1];
- g3 = M[nPointIdx+1];
- g4 = M[nPointIdx+nWidth+1];
- dWeight = fabs(Q[nPointIdx])/fabs(P[nPointIdx]); //正切
- dTmp1 = g2*dWeight+g1*(1-dWeight);
- dTmp2 = g4*dWeight+g3*(1-dWeight);
- }
- ////////////////////第三種情況///////////////////////
- ///////// g1 g2 /////////////
- ///////// C /////////////
- ///////// g4 g3 /////////////
- /////////////////////////////////////////////////////
- else if( ((Theta[nPointIdx]>=45)&&(Theta[nPointIdx]<90)) ||
- ((Theta[nPointIdx]>=225)&&(Theta[nPointIdx]<270)))
- {
- g1 = M[nPointIdx-nWidth];
- g2 = M[nPointIdx-nWidth+1];
- g3 = M[nPointIdx+nWidth];
- g4 = M[nPointIdx+nWidth-1];
- dWeight = fabs(P[nPointIdx])/fabs(Q[nPointIdx]); //反正切
- dTmp1 = g2*dWeight+g1*(1-dWeight);
- dTmp2 = g3*dWeight+g4*(1-dWeight);
- }
- ////////////////////第四種情況///////////////////////
- ///////// g1 /////////////
- ///////// g4 C g2 /////////////
- ///////// g3 /////////////
- /////////////////////////////////////////////////////
- else if( ((Theta[nPointIdx]>=0)&&(Theta[nPointIdx]<45)) ||
- ((Theta[nPointIdx]>=180)&&(Theta[nPointIdx]<225)))
- {
- g1 = M[nPointIdx-nWidth+1];
- g2 = M[nPointIdx+1];
- g3 = M[nPointIdx+nWidth-1];
- g4 = M[nPointIdx-1];
- dWeight = fabs(Q[nPointIdx])/fabs(P[nPointIdx]); //正切
- dTmp1 = g1*dWeight+g2*(1-dWeight);
- dTmp2 = g3*dWeight+g4*(1-dWeight);
- }
- }
- //////////進行局部最大值判斷,並寫入檢測結果////////////////
- if((M[nPointIdx]>=dTmp1) && (M[nPointIdx]>=dTmp2))
- N[nPointIdx] = 128;
- else
- N[nPointIdx] = 0;
- }
- }
3.5雙閾值檢測實現
1)定義相應參數如下
- int nHist[1024];
- int nEdgeNum; //可能邊界數
- int nMaxMag = 0; //最大梯度數
- int nHighCount;
2)構造灰度圖的統計直方圖,根據上文梯度幅值的計算公式可知,最大的梯度幅值爲:
- for(i=0;i<1024;i++)
- nHist[i] = 0;
- for(i=0; i<nHeight; i++)
- {
- for(j=0; j<nWidth; j++)
- {
- if(N[i*nWidth+j]==128)
- nHist[M[i*nWidth+j]]++;
- }
- }
3)獲取最大梯度幅值及潛在邊緣點個數
- nEdgeNum = nHist[0];
- nMaxMag = 0; //獲取最大的梯度值
- for(i=1; i<1024; i++) //統計經過“非最大值抑制”後有多少像素
- {
- if(nHist[i] != 0) //梯度爲0的點是不可能爲邊界點的
- {
- nMaxMag = i;
- }
- nEdgeNum += nHist[i]; //經過non-maximum suppression後有多少像素
- }
4)計算兩個閾值
- double dRatHigh = 0.79;
- double dThrHigh;
- double dThrLow;
- double dRatLow = 0.5;
- nHighCount = (int)(dRatHigh * nEdgeNum + 0.5);
- j=1;
- nEdgeNum = nHist[1];
- while((j<(nMaxMag-1)) && (nEdgeNum < nHighCount))
- {
- j++;
- nEdgeNum += nHist[j];
- }
- dThrHigh = j; //高閾值
- dThrLow = (int)((dThrHigh) * dRatLow + 0.5); //低閾值
這段代碼的意思是,按照灰度值從低到高的順序,選取前79%個灰度值中的最大的灰度值爲高閾值,低閾值大約爲高閾值的一半。這是根據經驗數據的來的,至於更好地參數選取方法,作者後面會另文研究。
5)進行邊緣檢測
- SIZE sz;
- sz.cx = nWidth;
- sz.cy = nHeight;
- for(i=0; i<nHeight; i++)
- {
- for(j=0; j<nWidth; j++)
- {
- if((N[i*nWidth+j]==128) && (M[i*nWidth+j] >= dThrHigh))
- {
- N[i*nWidth+j] = 255;
- TraceEdge(i, j, dThrLow, N, M, sz);
- }
- }
- }
- //將還沒有設置爲邊界的點設置爲非邊界點
- for(i=0; i<nHeight; i++)
- {
- for(j=0; j<nWidth; j++)
- {
- if(N[i*nWidth+j] != 255)
- {
- N[i*nWidth+j] = 0 ; // 設置爲非邊界點
- }
- }
- }
其中TraceEdge函數爲一個嵌套函數,用於在每個像素點的鄰域內尋找滿足條件的點。其實現代碼如下:
- void TraceEdge(int y, int x, int nThrLow, LPBYTE pResult, int *pMag, SIZE sz)
- {
- //對8鄰域像素進行查詢
- int xNum[8] = {1,1,0,-1,-1,-1,0,1};
- int yNum[8] = {0,1,1,1,0,-1,-1,-1};
- LONG yy,xx,k;
- for(k=0;k<8;k++)
- {
- yy = y+yNum[k];
- xx = x+xNum[k];
- if(pResult[yy*sz.cx+xx]==128 && pMag[yy*sz.cx+xx]>=nThrLow )
- {
- //該點設爲邊界點
- pResult[yy*sz.cx+xx] = 255;
- //以該點爲中心再進行跟蹤
- TraceEdge(yy,xx,nThrLow,pResult,pMag,sz);
- }
- }
- }
以上就從原理上實現了整個Canny算法。其檢測效果如圖4所示。注意:以上代碼僅爲作者理解所爲,目的是驗證本人對算法的理解,暫時沒有考慮到代碼的執行效率的問題。
4、擴展
對比圖4和圖5可以發現,作者自己實現的邊緣檢測效果沒有OpenCV的好,具體體現在:1)丟失了一些真的邊緣;2)增加了一些假的邊緣。
經過對整個算法的來回檢查,初步推斷主要的問題可能在於在進行灰度矩陣梯度幅值計算式所採用的模板算子性能不是太好,還有就是關於兩個閾值的選取方法。關於這兩個方面的改進研究,後文闡述。5、總結
本文是過去一段時間,對圖像邊緣檢測方法學習的總結。主要闡述了Canny算法的工作原理,實現過程,在此基礎上基於VC6.0實現了該算法,並給出了效果圖。最後,通過對比發現本文的實現方法雖然能夠實現邊緣檢測,但效果還不是很理想,今後將在閾值選取原則和梯度幅值算子兩個方面進行改進。