我理解的霍夫變換~(主要是笛卡爾坐標系跟極坐標系的相互轉換)
首先要對兩個坐標系有了解才行:

,

ρ,θ就是一對hough空間的變量表示。若將ρ,θ看成直角坐標空間,一個點(x0, y0)就是一個關于ρ,θ的正弦曲線。同樣,直線上的其他點(Xn,Yn)也會構成一組關于ρ,θ的正弦曲線,這樣勢必存在一個關于ρ,θ相交(即垂直點(r,θ))。

于是乎, 一條直線能夠通過在極坐標下尋找交于一點的曲線數量來檢測,如果越多曲線交于一點,就意味著這個交點表示的直線由更多的點組成。我們可以通過設置直線上點的閾值來定義多少條曲線交于一點我們才認為檢測到了一條直線。
tatic voidicvHoughLinesStandard( const CvMat* img, float rho, float theta, int threshold, CvSeq *lines, int linesMax ){ cv::AutoBuffer<int> _accum, _sort_buf; cv::AutoBuffer<float> _tabSin, _tabCos; const uchar* image; int step, width, height; int numangle, numrho; int total = 0; int i, j; float irho = 1 / rho; double scale; CV_Assert( CV_IS_MAT(img) && CV_MAT_TYPE(img->type) == CV_8UC1 ); image = img->data.ptr; step = img->step; width = img->cols; height = img->rows; numangle = cvRound(CV_PI / theta); numrho = cvRound(((width + height) * 2 + 1) / rho); _accum.allocate((numangle+2) * (numrho+2)); _sort_buf.allocate(numangle * numrho); _tabSin.allocate(numangle); _tabCos.allocate(numangle); int *accum = _accum, *sort_buf = _sort_buf; float *tabSin = _tabSin, *tabCos = _tabCos; memset( accum, 0, sizeof(accum[0]) * (numangle+2) * (numrho+2) ); float ang = 0; for(int n = 0; n < numangle; ang += theta, n++ ) { tabSin[n] = (float)(sin((double)ang) * irho); tabCos[n] = (float)(cos((double)ang) * irho); } // stage 1. fill accumulator for( i = 0; i < height; i++ ) for( j = 0; j < width; j++ ) { if( image[i * step + j] != 0 ) for(int n = 0; n < numangle; n++ ) { int r = cvRound( j * tabCos[n] + i * tabSin[n] ); r += (numrho - 1) / 2; accum[(n+1) * (numrho+2) + r+1]++; } } // stage 2. find local maximums for(int r = 0; r < numrho; r++ ) for(int n = 0; n < numangle; n++ ) { int base = (n+1) * (numrho+2) + r+1; if( accum[base] > threshold && accum[base] > accum[base - 1] && accum[base] >= accum[base + 1] && accum[base] > accum[base - numrho - 2] && accum[base] >= accum[base + numrho + 2] ) sort_buf[total++] = base; } // stage 3. sort the detected lines by accumulator value icvHoughSortDescent32s( sort_buf, total, accum ); // stage 4. store the first min(total,linesMax) lines to the output buffer linesMax = MIN(linesMax, total); scale = 1./(numrho+2); for( i = 0; i < linesMax; i++ ) { CvLinePolar line; int idx = sort_buf[i]; int n = cvFloor(idx*scale) - 1; int r = idx - (n+1)*(numrho+2) - 1; line.rho = (r - (numrho - 1)*0.5f) * rho; line.angle = n * theta; cvSeqPush( lines, &line ); }}在博文http://blog.csdn.net/traumland/article/details/51319644里面有注釋。(1)HoughLines( )函數詳解
void cv::HoughLines( InputArray _image, OutputArray _lines, double rho, double theta, int threshold, double srn, double stn ){ Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE); Mat image = _image.getMat(); CvMat c_image = image; CvSeq* seq = cvHoughLines2( &c_image, storage, srn == 0 && stn == 0 ? CV_HOUGH_STANDARD : CV_HOUGH_MULTI_SCALE, rho, theta, threshold, srn, stn ); seqToMat(seq, _lines);}1.第一個參數,InputArray類型的image,輸入圖像,即源圖像,需為8位的單通道二進制圖像,可以將任意的源圖載入進來后由函數修改成此格式后,再填在這里。2.第二個參數,InputArray類型的lines,經過調用HoughLines函數后儲存了霍夫線變換檢測到線條的輸出矢量。每一條線由具有兩個元素的矢量表示,其中,是離坐標原點((0,0)(也就是圖像的左上角)的距離。 是弧度線條旋轉角度(0~垂直線,π/2~水平線)。3.第三個參數,double類型的rho,以像素為單位的距離精度。另一種形容方式是直線搜索時的進步尺寸的單位半徑。PS:Latex中/rho就表示 。4.第四個參數,double類型的theta,以弧度為單位的角度精度。另一種形容方式是直線搜索時的進步尺寸的單位角度。5.第五個參數,int類型的threshold,累加平面的閾值參數,即識別某部分為圖中的一條直線時它在累加平面中必須達到的值。大于閾值threshold的線段才可以被檢測通過并返回到結果中。6.第六個參數,double類型的srn,有默認值0。對于多尺度的霍夫變換,這是第三個參數進步尺寸rho的除數距離。粗略的累加器進步尺寸直接是第三個參數rho,而精確的累加器進步尺寸為rho/srn。7.第七個參數,double類型的stn,有默認值0,對于多尺度霍夫變換,srn表示第四個參數進步尺寸的單位角度theta的除數距離。且如果srn和stn同時為0,就表示使用經典的霍夫變換。否則,這兩個參數應該都為正數。HoughLines函數會調用cvHoughLines2函數,它通過參數CV_HOUGH_STANDARD,最終調用了icvHoughLinesStandard函數.
實例:
#include "opencv2/core/core.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv;using namespace std;int main(){ Mat srcImage = imread("F://IM_VIDEO//building2.jpg"); Mat midImage, dstImage; Canny(srcImage, midImage, 50, 200, 3);//進行canny邊緣檢測 cvtColor(midImage, dstImage, CV_GRAY2BGR);//轉化邊緣檢測后的圖為灰度圖 vector<Vec2f> lines;//定義一個矢量結構lines用于存放得到的線段矢量集合 HoughLines(midImage, lines, 1, CV_PI / 180, 160, 0, 0);//第五個參數的設置會影響效果,可以多試幾個 for (size_t i = 0; i < lines.size(); i++) { float rho = lines[i][0], theta = lines[i][1]; Point 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)); line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, CV_AA); } namedWindow("building2", 0); namedWindow("canny_building2", 0); namedWindow("lines", 0); imshow("building2", srcImage); imshow("canny_building2", midImage); imshow("lines", dstImage); waitKey(0); return 0;}運行結果:
(2)HoughLinesP( )函數詳解
(此部分主要參考:http://blog.csdn.net/zhaocj/article/details/40047397)
標準霍夫變換本質上是把圖像映射到它的參數空間上,它需要計算所有的M個邊緣點,這樣它的運算量和所需內存空間都會很大。如果在輸入圖像中只是處理m(m<M)個邊緣點,則這m個邊緣點的選取是具有一定概率性的,因此該方法被稱為概率霍夫變換(Probabilistic Hough Transform)。該方法還有一個重要的特點就是能夠檢測出線端,即能夠檢測出圖像中直線的兩個端點,確切地定位圖像中的直線。HoughLinesP函數就是利用概率霍夫變換來檢測直線的。它的一般步驟為:1、隨機抽取圖像中的一個特征點,即邊緣點,如果該點已經被標定為是某一條直線上的點,則繼續在剩下的邊緣點中隨機抽取一個邊緣點,直到所有邊緣點都抽取完了為止;2、對該點進行霍夫變換,并進行累加和計算;3、選取在霍夫空間內值最大的點,如果該點大于閾值的,則進行步驟4,否則回到步驟1;4、根據霍夫變換得到的最大值,從該點出發,沿著直線的方向位移,從而找到直線的兩個端點;5、計算直線的長度,如果大于某個閾值,則被認為是好的直線輸出,回到步驟1。
opencv源碼:
void cv::HoughLinesP( InputArray _image, OutputArray _lines, double rho, double theta, int threshold, double minLineLength, double maxGap ){ Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE); Mat image = _image.getMat(); CvMat c_image = image; CvSeq* seq = cvHoughLines2( &c_image, storage, CV_HOUGH_PROBABILISTIC, rho, theta, threshold, minLineLength, maxGap ); seqToMat(seq, _lines);}image為輸入圖像,要求是8位單通道圖像。lines為輸出的直線向量,每條線用4個元素表示,即直線的兩個端點的4個坐標值。rho和theta分別為距離和角度的分辨率,我覺得可以理解為極坐系中r和θ的分辨率。threshold為閾值,它表示要判斷為一條直線所需的最少度量,顯然這個值越大,所判斷出的直線越少;這個值越小,所判斷出的直線越多。minLineLength:根據threshold提取出的直線長短不一,這個參數以長度對這些直線作一次篩選,小于這個參數值的就被拋棄。顯然這個值越大,所判斷出的直線越少;這個值越小,所判斷出的直線越多。maxLineGap:最大直線間隙,即如果有兩條線段在一條直線上,但它們之間因為有間隙,所以被認為是兩個線段,如果這個間隙大于該值,則被認為是兩條線段,否則是一條。顯然這個值越大,所判斷出的直線越少;這個值越小,所判斷出的直線越多(值越小,那么間隙值就越容易大于這個值)。
HoughLinesP函數會調用cvHoughLines2函數,它通過參數CV_HOUGH_PROBABILISTIC,最終調用了icvHoughLinesProbabilistic函數,后面我直接附上zhaocj博文里的帶注釋部分:static void icvHoughLinesProbabilistic( CvMat* image, float rho, float theta, int threshold, int lineLength, int lineGap, CvSeq *lines, int linesMax ) { //accum為累加器矩陣,mask為掩碼矩陣 cv::Mat accum, mask; cv::vector<float> trigtab; //用于存儲事先計算好的正弦和余弦值 //開辟一段內存空間 cv::MemStorage storage(cvCreateMemStorage(0)); //用于存儲特征點坐標,即邊緣像素的位置 CvSeq* seq; CvSeqWriter writer; int width, height; //圖像的寬和高 int numangle, numrho; //角度和距離的離散數量 float ang; int r, n, count; CvPoint pt; float irho = 1 / rho; //距離分辨率的倒數 CvRNG rng = cvRNG(-1); //隨機數 const float* ttab; //向量trigtab的地址指針 uchar* mdata0; //矩陣mask的地址指針 //確保輸入圖像的正確性 CV_Assert( CV_IS_MAT(image) && CV_MAT_TYPE(image->type) == CV_8UC1 ); width = image->cols; //提取出輸入圖像的寬 height = image->rows; //提取出輸入圖像的高 //由角度和距離分辨率,得到角度和距離的離散數量 numangle = cvRound(CV_PI / theta); numrho = cvRound(((width + height) * 2 + 1) / rho); //創建累加器矩陣,即霍夫空間 accum.create( numangle, numrho, CV_32SC1 ); //創建掩碼矩陣,大小與輸入圖像相同 mask.create( height, width, CV_8UC1 ); //定義trigtab的大小,因為要存儲正弦和余弦值,所以長度為角度離散數的2倍 trigtab.resize(numangle*2); //累加器矩陣清零 accum = cv::Scalar(0); //避免重復計算,事先計算好所需的所有正弦和余弦值 for( ang = 0, n = 0; n < numangle; ang += theta, n++ ) { trigtab[n*2] = (float)(cos(ang) * irho); trigtab[n*2+1] = (float)(sin(ang) * irho); } //賦值首地址 ttab = &trigtab[0]; mdata0 = mask.data; //開始寫入序列 cvStartWriteSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage, &writer ); // stage 1. collect non-zero image points //收集圖像中的所有非零點,因為輸入圖像是邊緣圖像,所以非零點就是邊緣點 for( pt.y = 0, count = 0; pt.y < height; pt.y++ ) { //提取出輸入圖像和掩碼矩陣的每行地址指針 const uchar* data = image->data.ptr + pt.y*image->step; uchar* mdata = mdata0 + pt.y*width; for( pt.x = 0; pt.x < width; pt.x++ ) { if( data[pt.x] ) //是邊緣點 { mdata[pt.x] = (uchar)1; //掩碼的相應位置置1 CV_WRITE_SEQ_ELEM( pt, writer ); 把該坐標位置寫入序列 } else //不是邊緣點 mdata[pt.x] = 0; //掩碼的相應位置清0 } } //終止寫序列,seq為所有邊緣點坐標位置的序列 seq = cvEndWriteSeq( &writer ); count = seq->total; //得到邊緣點的數量 // stage 2. process all the points in random order //隨機處理所有的邊緣點 for( ; count > 0; count-- ) { // choose random point out of the remaining ones //步驟1,在剩下的邊緣點中隨機選擇一個點,idx為不大于count的隨機數 int idx = cvRandInt(&rng) % count; //max_val為累加器的最大值,max_n為最大值所對應的角度 int max_val = threshold-1, max_n = 0; //由隨機數idx在序列中提取出所對應的坐標點 CvPoint* point = (CvPoint*)cvGetSeqElem( seq, idx ); //定義直線的兩個端點 CvPoint line_end[2] = {{0,0}, {0,0}}; float a, b; //累加器的地址指針,也就是霍夫空間的地址指針 int* adata = (int*)accum.data; int i, j, k, x0, y0, dx0, dy0, xflag; int good_line; const int shift = 16; //提取出坐標點的橫、縱坐標 i = point->y; j = point->x; // "remove" it by overriding it with the last element //用序列中的最后一個元素覆蓋掉剛才提取出來的隨機坐標點 *point = *(CvPoint*)cvGetSeqElem( seq, count-1 ); // check if it has been excluded already (i.e. belongs to some other line) //檢測這個坐標點是否已經計算過,也就是它已經屬于其他直線 //因為計算過的坐標點會在掩碼矩陣mask的相對應位置清零 if( !mdata0[i*width + j] ) //該坐標點被處理過 continue; //不做任何處理,繼續主循環 // update accumulator, find the most probable line //步驟2,更新累加器矩陣,找到最有可能的直線 for( n = 0; n < numangle; n++, adata += numrho ) { //由角度計算距離 r = cvRound( j * ttab[n*2] + i * ttab[n*2+1] ); r += (numrho - 1) / 2; //在累加器矩陣的相應位置上數值加1,并賦值給val int val = ++adata[r]; //更新最大值,并得到它的角度 if( max_val < val ) { max_val = val; max_n = n; } } // if it is too "weak" candidate, continue with another point //步驟3,如果上面得到的最大值小于閾值,則放棄該點,繼續下一個點的計算 if( max_val < threshold ) continue; // from the current point walk in each direction // along the found line and extract the line segment //步驟4,從當前點出發,沿著它所在直線的方向前進,直到達到端點為止 a = -ttab[max_n*2+1]; //a=-sinθ b = ttab[max_n*2]; //b=cosθ //當前點的橫、縱坐標值 x0 = j; y0 = i; //確定當前點所在直線的角度是在45度~135度之間,還是在0~45或135度~180度之間 if( fabs(a) > fabs(b) ) //在45度~135度之間 { xflag = 1; //置標識位,標識直線的粗略方向 //確定橫、縱坐標的位移量 dx0 = a > 0 ? 1 : -1; dy0 = cvRound( b*(1 << shift)/fabs(a) ); //確定縱坐標 y0 = (y0 << shift) + (1 << (shift-1)); } else //在0~45或135度~180度之間 { xflag = 0; //清標識位 //確定橫、縱坐標的位移量 dy0 = b > 0 ? 1 : -1; dx0 = cvRound( a*(1 << shift)/fabs(b) ); //確定橫坐標 x0 = (x0 << shift) + (1 << (shift-1)); } //搜索直線的兩個端點 for( k = 0; k < 2; k++ ) { //gap表示兩條直線的間隙,x和y為搜索位置,dx和dy為位移量 int gap = 0, x = x0, y = y0, dx = dx0, dy = dy0; //搜索第二個端點的時候,反方向位移 if( k > 0 ) dx = -dx, dy = -dy; // walk along the line using fixed-point arithmetics, // stop at the image border or in case of too big gap //沿著直線的方向位移,直到到達圖像的邊界或大的間隙為止 for( ;; x += dx, y += dy ) { uchar* mdata; int i1, j1; //確定新的位移后的坐標位置 if( xflag ) { j1 = x; i1 = y >> shift; } else { j1 = x >> shift; i1 = y; } //如果到達了圖像的邊界,停止位移,退出循環 if( j1 < 0 || j1 >= width || i1 < 0 || i1 >= height ) break; //定位位移后掩碼矩陣位置 mdata = mdata0 + i1*width + j1; // for each non-zero point: // update line end, // clear the mask element // reset the gap //該掩碼不為0,說明該點可能是在直線上 if( *mdata ) { gap = 0; //設置間隙為0 //更新直線的端點位置 line_end[k].y = i1; line_end[k].x = j1; } //掩碼為0,說明不是直線,但仍繼續位移,直到間隙大于所設置的閾值為止 else if( ++gap > lineGap ) //間隙加1 break; } } //步驟5,由檢測到的直線的兩個端點粗略計算直線的長度 //當直線長度大于所設置的閾值時,good_line為1,否則為0 good_line = abs(line_end[1].x - line_end[0].x) >= lineLength || abs(line_end[1].y - line_end[0].y) >= lineLength; //再次搜索端點,目的是更新累加器矩陣和更新掩碼矩陣,以備下一次循環使用 for( k = 0; k < 2; k++ ) { int x = x0, y = y0, dx = dx0, dy = dy0; if( k > 0 ) dx = -dx, dy = -dy; // walk along the line using fixed-point arithmetics, // stop at the image border or in case of too big gap for( ;; x += dx, y += dy ) { uchar* mdata; int i1, j1; if( xflag ) { j1 = x; i1 = y >> shift; } else { j1 = x >> shift; i1 = y; } mdata = mdata0 + i1*width + j1; // for each non-zero point: // update line end, // clear the mask element // reset the gap if( *mdata ) { //if語句的作用是清除那些已經判定是好的直線上的點對應的累加器的值,避免再次利用這些累加值 if( good_line ) //在第一次搜索中已經確定是好的直線 { //得到累加器矩陣地址指針 adata = (int*)accum.data; for( n = 0; n < numangle; n++, adata += numrho ) { r = cvRound( j1 * ttab[n*2] + i1 * ttab[n*2+1] ); r += (numrho - 1) / 2; adata[r]--; //相應的累加器減1 } } //搜索過的位置,不管是好的直線,還是壞的直線,掩碼相應位置都清0,這樣下次就不會再重復搜索這些位置了,從而達到減小計算邊緣點的目的 *mdata = 0; } //如果已經到達了直線的端點,則退出循環 if( i1 == line_end[k].y && j1 == line_end[k].x ) break; } } //如果是好的直線 if( good_line ) { CvRect lr = { line_end[0].x, line_end[0].y, line_end[1].x, line_end[1].y }; //把兩個端點壓入序列中 cvSeqPush( lines, &lr ); //如果檢測到的直線數量大于閾值,則退出該函數 if( lines->total >= linesMax ) return; } } } 實例程序:#include "opencv2/core/core.hpp" #include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> using namespace cv;using namespace std;int main(int argc, char** argv){ Mat src, edge, color_edge; src = imread("F://IM_VIDEO//building2.jpg"); if (!src.data) return -1; Canny(src, edge, 50, 200, 3); cvtColor(edge, color_edge, CV_GRAY2BGR); vector<Vec4i> lines; HoughLinesP(edge, lines, 1, CV_PI / 180, 80, 30, 10); for (size_t i = 0; i < lines.size(); i++) { Vec4i l = lines[i]; line(color_edge, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0, 0, 255), 2); } namedWindow("building2", 0); namedWindow("canny_building2", 0); namedWindow("lines", 0); imshow("building2", src); imshow("canny_building2", edge); imshow("lines", color_edge); waitKey(0); return 0;}運行結果:
參考:
http://blog.csdn.net/viewcode/article/details/8090932
http://www.cnblogs.com/xinxue/p/5229259.html
http://blog.csdn.net/poem_qianmo/article/details/26977557/
http://blog.csdn.net/zhaocj/article/details/40047397
http://blog.csdn.net/u013634684/article/details/49076329 (參數影響)
新聞熱點
疑難解答