HOUGH變換檢測線段
利用 Hough 變換在二值影象中找到直線
一.函式介紹
CvSeq* cvHoughLines2( CvArr* image, void* line_storage, int method,double rho, double theta, int threshold,double param1=0, double param2=0 );
其中
(1)image
輸入 8-位元、單通道 (二值) 影象,當用CV_HOUGH_PROBABILISTIC方法檢測的時候其內容會被函式改變
(2)line_storage
檢測到的線段儲存倉. 可以是記憶體儲存倉 (此種情況下,一個線段序列在儲存倉中被建立,並且由函式返回),或者是包含線段引數的特殊型別(見下面)的具有單行/單列的矩陣(CvMat*)。矩陣頭為函式所修改,使得它的cols/rows 將包含一組檢測到的線段。如果 line_storage 是矩陣,而實際線段的數目超過矩陣尺寸,那麼最大可能數目的線段被返回(對於標準hough變換,線段按照長度降序輸出).
(3)method
Hough 變換變數,是下面變數的其中之一:
CV_HOUGH_STANDARD - 傳統或標準 Hough 變換. 每一個線段由兩個浮點數 (ρ, θ) 表示,其中 ρ 是直線與原點 (0,0) 之間的距離,θ 線段與 x-軸之間的夾角。因此,矩陣型別必須是 CV_32FC2 type.
CV_HOUGH_PROBABILISTIC - 概率 Hough 變換(如果影象包含一些長的線性分割,則效率更高). 它返回線段分割而不是整個線段。每個分割用起點和終點來表示,所以矩陣(或建立的序列)型別是 CV_32SC4.
CV_HOUGH_MULTI_SCALE - 傳統 Hough 變換的多尺度變種。線段的編碼方式與 CV_HOUGH_STANDARD 的一致。
(4)theta
弧度測量的角度精度
(5)threshold
閾值引數。如果相應的累計值大於 threshold, 則函式返回的這個線段.
(6)param1
第一個方法相關的引數:
對傳統 Hough 變換,不使用(0).
對概率 Hough 變換,它是最小線段長度.
對多尺度 Hough 變換,它是距離精度 rho 的分母 (大致的距離精度是 rho 而精確的應該是 rho / param1 ).
(7)param2
第二個方法相關引數:
對傳統 Hough 變換,不使用 (0).
對概率 Hough 變換,這個引數表示在同一條直線上進行碎線段連線的最大間隔值(gap), 即當同一條直線上的兩條碎線段之間的間隔小於param2時,將其合二為一。
對多尺度 Hough 變換,它是角度精度 theta 的分母 (大致的角度精度是 theta 而精確的角度應該是 theta /param2).
二.檢測原理
(1)直線表示方式
對於平面中的一條直線,在笛卡爾座標系中,常見的有點斜式,兩點式兩種表示方法。然而在hough變換中,考慮的是另外一種表示方式:使用(r,theta)來表示一條直線。其中r為該直線到原點的距離,theta為該直線的垂線與x軸的夾角。如下圖所示。
使用hough變換來檢測直線的思想就是:為每一個點假設n個方向的直線,通常n=180,此時檢測的直線的角度精度為1°,分別計算這n條直線的(r,theta)座標,得到n個座標點。如果要判斷的點共有N個,最終得到的(r,theta)座標有N*n個。有關這N*n個(r,theta)座標,其中theta是離散的角度,共有180個取值。
最重要的地方來了,如果多個點在一條直線上,那麼必有這多個點在theta=某個值theta_i時,這多個點的r近似相等於r_i。也就是說這多個點都在直線(r_i,theta_i)上。 三.程式舉例
#include <cv.h>
#include <highgui.h>
#include <math.h>
int main(int argc, char** argv)
{
argc=2;
argv[1]="4.jpg";
IplImage* src;
if( argc == 2 && (src=cvLoadImage(argv[1], 0))!= 0)
{
IplImage* dst = cvCreateImage( cvGetSize(src), 8, 1 );
IplImage* color_dst = cvCreateImage( cvGetSize(src), 8, 3 );
CvMemStorage* storage = cvCreateMemStorage(0);//記憶體儲存器是一個可用來儲存諸如序列,輪廓,圖形,子劃分等動態增長資料結構的底層結構。
CvSeq* lines = 0;//可動態增長元素序列
int i;
//cvCanny( src, dst, 50, 200, 3 );//邊緣檢測,變換為二值影象
cvThreshold( src, dst, 200, 255, CV_THRESH_BINARY_INV );//變換為二值影象
cvCvtColor( dst, color_dst, CV_GRAY2BGR );
cvNamedWindow( "二值影象", 1 );
cvShowImage( "二值影象", color_dst );
#if 1
//輸入8位單通道二值影象,storage線段儲存倉,CV_HOUGH_PROBABILISTIC採用概率HOUGH變換,
//1表示距離的畫素精度,CV_PI/180表示角度的弧度精度,80線段的累計返回閾值,
//後兩個引數是在概率HOUGH變換和多尺度HOUGH變換時才用到,傳統(標準)HOUGH不使用,均設為0
lines = cvHoughLines2( dst, storage, CV_HOUGH_STANDARD, 1, CV_PI/180, 100, 0, 0 );
//利用傳統的hough變換時,得到的每個線段不是線段的起點和終點的座標,而是線段所在直線與原點的距離和X軸的夾角
//故所得到的線段都是貫穿整個影象,顯示這些線段時要計算出它的起點和終點。
for( i = 0; i < lines->total; i++ )
{
float* line = (float*)cvGetSeqElem(lines,i);////返回索引所指定的元素指標
float rho = line[0];//線段所在直線與原點的距離
float theta = line[1];//線段所在直線的垂線與X軸的夾角
CvPoint pt1, pt2;
double a = cos(theta), b = sin(theta);
//當直線與X軸平行時
if( fabs(a) < 0.001 )
{
pt1.x = pt2.x = cvRound(rho);//將直線與原點的距離轉換為整數
pt1.y = 0;
pt2.y = color_dst->height;
}
//當直線與Y軸平行時
else if( fabs(b) < 0.001 )
{
pt1.y = pt2.y = cvRound(rho);
pt1.x = 0;
pt2.x = color_dst->width;
}
else
{
pt1.x = 0;
pt1.y = cvRound(rho/b);//直角三角形中,rho/y1=sin(theta)
pt2.x = cvRound(rho/a);//直角三角形中,rho/x2=cos(theta)
pt2.y = 0;
}
cvLine( color_dst, pt1, pt2, CV_RGB(255,0,0), 1, 8 );
}
#else
//輸入8位單通道二值影象,storage線段儲存倉,CV_HOUGH_PROBABILISTIC採用概率HOUGH變換,
//1表示距離的畫素精度,CV_PI/180表示角度的弧度精度,80線段的累計返回閾值,
//30是最小線段長度,10表示同一條線段上碎線段連線的最大間隔值,當限度案件的間隔小於它時將這兩個線段合二為一
lines = cvHoughLines2( dst, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 80, 30, 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, 8 );//繪製連線兩個點的線段,line[0]、line[1]是線段的兩個端點
}
#endif
cvNamedWindow( "Source", 1 );
cvShowImage( "Source", src );
cvNamedWindow( "Hough", 1 );
cvShowImage( "Hough", color_dst );
cvWaitKey(0);
}
return 0;
}
程式中採用傳統型HOUGH變化進行檢測,其結果如下:
原圖:
二值影象(此處未採用canny,而是直接進行了固定閾值二值化):
檢測效果:
通過上圖可以看出,檢測結果不正確。原因在於:在畫出這些線段時,1.錯誤理解夾角的意義,theta為該直線的垂線與X軸的夾角,而不是該直線與X軸的夾角;2.未考慮線段所在直線影象的位置關係,導致畫直線所用到的兩個端點計算錯誤。
(2)下面是我在例題基礎上修改後的例子
#include <cv.h>
#include <highgui.h>
#include <math.h>
int main(int argc, char** argv)
{
argc=2;
//argv[1]="building.jpg";
argv[1]="4.jpg";
IplImage* src;
if( argc == 2 && (src=cvLoadImage(argv[1], 0))!= 0)
{
IplImage* dst = cvCreateImage( cvGetSize(src), 8, 1 );
IplImage* color_dst = cvCreateImage( cvGetSize(src), 8, 3 );
CvMemStorage* storage = cvCreateMemStorage(0);//記憶體儲存器是一個可用來儲存諸如序列,輪廓,圖形,子劃分等動態增長資料結構的底層結構。
CvSeq* lines = 0;//可動態增長元素序列
int i;
//cvCanny( src, dst, 50, 200, 3 );//邊緣檢測,變換為二值影象
cvThreshold( src, dst, 200, 255, CV_THRESH_BINARY_INV );
cvCvtColor( dst, color_dst, CV_GRAY2BGR );
cvNamedWindow( "二值影象", 1 );
cvShowImage( "二值影象", color_dst );
#if 1
//輸入8位單通道二值影象,storage線段儲存倉,CV_HOUGH_PROBABILISTIC採用概率HOUGH變換,
//1表示距離的畫素精度,CV_PI/180表示角度的弧度精度,80線段的累計返回閾值,
//後兩個引數是在概率HOUGH變換和多尺度HOUGH變換時才用到,傳統(標準)HOUGH不使用,均設為0
lines = cvHoughLines2( dst, storage, CV_HOUGH_STANDARD, 1, CV_PI/180, 100, 0, 0 );
//利用傳統的hough變換時,得到的每個線段不是線段的起點和終點的座標,而是線段所在直線與原點的距離和Y軸的夾角
//故所得到的線段都是貫穿整個影象,顯示這些線段時要計算出它的起點和終點。
for( i = 0; i < lines->total; i++ )
{
float* line = (float*)cvGetSeqElem(lines,i);////返回索引所指定的元素指標
float rho = line[0];//線段所在直線與原點的距離
float theta = line[1];//線段所在直線的垂線與x軸的夾角與X軸的夾角
double a = cos(theta), b = sin(theta);
//printf("theta=%f\n",theta);
//printf("rho=%f\n",rho);
CvPoint pt1, pt2;
if( fabs(b) < 0.001 )
{
pt1.x = pt2.x = cvRound(rho);//將直線與原點的距離轉換為整數
pt1.y = 0;
pt2.y = color_dst->height;
}
else if( fabs(a) < 0.001 )
{
pt1.y = pt2.y = cvRound(rho);
pt1.x = 0;
pt2.x = color_dst->width;
}
else if(theta>=CV_PI/2&&rho>=0)
{
pt1.x = 0;
pt1.y = cvRound(rho/b);
pt2.x = color_dst->width;
pt2.y = cvRound(pt1.y+color_dst->width*fabs(a)/fabs(b));
}
else if(theta>=CV_PI/2&&rho<0)
{
pt1.x = cvRound(rho/a);
pt1.y = 0;
pt2.x = cvRound(pt1.x+color_dst->height*fabs(b)/fabs(a));
pt2.y = color_dst->height;
}
else if(theta<CV_PI/2&&rho>=0)
{
pt1.x = cvRound(rho/a);
pt1.y = 0;
pt2.x = 0;
pt2.y = cvRound(rho/b);
}
cvLine( color_dst, pt1, pt2, CV_RGB(255,0,0), 2, 8 );
}
#else
//輸入8位單通道二值影象,storage線段儲存倉,CV_HOUGH_PROBABILISTIC採用概率HOUGH變換,
//1表示距離的畫素精度,CV_PI/180表示角度的弧度精度,80線段的累計返回閾值,
//30是最小線段長度,10表示同一條線段上碎線段連線的最大間隔值,當限度案件的間隔小於它時將這兩個線段合二為一
lines = cvHoughLines2( dst, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 80, 30, 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), 2, 8 );//繪製連線兩個點的線段,line[0]、line[1]是線段的兩個端點
}
#endif
//cvNamedWindow( "Source", 1 );
//cvShowImage( "Source", src );
cvNamedWindow( "Hough", 1 );
cvShowImage( "Hough", color_dst );
cvWaitKey(0);
}
return 0;
}
檢測結果如下:
二值影象:
檢測效果:
(3)下面是不利用opencv自帶的cvHoughLines2函式,而是自己編寫函式進行直線檢測的例子
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
#include <vector>
#include <cmath>
using namespace cv;
using namespace std;
const double pi = 3.1415926f;
const double RADIAN = 180.0/pi;
struct line
{
int theta;
int r;
};
// r = xcos(theta) + ysin(theta)
vector<struct line> houghLine(Mat &img, int threshold)
{
vector<struct line> lines;
int diagonal = floor(sqrt((float)(img.rows*img.rows + img.cols*img.cols)));
vector< vector<int> >p(360 ,vector<int>(diagonal));
for( int j = 0; j < img.rows ; j++ ) {
for( int i = 0; i < img.cols; i++ ) {
if( img.at<unsigned char>(j,i) > 0)
{
for(int theta = 0;theta < 360;theta++)
{
int r = floor(i*cos(theta/RADIAN) + j*sin(theta/RADIAN));
if(r < 0)
continue;
p[theta][r]++;
}
}
}
}
//get local maximum
for( int theta = 0;theta < 360;theta++)
{
for( int r = 0;r < diagonal;r++)
{
int thetaLeft = max(0,theta-1);
int thetaRight = min(359,theta+1);
int rLeft = max(0,r-1);
int rRight = min(diagonal-1,r+1);
int tmp = p[theta][r];
if( tmp > threshold
&& tmp > p[thetaLeft][rLeft] && tmp > p[thetaLeft][r] && tmp > p[thetaLeft][rRight]
&& tmp > p[theta][rLeft] && tmp > p[theta][rRight]
&& tmp > p[thetaRight][rLeft] && tmp > p[thetaRight][r] && tmp > p[thetaRight][rRight])
{
struct line newline;
newline.theta = theta;
newline.r = r;
lines.push_back(newline);
}
}
}
return lines;
}
void drawLines(Mat &img, const vector<struct line> &lines)
{
for(int i = 0;i < lines.size();i++)
{
vector<Point> points;
int theta = lines[i].theta;
int r = lines[i].r;
double ct = cos(theta/RADIAN);
double st = sin(theta/RADIAN);
//r = x*ct + y*st
//left
int y = int(r/st);
if(y >= 0 && y < img.rows){
Point p(0, y);
points.push_back(p);
}
//right
y = int((r-ct*(img.cols-1))/st);
if(y >= 0 && y < img.rows){
Point p(img.cols-1, y);
points.push_back(p);
}
//top
int x = int(r/ct);
if(x >= 0 && x < img.cols){
Point p(x, 0);
points.push_back(p);
}
//down
x = int((r-st*(img.rows-1))/ct);
if(x >= 0 && x < img.cols){
Point p(x, img.rows-1);
points.push_back(p);
}
cv::line( img, points[0], points[1], Scalar(0,0,255), 1, CV_AA);
}
}
int main( int, char** argv )
{
Mat src,src_gray,edge,dst;
//argv[1]="building.jpg";
argv[1]="4.jpg";
src = imread( argv[1] );
cvtColor( src, src_gray, CV_BGR2GRAY );
blur( src_gray, src_gray, Size(3,3) );
Canny( src_gray, edge, 50, 200);
//threshold( src_gray, edge, 250, 255, CV_THRESH_BINARY_INV );
cvtColor( edge, dst, CV_GRAY2BGR );
vector<struct line> lines = houghLine(edge, 100);
drawLines(dst, lines);
namedWindow("result", 1);
imshow("result", dst);
waitKey(0);
return 0;
}
檢測效果: