基于opencv利用霍夫变换实现圆形物体的检测

http://blog.csdn.net/piaoxuezhong/article/details/58587907中对霍夫变换实现直线检测进行了汇总,这篇对霍夫变换实现圆形检测进行汇总~

总体来讲,检测圆形和检测直线的实现原理相似,在笛卡尔坐标下,圆的表示方程为:(x-a)²+(y-b)²=r²;但在极坐标下,假设已知圆心(x0,y0),那么圆上的点可以表示为:


所以对于任意一个圆, 假设中心像素点p(x0, y0)像素点已知, 圆半径已知,则旋转360度,由极坐标方程可以得到每个点上的坐标。同样,如果只是知道图像上像素点, 圆半径,旋转360°,则会有一个集中的交点,即圆心,也就是说圆点处的坐标值最强,这正是霍夫变换检测圆的数学原理。

基于opencv实现圆形检测:

HoughCircles函数实现了圆形检测,它使用的算法是改进的霍夫变换,该算法把霍夫变换分为两个阶段,从而减小了霍夫空间的维数。第一阶段用于检测圆心,第二阶段从圆心推导出圆半径。

检测圆心的方法是圆心是它所在圆周所有法线的交汇处,因此只要找到这个交点,即可确定圆心,该方法所用的霍夫空间与图像空间的性质相同,因此它仅仅是二维空间。检测圆半径的方法是从圆心到圆周上的任意一点的距离相同,首先确定一个阈值,只要计算得到相同距离的数量大于该阈值,就认为该距离就是该圆心所对应的圆半径,并且该方法只需要计算半径直方图,不使用霍夫空间。圆心和圆半径都得到后,就能确定圆形了。

霍夫变换具体步骤:
第一阶段:检测圆心
1.1、对输入图像边缘检测;
1.2、计算图形的梯度,并确定圆周线,其中圆周的梯度就是它的法线;
1.3、在二维霍夫空间内绘出所有图形的梯度直线,坐标点累加和的值越大,则该点上直线相交的次数越多,该点越有可能是圆心;
1.4、在霍夫空间的4邻域内进行非最大值抑制;
1.5、设定一个阈值,霍夫空间内累加和大于该阈值的点就对应于圆心。
第二阶段:检测圆半径
2.1、计算某一个圆心到所有圆周线的距离,这些距离中就有该圆心所对应的圆的半径的值,这些半径值当然是相等的,并且这些圆半径的数量要远远大于其他距离值相等的数量;
2.2、设定两个阈值:最大半径和最小半径。保留距离在这两个半径之间的值,这意味着我们检测的圆不能太大,也不能太小;
2.3、对保留下来的距离进行排序;
2.4、找到距离相同的那些值,并计算相同值的数量;
2.5、设定一个阈值,只有相同值的数量大于该阈值,就认为该值是该圆心对应的圆半径;
2.6、对每一个圆心,完成上面的2.1~2.5步骤,得到所有的圆半径。

opencv函数源码:

void cv::HoughCircles( InputArray _image, OutputArray _circles,
                       int method, double dp, double min_dist,
                       double param1, double param2,
                       int minRadius, int maxRadius )
{
    Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);
    Mat image = _image.getMat();
    CvMat c_image = image;
    CvSeq* seq = cvHoughCircles( &c_image, storage, method,
                    dp, min_dist, param1, param2, minRadius, maxRadius );
    seqToMat(seq, _circles);
}
image为输入图像,要求是灰度图像;
circles为输出圆向量,每个向量包括三个浮点型的元素:圆心横坐标,圆心纵坐标和圆半径;
method为使用霍夫变换圆检测的算法,Opencv2.4.13的参数是CV_HOUGH_GRADIENT;
dp为第一阶段所使用的霍夫空间的分辨率,dp=1时表示霍夫空间与输入图像空间的大小一致,dp=2时霍夫空间是输入图像空间的一半,以此类推;
minDist为圆心之间的最小距离,如果检测到的两个圆心之间距离小于该值,则认为它们是同一个圆心;
param1为边缘检测时使用Canny算子的高阈值;
param2为步骤1.5和步骤2.5中所共有的阈值;
minRadius和maxRadius为所检测到的圆半径的最小值和最大值,默认值0
在opencv检测圆形调用的函数为:cvHoughCircles跟icvHoughCirclesGradient.具体实现可以参见:点击打开链接,这里只附上

icvHoughCirclesGradient的代码及注释。

static void  
icvHoughCirclesGradient( CvMat* img, float dp, float min_dist,  
                         int min_radius, int max_radius,  
                         int canny_threshold, int acc_threshold,  
                         CvSeq* circles, int circles_max )  
{  
    //为了提高运算精度,定义一个数值的位移量  
    const int SHIFT = 10, ONE = 1 << SHIFT;  
    //定义水平梯度和垂直梯度矩阵的地址指针  
    cv::Ptr<CvMat> dx, dy;  
    //定义边缘图像、累加器矩阵和半径距离矩阵的地址指针  
    cv::Ptr<CvMat> edges, accum, dist_buf;  
    //定义排序向量  
    std::vector<int> sort_buf;  
    cv::Ptr<CvMemStorage> storage;  
  
    int x, y, i, j, k, center_count, nz_count;  
    //事先计算好最小半径和最大半径的平方  
    float min_radius2 = (float)min_radius*min_radius;  
    float max_radius2 = (float)max_radius*max_radius;  
    int rows, cols, arows, acols;  
    int astep, *adata;  
    float* ddata;  
    //nz表示圆周序列,centers表示圆心序列  
    CvSeq *nz, *centers;  
    float idp, dr;  
    CvSeqReader reader;  
    //创建一个边缘图像矩阵  
    edges = cvCreateMat( img->rows, img->cols, CV_8UC1 );  
    //第一阶段  
    //步骤1.1,用canny边缘检测算法得到输入图像的边缘图像  
    cvCanny( img, edges, MAX(canny_threshold/2,1), canny_threshold, 3 );  
    //创建输入图像的水平梯度图像和垂直梯度图像  
    dx = cvCreateMat( img->rows, img->cols, CV_16SC1 );  
    dy = cvCreateMat( img->rows, img->cols, CV_16SC1 );  
    //步骤1.2,用Sobel算子法计算水平梯度和垂直梯度  
    cvSobel( img, dx, 1, 0, 3 );  
    cvSobel( img, dy, 0, 1, 3 );  
    /确保累加器矩阵的分辨率不小于1  
    if( dp < 1.f )  
        dp = 1.f;  
    //分辨率的倒数  
    idp = 1.f/dp;  
    //根据分辨率,创建累加器矩阵  
    accum = cvCreateMat( cvCeil(img->rows*idp)+2, cvCeil(img->cols*idp)+2, CV_32SC1 );  
    //初始化累加器为0  
    cvZero(accum);  
    //创建两个序列,  
    storage = cvCreateMemStorage();  
    nz = cvCreateSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage );  
    centers = cvCreateSeq( CV_32SC1, sizeof(CvSeq), sizeof(int), storage );  
  
    rows = img->rows;    //图像的高  
    cols = img->cols;    //图像的宽  
    arows = accum->rows - 2;    //累加器的高  
    acols = accum->cols - 2;    //累加器的宽  
    adata = accum->data.i;    //累加器的地址指针  
    astep = accum->step/sizeof(adata[0]);    /累加器的步长  
    // Accumulate circle evidence for each edge pixel  
    //步骤1.3,对边缘图像计算累加和  
    for( y = 0; y < rows; y++ )  
    {  
        //提取出边缘图像、水平梯度图像和垂直梯度图像的每行的首地址  
        const uchar* edges_row = edges->data.ptr + y*edges->step;  
        const short* dx_row = (const short*)(dx->data.ptr + y*dx->step);  
        const short* dy_row = (const short*)(dy->data.ptr + y*dy->step);  
  
        for( x = 0; x < cols; x++ )  
        {  
            float vx, vy;  
            int sx, sy, x0, y0, x1, y1, r;  
            CvPoint pt;  
            //当前的水平梯度值和垂直梯度值  
            vx = dx_row[x];  
            vy = dy_row[x];  
            //如果当前的像素不是边缘点,或者水平梯度值和垂直梯度值都为0,则继续循环。因为如果满足上面条件,该点一定不是圆周上的点  
            if( !edges_row[x] || (vx == 0 && vy == 0) )  
                continue;  
            //计算当前点的梯度值  
            float mag = sqrt(vx*vx+vy*vy);  
            assert( mag >= 1 );  
            //定义水平和垂直的位移量  
            sx = cvRound((vx*idp)*ONE/mag);  
            sy = cvRound((vy*idp)*ONE/mag);  
            //把当前点的坐标定位到累加器的位置上  
            x0 = cvRound((x*idp)*ONE);  
            y0 = cvRound((y*idp)*ONE);  
            // Step from min_radius to max_radius in both directions of the gradient  
            //在梯度的两个方向上进行位移,并对累加器进行投票累计  
            for(int k1 = 0; k1 < 2; k1++ )  
            {  
                //初始一个位移的启动  
                //位移量乘以最小半径,从而保证了所检测的圆的半径一定是大于最小半径  
                x1 = x0 + min_radius * sx;  
                y1 = y0 + min_radius * sy;  
                //在梯度的方向上位移  
                // r <= max_radius保证了所检测的圆的半径一定是小于最大半径  
                for( r = min_radius; r <= max_radius; x1 += sx, y1 += sy, r++ )  
                {  
                    int x2 = x1 >> SHIFT, y2 = y1 >> SHIFT;  
                    //如果位移后的点超过了累加器矩阵的范围,则退出  
                    if( (unsigned)x2 >= (unsigned)acols ||  
                        (unsigned)y2 >= (unsigned)arows )  
                        break;  
                    //在累加器的相应位置上加1  
                    adata[y2*astep + x2]++;  
                }  
                //把位移量设置为反方向  
                sx = -sx; sy = -sy;  
            }  
            //把输入图像中的当前点(即圆周上的点)的坐标压入序列圆周序列nz中  
            pt.x = x; pt.y = y;  
            cvSeqPush( nz, &pt );  
        }  
    }  
    //计算圆周点的总数  
    nz_count = nz->total;  
    //如果总数为0,说明没有检测到圆,则退出该函数  
    if( !nz_count )  
        return;  
    //Find possible circle centers  
    //步骤1.4和1.5,遍历整个累加器矩阵,找到可能的圆心  
    for( y = 1; y < arows - 1; y++ )  
    {  
        for( x = 1; x < acols - 1; x++ )  
        {  
            int base = y*(acols+2) + x;  
            //如果当前的值大于阈值,并在4邻域内它是最大值,则该点被认为是圆心  
            if( adata[base] > acc_threshold &&  
                adata[base] > adata[base-1] && adata[base] > adata[base+1] &&  
                adata[base] > adata[base-acols-2] && adata[base] > adata[base+acols+2] )  
                //把当前点的地址压入圆心序列centers中  
                cvSeqPush(centers, &base);  
        }  
    }  
    //计算圆心的总数  
    center_count = centers->total;  
    //如果总数为0,说明没有检测到圆,则退出该函数  
    if( !center_count )  
        return;  
    //定义排序向量的大小  
    sort_buf.resize( MAX(center_count,nz_count) );  
    //把圆心序列放入排序向量中  
    cvCvtSeqToArray( centers, &sort_buf[0] );  
    //对圆心按照由大到小的顺序进行排序  
    //它的原理是经过icvHoughSortDescent32s函数后,以sort_buf中元素作为adata数组下标,adata中的元素降序排列,即adata[sort_buf[0]]是adata所有元素中最大的,adata[sort_buf[center_count-1]]是所有元素中最小的  
    icvHoughSortDescent32s( &sort_buf[0], center_count, adata );  
    //清空圆心序列  
    cvClearSeq( centers );  
    //把排好序的圆心重新放入圆心序列中  
    cvSeqPushMulti( centers, &sort_buf[0], center_count );  
    //创建半径距离矩阵  
    dist_buf = cvCreateMat( 1, nz_count, CV_32FC1 );  
    //定义地址指针  
    ddata = dist_buf->data.fl;  
  
    dr = dp;    //定义圆半径的距离分辨率  
    //重新定义圆心之间的最小距离  
    min_dist = MAX( min_dist, dp );  
    //最小距离的平方  
    min_dist *= min_dist;  
    // For each found possible center  
    // Estimate radius and check support  
    //按照由大到小的顺序遍历整个圆心序列  
    for( i = 0; i < centers->total; i++ )  
    {  
        //提取出圆心,得到该点在累加器矩阵中的偏移量  
        int ofs = *(int*)cvGetSeqElem( centers, i );  
        //得到圆心在累加器中的坐标位置  
        y = ofs/(acols+2);  
        x = ofs - (y)*(acols+2);  
        //Calculate circle's center in pixels  
        //计算圆心在输入图像中的坐标位置  
        float cx = (float)((x + 0.5f)*dp), cy = (float)(( y + 0.5f )*dp);  
        float start_dist, dist_sum;  
        float r_best = 0;  
        int max_count = 0;  
        // Check distance with previously detected circles  
        //判断当前的圆心与之前确定作为输出的圆心是否为同一个圆心  
        for( j = 0; j < circles->total; j++ )  
        {  
            //从序列中提取出圆心  
            float* c = (float*)cvGetSeqElem( circles, j );  
            //计算当前圆心与提取出的圆心之间的距离,如果两者距离小于所设的阈值,则认为两个圆心是同一个圆心,退出循环  
            if( (c[0] - cx)*(c[0] - cx) + (c[1] - cy)*(c[1] - cy) < min_dist )  
                break;  
        }  
        //如果j < circles->total,说明当前的圆心已被认为与之前确定作为输出的圆心是同一个圆心,则抛弃该圆心,返回上面的for循环  
        if( j < circles->total )  
            continue;  
        // Estimate best radius  
        //第二阶段  
        //开始读取圆周序列nz  
        cvStartReadSeq( nz, &reader );  
        for( j = k = 0; j < nz_count; j++ )  
        {  
            CvPoint pt;  
            float _dx, _dy, _r2;  
            CV_READ_SEQ_ELEM( pt, reader );  
            _dx = cx - pt.x; _dy = cy - pt.y;  
            //步骤2.1,计算圆周上的点与当前圆心的距离,即半径  
            _r2 = _dx*_dx + _dy*_dy;  
            //步骤2.2,如果半径在所设置的最大半径和最小半径之间  
            if(min_radius2 <= _r2 && _r2 <= max_radius2 )  
            {  
                //把半径存入dist_buf内  
                ddata[k] = _r2;  
                sort_buf[k] = k;  
                k++;  
            }  
        }  
        //k表示一共有多少个圆周上的点  
        int nz_count1 = k, start_idx = nz_count1 - 1;  
        //nz_count1等于0也就是k等于0,说明当前的圆心没有所对应的圆,意味着当前圆心不是真正的圆心,所以抛弃该圆心,返回上面的for循环  
        if( nz_count1 == 0 )  
            continue;  
        dist_buf->cols = nz_count1;    //得到圆周上点的个数  
        cvPow( dist_buf, dist_buf, 0.5 );    //求平方根,得到真正的圆半径  
        //步骤2.3,对圆半径进行排序  
        icvHoughSortDescent32s( &sort_buf[0], nz_count1, (int*)ddata );  
  
        dist_sum = start_dist = ddata[sort_buf[nz_count1-1]];  
        //步骤2.4  
        for( j = nz_count1 - 2; j >= 0; j-- )  
        {  
            float d = ddata[sort_buf[j]];  
  
            if( d > max_radius )  
                break;  
            //d表示当前半径值,start_dist表示上一次通过下面if语句更新后的半径值,dr表示半径距离分辨率,如果这两个半径距离之差大于距离分辨率,说明这两个半径一定不属于同一个圆,而两次满足if语句条件之间的那些半径值可以认为是相等的,即是属于同一个圆  
            if( d - start_dist > dr )  
            {  
                //start_idx表示上一次进入if语句时更新的半径距离排序的序号  
                // start_idx – j表示当前得到的相同半径距离的数量  
                //(j + start_idx)/2表示j和start_idx中间的数  
                //取中间的数所对应的半径值作为当前半径值r_cur,也就是取那些半径值相同的值  
                float r_cur = ddata[sort_buf[(j + start_idx)/2]];  
                //如果当前得到的半径相同的数量大于最大值max_count,则进入if语句  
                if( (start_idx - j)*r_best >= max_count*r_cur ||  
                    (r_best < FLT_EPSILON && start_idx - j >= max_count) )  
                {  
                    r_best = r_cur;    //把当前半径值作为最佳半径值  
                    max_count = start_idx - j;    //更新最大值  
                }  
                //更新半径距离和序号  
                start_dist = d;  
                start_idx = j;  
                dist_sum = 0;  
            }  
            dist_sum += d;  
        }  
        // Check if the circle has enough support  
        //步骤2.5,最终确定输出  
        //如果相同半径的数量大于所设阈值  
        if( max_count > acc_threshold )  
        {  
            float c[3];  
            c[0] = cx;    //圆心的横坐标  
            c[1] = cy;    //圆心的纵坐标  
            c[2] = (float)r_best;    //所对应的圆的半径  
            cvSeqPush( circles, c );    //压入序列circles内  
            //如果得到的圆大于阈值,则退出该函数  
            if( circles->total > circles_max )  
                return;  
        }  
    }  
}  
使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。通过第八个参数minRadius和第九个参数maxRadius指定最小和最大的圆半径,来辅助圆检测的效果。或者可以直接忽略返回半径,因为它们都有着默认值0,单单用HoughCircles函数检测出来的圆心,然后用额外的一些步骤来进一步确定半径。
霍夫梯度法的缺点(浅墨大神的观点)
<1> 在霍夫梯度法中,使用Sobel导数来计算局部梯度,其可以视作等同于一条局部切线,这不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。
<2> 在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。第三,因为每一个中心只选择一个圆,如果有同心圆,就只能选择其中的一个。
<3> 因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,若是对于无穷分辨率的平滑图像而言的话,这才是必须的。

实例:

#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

int main(int argc, char** argv)
{

	Mat srcImage = imread("F://IM_VIDEO//yingbi1.jpg");  
	Mat midImage, dstImage;
	imshow("【原始图】", srcImage);

	cvtColor(srcImage, midImage, CV_BGR2GRAY);//转化边缘检测后的图为灰度图
	GaussianBlur(midImage, midImage, Size(9, 9), 2, 2);
	vector<Vec3f> circles;
	HoughCircles(midImage, circles, CV_HOUGH_GRADIENT, 1, midImage.rows/20, 100, 100, 0, 0);
	//依次在图中绘制出圆
	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(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
		//绘制圆轮廓
		circle(srcImage, center, radius, Scalar(155, 50, 255), 3, 8, 0);
	}

	imshow("【效果图】", srcImage);
	waitKey(0);
	return 0;
}

运行结果:


参考:

http://blog.csdn.net/zhaocj/article/details/50454847

http://blog.csdn.net/poem_qianmo/article/details/26977557

发布了156 篇原创文章 · 获赞 722 · 访问量 131万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览