OpenCV霍夫系列(後篇)-統計概率霍夫變換(HoughLinesP)
之前我也忽視了這個統計概率霍夫變換-作為霍夫系列的完結篇,今天終於算是結束了。
原理+Samples+原始碼分析
其實網上關於統計概率霍夫變換的介紹真的不多,大多數就是介紹一下引數,弄個例程式跑一下就行了。
主要參考部落格:
1.原理分析:
標準霍夫變換本質上是把影象對映到它的引數空間上,它需要計算所有的M個邊緣點,這樣它的運算量和所需記憶體空間都會很大。如果在輸入影象中只是處理m(m<M)個邊緣點,則這m個邊緣點的選取是具有一定概率性的,因此該方法被稱為概率霍夫變換(Probabilistic Hough Transform)。該方法還有一個重要的特點就是能夠檢測出線端,即能夠檢測出影象中直線的兩個端點,確切地定點陣圖像中的直線。
HoughLinesP函式就是利用概率霍夫變換來檢測直線的。它的一般步驟為:
1、隨機抽取影象中的一個特徵點,即邊緣點,如果該點已經被標定為是某一條直線上的點,則繼續在剩下的邊緣點中隨機抽取一個邊緣點,直到所有邊緣點都抽取完了為止;
2、對該點進行霍夫變換,並進行累加和計算;
3、選取在霍夫空間內值最大的點,如果該點大於閾值的,則進行步驟4,否則回到步驟1;
4、根據霍夫變換得到的最大值,從該點出發,沿著直線的方向位移,從而找到直線的兩個端點;
5、計算直線的長度,如果大於某個閾值,則被認為是好的直線輸出,回到步驟1。
2.Samples:
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:最大直線間隙,即如果有兩條線段在一條直線上,但它們之間因為有間隙,所以被認為是兩個線段,如果這個間隙大於該值,則被認為是兩條線段,否則是一條。顯然這個值越大,所判斷出的直線越少;這個值越小,所判斷出的直線越多(值越小,那麼間隙值就越容易大於這個值)。
Samples:
#include<iostream>
#include<opencv2/opencv.hpp>
#include<vector>
using namespace std;
using namespace cv;
int g_CannyThred = 150, g_CannyP = 0, g_CannySize = 0, g_HoughThred = 100, g_HoughThick = 0;
int g_Blue = 255, g_Green = 255, g_Red = 0;
int g_nWay = 0;
int g_nHoughLineMax = 10, g_nHoughLineMin = 50;
int main()
{
Mat srcImage = imread("1.jpg");
imshow("【原圖】", srcImage);
Mat grayImage;
cvtColor(srcImage, grayImage, CV_BGR2GRAY);
Mat cannyImage;
vector<Vec4i> lines;
Point point1, point2;
namedWindow("【滾動條視窗】", 0);
createTrackbar("CannyThred", "【滾動條視窗】", &g_CannyThred, 255, 0);
createTrackbar("P:", "【滾動條視窗】", &g_CannyP, 100, 0);
createTrackbar("Size", "【滾動條視窗】", &g_CannySize, 20, 0);
createTrackbar("HThred", "【滾動條視窗】", &g_HoughThred, 255, 0);
createTrackbar("Blue", "【滾動條視窗】", &g_Blue, 255, 0);
createTrackbar("Green", "【滾動條視窗】", &g_Green, 255, 0);
createTrackbar("Red", "【滾動條視窗】", &g_Red, 255, 0);
createTrackbar("Bgr/Gray", "【滾動條視窗】", &g_nWay, 1, 0);
createTrackbar("Thick", "【滾動條視窗】", &g_HoughThick, 100, 0);
createTrackbar("MaxLine", "【滾動條視窗】", &g_nHoughLineMax, 200, 0);
createTrackbar("MinLine", "【滾動條視窗】", &g_nHoughLineMin, 200, 0);
char key;
Mat CannyBgrImage;
Mat dstImage;
while (1)
{
Canny(srcImage, cannyImage, (double)g_CannyThred, (double)((g_CannyThred + 1) * (2 + g_CannyP / 100.0)), 3);
HoughLinesP(cannyImage, lines, 1, CV_PI / 180, g_HoughThred + 1, g_nHoughLineMin, g_nHoughLineMax);
cvtColor(cannyImage, CannyBgrImage, CV_GRAY2BGR);
//顯示線段
for (size_t i = 0; i < lines.size(); i++)
{
point1 = Point(lines[i][0], lines[i][1]);
point2 = Point(lines[i][2], lines[i][3]);
if (g_nWay)
dstImage = cannyImage;
else
dstImage = CannyBgrImage;
line(dstImage, point1, point2, Scalar(g_Blue, g_Green, g_Red), g_HoughThick + 1, CV_AA);
}
imshow("【處理後】", dstImage);
key = waitKey(1);
if (key == 27)
break;
}
return 0;
}
效果如下:
3.原始碼分析:
從HoughLinesP函式可以看出,該函式會呼叫cvHoughLines2函式。它通過引數CV_HOUGH_PROBABILISTIC,最終呼叫了icvHoughLinesProbabilistic函式:
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;
}
}
}
今天終於總算是完結了,吃飯了,拜拜!