霍夫变换检测直线的公式推导以及基于opencv的源代码分析并实例实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/piaoxuezhong/article/details/58587907

我理解的霍夫变换~(主要是笛卡尔坐标系跟极坐标系的相互转换)

首先要对两个坐标系有了解才行:



笛卡尔坐标和极坐标

笛卡尔坐标的点 <=> 极坐标的曲线
所谓极坐标平面化是指, 将ρ-θ的关系像x-y那样在平面内展开。
公式推导: x-y坐标中的红点(x0, y0), 代入极坐标ρ-θ中得

,

ρ,θ就是一对hough空间的变量表示。若将ρ,θ看成直角坐标空间,一个点(x0, y0)就是一个关于ρ,θ的正弦曲线。同样,直线上的其他点(Xn,Yn)也会构成一组关于ρ,θ的正弦曲线,这样势必存在一个关于ρ,θ相交(即垂直点(r,θ))。


于是乎, 一条直线能够通过在极坐标下寻找交于一点的曲线数量来检测,如果越多曲线交于一点,就意味着这个交点表示的直线由更多的点组成。我们可以通过设置直线上点的阈值来定义多少条曲线交于一点我们才认为检测到了一条直线。

opencv实现部分:

OpenCV中的霍夫线变换有如下三种:
<1>标准霍夫变换(StandardHough Transform,SHT),由HoughLines函数调用。
<2>多尺度霍夫变换(Multi-ScaleHough Transform,MSHT),由HoughLines函数调用。
<3>累计概率霍夫变换(ProgressiveProbabilistic Hough Transform,PPHT),由HoughLinesP函数调用。

icvHoughLinesStandard源代码:

我编译的opencv里面,在../openc_imgproc/hough.cpp里面找到相关源代码:
tatic void
icvHoughLinesStandard( 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 (参数影响)



猜你喜欢

转载自blog.csdn.net/piaoxuezhong/article/details/58587907