OpenCV霍夫变换系列(前篇)-经典霍夫线变换

前言:最近新来的的我的大学室友(现在也是我的学弟)在研究霍夫线变换,我之前只是知道这玩意可以拿来做直线检测,并没有深入研究,那既然提到了,还是按照我们的老规矩,原理,示例以及OpenCV这一套流程走下来。

菜鸟一枚,好多写的不好,有点啰嗦,见谅

主要参考博客

原理部分http://www.cnblogs.com/php-rearch/p/6760683.html(相当清楚,不解释,)

源码分析1:http://blog.csdn.net/zhaocj/article/details/50281537(赵春江老师,很多源码都给出了详解,尤其是那篇sift,看的热血沸腾)

源码分析2:http://blog.csdn.net/traumland/article/details/51319644

源码分析3:http://blog.csdn.net/sunshine_in_moon/article/details/45271647

Samples1:http://blog.csdn.net/poem_qianmo/article/details/26977557/   (还是浅墨)

Samples2:http://www.cnblogs.com/skyfsm/p/6881686.html

还有一些博主未给出链接,也衷心表示感谢!



霍夫变换(Hough)

1.基本原理:

一条直线可由两个点A=(X1,Y1)和B=(X2,Y2)确定(笛卡尔坐标)



另一方面,也可以写成关于(k,q)的函数表达式(霍夫空间):


对应的变换可以通过图形直观表示:


变换后的空间成为霍夫空间。即:笛卡尔坐标系中一条直线,对应霍夫空间的一个点


反过来同样成立(霍夫空间的一条直线,对应笛卡尔坐标系的一个点):


再来看看A、B两个点,对应霍夫空间的情形:


一步步来,再看一下三个点共线的情况:


可以看出如果笛卡尔坐标系的点共线,这些点在霍夫空间对应的直线交于一点:这也是必然,共线只有一种取值可能。

如果不止一条直线呢?再看看多个点的情况(有两条直线):


其实(3,2)与(4,1)也可以组成直线,只不过它有两个点确定,而图中A、B两点是由三条直线汇成,这也是霍夫变换的后处理的基本方式:选择由尽可能多直线汇成的点。

看看,霍夫空间:选择由三条交汇直线确定的点(中间图),对应的笛卡尔坐标系的直线(右图)。



到这里问题似乎解决了,已经完成了霍夫变换的求解,但是如果像下图这种情况呢?


k=∞是不方便表示的,而且q怎么取值呢,这样不是办法。因此考虑将笛卡尔坐标系换为:极坐标表示。

备注:很多文章一来就是极坐标,感觉没有这样看着顺畅,舒服。


在极坐标系下,其实是一样的:极坐标的点--->霍夫空间的直线这个地方要注意,这个必须注意,只有垂直的时候才可能是

一一对应关系。

只不过霍夫空间不再是[k,q]的参数,而是[ρ,θ]的参数,给出对比图:


是不是就一目了然了?

有一个离散化的过程,本质上就是这样(这张图太深动形象了,怪不得后面源码分析就有博主总是在说格子数)


交点怎么求解呢?细化成坐标形式,取整后将交点对应的坐标进行累加,最后找到数值最大的点就是求解的[ρ,θ],也就求解出了直线。(OpenCV不是这样做的,但是也用到了累加的思想)


下面给出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 );  //用序列存放多条直线
    }
}

恍恍惚惚,结束了奋斗



  • 18
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值