OpenCV霍夫變換系列(前篇)-經典霍夫線變換
前言:最近新來的的我的大學室友(現在也是我的學弟)在研究霍夫線變換,我之前只是知道這玩意可以拿來做直線檢測,並沒有深入研究,那既然提到了,還是按照我們的老規矩,原理,示例以及OpenCV這一套流程走下來。
菜鳥一枚,好多寫的不好,有點囉嗦,見諒
主要參考部落格:
還有一些博主未給出連結,也衷心表示感謝!
霍夫變換(Hough)
1.基本原理:
一條直線可由兩個點A=(X1,Y1)和B=(X2,Y2)確定(笛卡爾座標)
另一方面,也可以寫成關於(k,q)的函式表示式(霍夫空間):
對應的變換可以通過圖形直觀表示:
變換後的空間成為霍夫空間。即:笛卡爾座標系中一條直線,對應霍夫空間的一個點
反過來同樣成立(霍夫空間的一條直線,對應笛卡爾座標系的一個點):
再來看看A、B兩個點,對應霍夫空間的情形:
一步步來,再看一下三個點共線的情況:
可以看出如果笛卡爾座標系的點共線,這些點在霍夫空間對應的直線交於一點:這也是必然,共線只有一種取值可能。
如果不止一條直線呢?再看看多個點的情況(有兩條直線):
其實(3,2)與(4,1)也可以組成直線,只不過它有兩個點確定,而圖中A、B兩點是由三條直線匯成,這也是霍夫變換的後處理的基本方式:選擇由儘可能多直線匯成的點。
看看,霍夫空間:選擇由三條交匯直線確定的點(中間圖),對應的笛卡爾座標系的直線(右圖)。
到這裡問題似乎解決了,已經完成了霍夫變換的求解,但是如果像下圖這種情況呢?
k=∞是不方便表示的,而且q怎麼取值呢,這樣不是辦法。因此考慮將笛卡爾座標系換為:極座標表示。
備註:很多文章一來就是極座標,感覺沒有這樣看著順暢,舒服。
在極座標系下,其實是一樣的:極座標的點--->霍夫空間的直線,這個地方要注意,這個必須注意,只有垂直的時候才可能是
一一對應關係。
只不過霍夫空間不再是[k,q]的引數,而是[ρ,θ]的引數,給出對比圖:
是不是就一目瞭然了?
有一個離散化的過程,本質上就是這樣(這張圖太深動形象了,怪不得後面原始碼分析就有博主總是在說格子數)
交點怎麼求解呢?細化成座標形式,取整後將交點對應的座標進行累加,最後找到數值最大的點就是求解的[ρ,θ],也就求解出了直線
下面給出OpenCV中HoughLines的霍夫變換的直線檢測步驟:
1、對邊緣影象進行霍夫空間變換;
2、在4鄰域內找到霍夫空間變換的極大值;
3、對這些極大值安裝由大到小順序進行排序,極大值越大,越有可能是直線;
這裡說明一下極大值的含義,指的是霍夫空間中相交於某一點的曲線的數目的極大值,不要理解為數學上某一條曲線的極大值
4、輸出直線。
2. Samples
OpenCV中的霍夫直線檢測的函式為HoughLines
還有一個改進版本的HoughLinesP函式(統計概論霍夫直線檢測)
函式原型分別如下:
//函式HoughLines的原型為:
void HoughLines(InputArray image,OutputArray lines, double rho, double theta, int threshold, double srn=0,double stn=0 )
/*
image為輸入影象,要求是單通道的二值影象
lines為輸出直線向量,兩個元素的向量(ρ,θ)代表一條直線,ρ是從原點(影象的左上角)的距離,θ是直線的角度(單位是弧度),0表示垂直線,π/2表示水平線
rho為距離解析度
theta為角度解析度
threshold為閾值,在步驟2中,只有大於該值的點才有可能被當作極大值,即至少有多少條正弦曲線交於一點才被認為是直線
srn和stn在多尺度霍夫變換的時候才會使用,在這裡我們只研究經典霍夫變換的原始碼
*/
//函式HoughLinesP的原型為:
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength=0, double maxLineGap=0 )
/*
第一個引數,InputArray型別的image,輸入影象,即源影象,需為8位的單通道二進位制影象,可以將任意的源圖載入進來後由函式修改成此格式後,再填在這裡。
第二個引數,InputArray型別的lines,經過呼叫HoughLinesP函式後後儲存了檢測到的線條的輸出向量,每一條線由具有四個元素的向量(x_1,y_1, x_2, y_2) 表示,其中,(x_1, y_1)和(x_2, y_2) 是是每個檢測到的線段的結束點。
第三個引數,double型別的rho,以畫素為單位的距離精度。另一種形容方式是直線搜尋時的進步尺寸的單位半徑。
第四個引數,double型別的theta,以弧度為單位的角度精度。另一種形容方式是直線搜尋時的進步尺寸的單位角度。
第五個引數,int型別的threshold,累加平面的閾值引數,即識別某部分為圖中的一條直線時它在累加平面中必須達到的值。大於閾值threshold的線段才可以被檢測通過並返回到結果中。
第六個引數,double型別的minLineLength,有預設值0,表示最低線段的長度,比這個設定引數短的線段就不能被顯現出來。
第七個引數,double型別的maxLineGap,有預設值0,允許將同一行點與點之間連線起來的最大的距離。
*/
由於函式HoughLines只輸出直線所對應的角度和距離,所以在進行直線檢測的時候還要把其轉換為直角座標系下的資料,另外輸入影象還必須是邊緣影象,下面就是具體的例項:
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/core/core.hpp>
#include <iostream>
#include <Windows.h>
using namespace cv;
using namespace std;
Mat src, dst, cdst;
const char* filename = "1.jpg";
const char *winname="hough";
const char *trackbarname="1.houghlines\n2.houghlinep\n3.houghcircle";
int savevalue=100,houghtype;
const int maxthreshold=150;
void help()
{
cout << "\nThis program demonstrates line finding with the Hough transform.\n";
}
void choisehoughlines()
{
vector<Vec2f> lines;
Canny(src, dst,50, 200, 3);
cvtColor(dst, cdst, CV_GRAY2BGR);
HoughLines(dst, lines, 1, CV_PI/180, savevalue+10, 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( cdst, pt1, pt2, Scalar(0,0,255), 1, CV_AA);
float angle = theta / CV_PI *180;
//cout<<"line "<< i << ": "<<"rho: "<<rho<<"theta: "<<theta
//<<"angle: "<< angle<<endl;
}
imshow(winname, cdst);
}
void choisehoughlinep()
{
vector<Vec4i> lines;
Canny(src, dst,50, 200, 3);
cvtColor(dst, cdst, CV_GRAY2BGR);
HoughLinesP(dst, lines, 1, CV_PI/180, savevalue+10, savevalue+10, 10 );
for( size_t i = 0; i < lines.size(); i++ )
{
Vec4i l = lines[i];
line( cdst, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(0,0,255), 1, CV_AA);
}
imshow(winname, cdst);
}
void choisehoughcircle()
{
vector<Vec3f> circles;
cvtColor(src, cdst, CV_GRAY2BGR);
/// Apply the Hough Transform to find the circles
HoughCircles( src, circles, CV_HOUGH_GRADIENT, 1, dst.rows/10, 200, savevalue+10, 0, 0 );
/// Draw the circles detected
printf("%d",circles.size());
for( size_t i = 0; i < circles.size(); i++ )
{
Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
// circle center
circle( cdst, center, 3, Scalar(0,255,0), -1, 8, 0 );
// circle outline
circle( cdst, center, radius, Scalar(0,0,255), 3, 8, 0 );
}
imshow(winname, cdst);
}
void choice(int,void *)
{
switch (houghtype)
{
case 0:choisehoughlines();break;
case 1:choisehoughlinep();break;
//case 2:choisehoughcircle();break;
}
}
int main(int argc, char** argv)
{
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),FOREGROUND_GREEN |FOREGROUND_INTENSITY);
//system("color 3E");
help();
src = imread(filename, 0);
if(src.empty())
{
help();
cout << "can not open " << filename << endl;
return -1;
}
namedWindow(winname);
createTrackbar(trackbarname,winname,&houghtype,2,choice);
createTrackbar("thresholdvalue",winname,&savevalue,maxthreshold,choice);
imshow("source", src);
choice(0,0);
while(char(waitKey())!= 'q');
return 0;
}
效果如下:
這是一個綜合示例,我把霍夫圓變換註釋掉了,那個放在下一篇單獨講解,其中輸出經典霍夫線變換的角度的程式碼我也註釋掉了,
需要的可以自己加上。
對於經典的線變換計算得到的兩點的座標為(ρcosθ-1000sinθ,ρsinθ+1000cosθ),(ρcosθ+1000sinθ,ρsinθ-1000cosθ),我個人覺得
計算過程如下:
結論:
houghlines的計算效率比較低O(n*n*m),耗時較長,而且沒有檢測出直線的端點。
統計概論霍夫直線檢測houghlinesP是一個改進,不僅執行效率較高,而且能檢測到直線的兩個端點。
思想:
先隨機檢測出一部分直線,然後將直線上點的排查掉,再進行其他直線的檢測
1. 首先僅統計影象中非零點的個數,對於已經確認是某條直線上的點就不再變換了。
2. 對所以有非零點逐個變換到霍夫空間
a. 並累加到霍夫統計表(影象)中,並統計最大值
b. 最大值與閾值比較,小於閾值,則繼續下一個點的變換
c. 若大於閾值,則有一個新的直線段要產生了
d. 計算直線上線段的端點、長度,如果符合條件,則儲存此線段,並mark這個線段上的點不參與其他線段檢測的變換。
3.原始碼分析
HoughLines函式是在sources/modules/imgproc/src/hough.cpp檔案中定義的:
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;
//呼叫由C語音寫出的霍夫變換的函式
CvSeq* seq = cvHoughLines2( &c_image, storage, srn == 0 && stn == 0 ?
CV_HOUGH_STANDARD : CV_HOUGH_MULTI_SCALE,
rho, theta, threshold, srn, stn );
//把由cvHoughLines2函式得到的霍夫變換直線序列轉換為陣列
seqToMat(seq, _lines);
}
cvHoughLines2函式是霍夫變換直線檢測的關鍵,函式的輸出為所檢測到的直線序列,它的第3個形參“srn == 0 && stn == 0 ? CV_HOUGH_STANDARD :CV_HOUGH_MULTI_SCALE”表示的是該霍夫變換是經典霍夫變換還是多尺度霍夫變換,它是由變數srn和stn決定的,只要這兩個變數有一個不為0,就進行多尺度霍夫變換,否則為經典霍夫變換。另外cvHoughLines2函式不僅可以用於經典霍夫變換和多尺度霍夫變換,還可以用於概率霍夫變換。
CV_IMPL CvSeq*
cvHoughLines2( CvArr* src_image, void* lineStorage, int method,
double rho, double theta, int threshold,
double param1, double param2 )
{
CvSeq* result = 0;
CvMat stub, *img = (CvMat*)src_image;
CvMat* mat = 0;
CvSeq* lines = 0;
CvSeq lines_header;
CvSeqBlock lines_block;
int lineType, elemSize;
int linesMax = INT_MAX; //輸出最多直線的數量,設為無窮多
int iparam1, iparam2;
img = cvGetMat( img, &stub );
//確保輸入影象是8位單通道
if( !CV_IS_MASK_ARR(img))
CV_Error( CV_StsBadArg, "The source image must be 8-bit, single-channel" );
//記憶體空間申請成功,以備輸出之用
if( !lineStorage )
CV_Error( CV_StsNullPtr, "NULL destination" );
//確保rho,theta,threshold這三個引數大於0
if( rho <= 0 || theta <= 0 || threshold <= 0 )
CV_Error( CV_StsOutOfRange, "rho, theta and threshold must be positive" );
if( method != CV_HOUGH_PROBABILISTIC ) //經典霍夫變換和多尺度霍夫變換
{
//輸出直線的型別為32位浮點雙通道,即ρ和θ兩個浮點型變數
lineType = CV_32FC2;
elemSize = sizeof(float)*2;
}
else //概率霍夫變換
{
//輸出直線的型別為32位有符號整型4通道,即兩個畫素點的4個座標
lineType = CV_32SC4;
elemSize = sizeof(int)*4;
}
//判斷lineStorage的型別,經分析lineStorage只可能是STORAGE型別
if( CV_IS_STORAGE( lineStorage ))
{
//在lineStorage記憶體中建立一個序列,用於儲存霍夫變換的直線,lines為該序列的指標
lines = cvCreateSeq( lineType, sizeof(CvSeq), elemSize, (CvMemStorage*)lineStorage );
}
else if( CV_IS_MAT( lineStorage ))
{
mat = (CvMat*)lineStorage;
if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 && mat->cols != 1) )
CV_Error( CV_StsBadArg,
"The destination matrix should be continuous and have a single row or a single column" );
if( CV_MAT_TYPE( mat->type ) != lineType )
CV_Error( CV_StsBadArg,
"The destination matrix data type is inappropriate, see the manual" );
lines = cvMakeSeqHeaderForArray( lineType, sizeof(CvSeq), elemSize, mat->data.ptr,
mat->rows + mat->cols - 1, &lines_header, &lines_block );
linesMax = lines->total;
cvClearSeq( lines );
}
else
CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* nor CvMat*" );
iparam1 = cvRound(param1);
iparam2 = cvRound(param2);
switch( method )
{
case CV_HOUGH_STANDARD: //經典霍夫變換
icvHoughLinesStandard( img, (float)rho,
(float)theta, threshold, lines, linesMax );
break;
case CV_HOUGH_MULTI_SCALE: //多尺度霍夫變換
icvHoughLinesSDiv( img, (float)rho, (float)theta,
threshold, iparam1, iparam2, lines, linesMax );
break;
case CV_HOUGH_PROBABILISTIC: //概率霍夫變換
icvHoughLinesProbabilistic( img, (float)rho, (float)theta,
threshold, iparam1, iparam2, lines, linesMax );
break;
default:
CV_Error( CV_StsBadArg, "Unrecognized method id" );
}
//在前面判斷lineStorage型別時,已經確定它是STORAGE型別,因此沒有進入else if內,也就是沒有對mat賦值,所以在這裡進入的是else
if( mat )
{
if( mat->cols > mat->rows )
mat->cols = lines->total;
else
mat->rows = lines->total;
}
else
result = lines;
//返回lines序列的指標,該序列儲存有霍夫變換的直線
return result;
}
上面這兩段程式碼我沒怎麼研究,我只是分析了經典霍夫變換的icvHoughLinesStandard函式:
並且其中有一個地方還是不明白,已經標註出來,希望有人解釋。
static void icvHoughLinesStandard( const CvMat* img, float rho, float theta,
int threshold, CvSeq *lines, int lineMax)
{
cv::AutoBuffer<int> _accum, _sort_buf;
cv::AutoBuffer<float> _tabSin, _tabCos;
const uchar* image;
int step, width, height;
int numangle, numrho;
int total=0;
float ang;
int r, n;
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); //r的空間範圍
/*
allocator類是一個模板類,定義在標頭檔案memory中,用於記憶體的分配、釋放、管理,它幫助我們將記憶體分配和物件構造分離開來。具體地說,allocator類將記憶體的分配和物件的構造解耦,分別用allocate和construct兩個函式完成,同樣將記憶體的釋放和物件的析構銷燬解耦,分別用deallocate和destroy函式完成。
*/
//為累加器陣列分配記憶體空間
//該累加器陣列其實就是霍夫空間,它是用一維陣列表示二維空間
_accum.allocate((numangle+2) * (numrho + 2));
//為排序陣列分配記憶體空間
_sort_buf.allocate(numangle * numrho);
//為正弦和餘弦列表分配記憶體空間
_tabSin.allocate(numangle);
_tabCos.allocate(numangle);
//分別定義上述記憶體空間的地址指標
int *accum = _accum, *sort_buf = _sort_buf;
int *tabSin = _tabSin, *tabCos = _tabCos;
//累加器陣列清零
memset( accum, 0, sizeof(accum[0]) * (numangle+2) * (numrho+2) );
//為避免重複運算,事先計算好sinθi/ρ和cosθi/ρ
for( ang = 0, n = 0; n < numangle; ang += theta, n++ ) //計算正弦曲線的準備工作,查表
{
tabSin[n] = (float)(sin(ang) * irho);
tabCos[n] = (float)(cos(ang) * irho);
}
//stage 1. fill accumulator
//執行步驟1,逐點進行霍夫空間變換,並把結果放入累加器陣列內
for( i = 0 ; i < height; i++)
for( j = 0; j < width; j++)
{
//只對影象的非零值處理,即只對影象的邊緣畫素進行霍夫變換
if( image[i * step + j] != 0 ) //將每個非零點,轉換為霍夫空間的離散正弦曲線,並統計。
for( n = 0; n < numangle; n++ )
{
//根據公式: ρ = xcosθ + ysinθ
//cvRound()函式:四捨五入
r = cvRound( j * tabCos[n] + i * tabSin[n])
//numrho是ρ的最大值,或者說最大取值範圍
r += (numrho - 1) / 2; //這一步是真的看不太明白???
//(另一位博主的解釋)哈哈,這裡讓我想了好久,為什麼這樣安排呢? 可能是因為θ、ρ正負問題 ,但我感覺解釋不通
//r表示的是距離,n表示的是角點,在累加器內找到它們所對應的位置(即霍夫空間內的位置),其值加1
accum[(n+1) * (numrho+2)+ r + 1]++;
/*
最初我也是下面這樣理解的,覺得比較好理解,直觀,但是是一維陣列
哪來的行與列額!
n+1是為了第一行空出來
// numrho+2 是總共的列數,這裡實際意思應該是半徑的所有可能取值,加2是為了防止越界,但是說成列數我覺得也沒錯,並且我覺得這樣比較好理解
//r+1 是為了把第一列空出來
//因為程式後面要比較前一行與前一列的資料, 這樣省的越界
因此空出首行和首列*/
}
}
// stage 2. find local maximums
// 執行步驟2,找到區域性極大值,即非極大值抑制
// 霍夫空間,區域性最大點,採用四領域判斷,比較。(也可以使8鄰域或者更大的方式),如果不判斷區域性最大值,同時選用次大值與最大值,就可能會是兩個相鄰的直線,但實際是一條直線。選用最大值,也是去除離散的近似計算帶來的誤差,或合併近似曲線。
for( r = 0 ; r < numrho; r++ )
for( 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
sort_buf[total++] = base;
}
//stage 3. sort the detected lines by accumulator value
//執行步驟3,對儲存在sort_buf陣列內的累加器的資料按由大到小的順序進行排序
icvHoughSortDescent32s( sort_buf, total, accum );
/*OpenCV中自帶了一個排序函式,名稱為:
void icvHoughSortDescent32s(int *sequence , int sum , int*data),引數解釋:
第三個引數:陣列的首地址
第二個引數:要排序的資料總數目
第一個引數:此陣列存放data中要參與排序的資料的序號
而且這個排序演算法改變的只是sequence[]陣列中的元素,源資料data[]未發生絲毫改變。
*/
// stage 4. store the first min(total, linesMax ) lines to the output buffer,輸出直線
lineMax = MIN(lineMax, total); //linesMax是輸入引數,表示最多輸出幾條直線
//事先定義一個尺度
scale = 1./(numrho+2);
for(i=0; i<linesMax; i++) // 依據霍夫空間解析度,計算直線的實際r,theta引數
{
//CvLinePolar 直線的資料結構
//CvLinePolar結構在該檔案的前面被定義
CvLinePolar line;
//idx為極大值在累加器陣列的位置
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 ); //用序列存放多條直線
}
}