OpenCV中Hough直線檢測和圓檢測
霍夫變換是影象處理中的一個檢測直線、圓等簡單幾何形狀的方法。它最初是用於在二值化的影象中進行直線檢測的。
1、霍夫直線檢測
(1)基本理論
Hough直線檢測的基本理論是二值影象中的任何點都可能是一些候選直線集合的一部分,所選的引數方式是每一行代表極座標中的一個點,並且隱含的直線是通過象徵點的,垂直於原點到此點的半徑,即:檢測的過程可以看成從影象中的一個個畫素點出發,尋找每個點成為直線一部分的可能,再把這條線上可能的點連起來形成直線。在實際檢測中,當一條線出現凹陷或是彎曲度時,也會檢測出直線,只是不是一條完整長度直線,而是斷斷續續重疊相近的很多直線。
而對於影象中的一條直線而言,利用直角座標系,可以表示為:(2)OpenCV的實現
OpenCV中(2.0版本以下)的函式cvHoughLines2()實現了用於直線(線段)檢測的霍夫變換(Hough transform)方法,將極座標空間中區域性峰值點予以返回。它支援兩種不同的霍夫直線變換:
CvSeq* cvHoughLines2(CvArr* image, void* lineStorage, int method, double rho, double theta, int threshold, double param1=0, double param2=0);
引數的說明如下:
image:
輸入 8-位元、單通道 (二值) 影象,當用CV_HOUGH_PROBABILISTIC方法檢測的時候其內容會被函式改變。
line_storage:
檢測到的線段儲存倉. 可以是記憶體儲存倉 (此種情況下,一個線段序列在儲存倉中被建立,並且由函式返回),或者是包含線段引數的特殊型別(見下面)的具有單行/單列的矩陣(CvMat*)。矩陣頭為函式所修改,使得它的 cols/rows 將包含一組檢測到的線段。如果 line_storage 是矩陣,而實際線段的數目超過矩陣尺寸,那麼最大可能數目的線段被返回(線段沒有按照長度、可信度或其它指標排序)。
method:
Hough 變換變數,是下面變數的其中之一:
CV_HOUGH_STANDARD - 傳統或標準 Hough 變換(SHT)。每一個線段由兩個浮點數 (ρ, θ) 表示,其中 ρ 是直線與原點 (0,0) 之間的距離,θ 線段與 x-軸之間的夾角。因此,矩陣型別必須是 CV_32FC2 type;
CV_HOUGH_PROBABILISTIC- 概率 Hough 變換(PPHT)。如果影象包含一些長的線性分割,則效率更高。它返回線段分割而不是整個線段。每個分割用起點和終點來表示,所以矩陣(或建立的序列)型別是 CV_32SC4 type;
CV_HOUGH_MULTI_SCALE- 傳統 Hough 變換的多尺度變種。線段的編碼方式與 CV_HOUGH_STANDARD 的一致。
SHT相對而言,找的線引數比較準,但是畫的位置不準。具體表現為檢測的線端點和圖片實際端點有一定偏差;PPHT檢測結果相對SHT來說,更靠近圖片中的線位置,具體表現為PPHT檢測的起始端點比SHT檢測的更靠近圖片線端點。
假如知道圖片中線的端點,你把它帶入SHT內的引數,更準。rho:
設定極座標系的精度:與畫素相關的距離精度(單位為畫素)。
theta:
設定極座標系的精度:弧度測量的角度精度(單位為弧度)。
threshold:
閾值引數。如果相應的累計值大於 threshold, 則函式返回的這個線段。
param1:
第一個方法相關的引數:
對傳統 Hough 變換,不使用(0);
對概率 Hough 變換,它是最小線段長度;
對多尺度 Hough 變換,它是距離精度 rho 的分母。首先根據使用者設定的rho和theta來檢測直線,之後按照param1和param2的值對檢測結果進行優化,即:大致的距離精度是 rho,而精確的應該是 rho / param1。
param2:
第二個方法相關引數:
對傳統 Hough 變換,不使用 (0);
對概率 Hough 變換,這個引數表示在同一條直線上進行碎線段連線的最大間隔值(gap), 即當同一條直線上的兩條碎線段之間的間隔小於param2時,將其合二為一;
對多尺度 Hough 變換,它是角度精度 theta 的分母。首先根據使用者設定的rho和theta來檢測直線,之後按照param1和param2的值對檢測結果進行優化,即:大致的角度精度是 theta,而精確的角度應該是 theta / param2。
返回值說明如下:
返回的是CvSeq*型別,儲存了檢測到的直線引數序列。其中在使用cvLine()函式繪製直線時有個line[0]和line[1],選擇不同形式的hough變換直線檢測型別,line[0]和line[1]表示的意思是不同的。
a.在標準hough變換中,對於返回的每條直線:
float* line = ( float* ) cvGetSeqElem( lines,i); float rho = line[0]; float theta = line[1];
line[ 0 ] 表示向量長度(rho_x來表示),line[1]表示直線的傾斜角(theta_x來表示)
b.在概率hough變換中,line[0]和line[1]表示返回直線的兩個端點:
CvPoint* line = (CvPoint*)cvGetSeqElem(lines,i); cvLine( color_dst, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 );
程式碼參考如下:
#define CV_NO_BACKWARD_COMPATIBILITY
/* This is a standalone program. Pass an image name as a first parameter of the program.
Switch between standard and probabilistic Hough transform by changing "#if 1" to "#if 0" and back */
#include <cv.h>
#include <highgui.h>
#include <math.h>
int main(int argc, char** argv)
{
const char* filename = argc >= 2 ? argv[1] : ".\\pic1.png";
IplImage* src = cvLoadImage( filename, 0 );
IplImage* dst;
IplImage* color_dst;
CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* lines = 0;
int i;
if( !src )
{
printf("Load image error!\n");
return -1;
}
dst = cvCreateImage( cvGetSize(src), 8, 1 );
color_dst = cvCreateImage( cvGetSize(src), 8, 3 );
cvCanny( src, dst, 50, 200, 3 );
//cvThreshold(src, dst, 50, 255, CV_THRESH_BINARY_INV);
cvCvtColor( dst, color_dst, CV_GRAY2BGR );
#if 0 //標準hough變換
printf("標準hough變換\n");
lines = cvHoughLines2( dst, storage, CV_HOUGH_STANDARD, 1, CV_PI/180, 100, 0, 0 );
for( i = 0; i < MIN(lines->total,100); i++ )
{
float* line = (float*)cvGetSeqElem(lines,i);
float rho = line[0];
float theta = line[1];
CvPoint pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a*rho, y0 = b*rho;
pt1.x = cvRound(x0 + 1000*(-b));
pt1.y = cvRound(y0 + 1000*(a));
pt2.x = cvRound(x0 - 1000*(-b));
pt2.y = cvRound(y0 - 1000*(a));
cvLine( color_dst, pt1, pt2, CV_RGB(255,0,0), 3, CV_AA, 0 );
}
#else //累積概率hough變換
printf("累積概率hough變換\n");
lines = cvHoughLines2( dst, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 );
for( i = 0; i < lines->total; i++ )
{
CvPoint* line = (CvPoint*)cvGetSeqElem(lines,i);
cvLine( color_dst, line[0], line[1], CV_RGB(255,0,0), 3, CV_AA, 0 );
}
#endif
cvNamedWindow( "Source", 1 );
cvShowImage( "Source", src );
cvNamedWindow( "Hough", 1 );
cvShowImage( "Hough", color_dst );
cvWaitKey(0);
return 0;
}
結果如下:
2、霍夫圓檢測
(1)基本理論
已知圓的一般方程為:(x - a)^2 + (y - b)^2 = r^2。其中,(a, b)為圓心,r為圓的半徑。把X-Y平面上的圓轉換到a-b-r引數空間,則影象空間中過(x, y)點圓對應引數空間中,高度r變化下的一個三維錐面,如圖:
同理,過影象空間中任意一點的圓對應於引數空間中的一個三維錐面。因此,過影象空間上同一圓上的點,對應的引數空間中的三維錐面,在r高度必然相交於一點(a, b, r)。這樣通過檢測這一點可以得到圓的引數,相應的圓也可求得了。影象平面的方程轉化為引數平面上的示意圖如圖:
相比霍夫直線檢測,圓形檢測的過程很類似:只是引數方程有變化,而且引數空間增加了一個維度(圓心座標x,y和半徑r)。霍夫變換的一個好處就是不需要影象中出現完整的圓,只要落在一個圓上的畫素數量足夠多,就能正確識別。同理,霍夫變換檢測橢圓如果使用橢圓的標準式,那麼將會有五個引數,它們通過標準式相關,檢測圓就已經相當耗時了,如果再用這中方程形式處理勢必失去實際用途。
(2)OpenCV實現
由於比直線檢測多出一個維度,使得標準的霍夫圓檢測需要大量記憶體且速度較慢。出於對運算效率的考慮, OpenCV實現的霍夫圓檢測是一個比標準霍夫圓變換更為靈活的檢測方法:霍夫梯度法,也叫2-1霍夫變換(21HT)。原理是首先對影象進行canny邊緣檢測,然後考慮邊緣影象中的每一個非0點的區域性梯度,通過cvSobel()函式計算x,y方向上的sobel一階導數得到梯度,利用得到的梯度,由斜率指定直線上的每一個點都在累加器中被累加,然後從累加器中這些點中選擇候選的中心畫圓。
它的原理依據是圓心一定是在圓上的每個點的模向量上,這些圓上點模向量的交點就是圓心,霍夫梯度法的第一步就是找到這些圓心,這樣三維的累加平面就又轉化為二維累加平面;第二步是根據所有候選中心的邊緣非0畫素對其的支援程度來確定半徑。)
OpenCV中(2.0版本以下)的函式cvHoughCircles()實現了上述方法。cvHoughCircles()函式原型如下:
CvSeq* cvHoughCircles( CvArr* image, void* circle_storage, int method, double dp, double min_dist, double param1=100, double param2=100, int min_radius=0, int max_radius=0 );
引數的說明如下:
image
輸入 8-位元、單通道灰度影象(這點跟直線檢測不同)。
circle_storage
檢測到的圓儲存倉,可以是記憶體儲存倉(此種情況下,一個線段序列在儲存倉中被建立,並且由函式返回),或者是包含圓引數的特殊型別的具有單行/單列的CV_32FC3型矩陣(CvMat*). 矩陣頭為函式所修改,使得它的 cols/rows 將包含一組檢測到的圓。如果 circle_storage 是矩陣,而實際圓的數目超過矩陣尺寸,那麼最大可能數目的圓被返回。每個圓由三個浮點數表示:圓心座標(x,y)和半徑r。
method
Hough 變換方式,目前只支援CV_HOUGH_GRADIENT(即:21HT)。
dp
累加器影象的解析度。這個引數允許建立一個比輸入影象解析度低的累加器。(這樣做是因為有理由認為影象中存在的圓會自然降低到與影象寬高相同數量的範疇)。如果dp設定為1,則解析度是相同的;如果設定為更大的值(比如2),累加器的解析度受此影響會變小(此情況下為一半)。dp的值不能比1小,但也不要太大,越大越易誤判,最好為2。
min_dist
該引數是讓演算法能明顯區分的兩個不同圓之間的最小距離。
param1
用於Canny的邊緣閥值上限,下限被置為上限的一半。根據影象總體灰度情況確定。
param2
累加器的閥值。較大,只檢測較大的圓;較小,小圓也檢測。
min_radius
最小圓半徑。
max_radius
最大圓半徑。
返回值說明如下:
返回的是CvSeq*型別,儲存了檢測到的圓的引數序列。
這裡能夠得到p[0],p[1]和p[2],分別代表圓心的x座標,y座標和半徑r。float* p = ( float* )cvGetSeqElem( results, i );
程式碼參考如下:
#include <cv.h>
#include <highgui.h>
#include <math.h>
int main(int argc, char* argv[])
{
const char* filename = argc >= 2 ? argv[1] : "..\\board.jpg";
IplImage* src = cvLoadImage(filename, 0);
cvSmooth(src, src, CV_GAUSSIAN, 5, 5); //降噪,不然誤檢測太高
IplImage* dst = cvCreateImage(cvGetSize(src), src->depth, 3);
cvCvtColor(src, dst, CV_GRAY2BGR);
CvMemStorage* storage = cvCreateMemStorage(0);
CvSeq* results = cvHoughCircles(
//cvHoughCircles函式需要估計每一個畫素梯度的方向,
//因此會在內部自動呼叫cvSobel,而二值邊緣影象的處理是比較難的
src,
storage,
CV_HOUGH_GRADIENT,
1, //累加器影象的解析度
10,
100,
30,
1,30); //改變著兩個引數檢測不同大小的圓
for( int i = 0; i < results->total; i++ )
{
float* p = ( float* )cvGetSeqElem( results, i );
//霍夫圓變換
CvPoint pt = cvPoint( cvRound( p[0] ), cvRound( p[1] ) );
cvCircle(
dst,
pt, //確定圓心
cvRound( p[2] ), //確定半徑
CV_RGB( 255, 0, 0 )
); //畫圓函式
}
cvNamedWindow( "cvHoughCircles", 1 );
cvShowImage( "cvHoughCircles", dst );
cvNamedWindow( "source", 1 );
cvShowImage( "source", src );
cvWaitKey(0);
cvReleaseMemStorage(&storage);
return 0;
}
結果如下:效果不好,但是opencv2.0以上版本的demo(\samples\cpp下面的houghcircles.cpp)效果不錯:
ref: