在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

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

  1. 基于OPENCV和图像减法的PCB缺陷检测

    基于OPENCV和图像减法的PCB缺陷检测 原文地址:PCB defect detection USING OPENCV with image subtraction method Abstract ...

  2. 基于OpenCV的视障人士实时目标检测

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 一.概述 计算机视觉领域一直是一个活跃的研究领域,在本文中,我们让 ...

  3. 基于opencv实现对图片中的物体计数

    目录 1. 基本思路 2.代码 3.局限性 1. 基本思路 1. 保证图片背景尽量为纯黑或纯白 为了数量检测的方便,将垃圾的背景设置为纯色,最好是纯黑色或纯白色,一会解释原因. 2. 将RGB图片转为 ...

  4. 基于opencv的一种快速有效椭圆检测方法

    本篇介绍的椭圆检测方法来自以下论文,论文作者提供了测试代码.本文主要是对这个方法做出详解. 参考论文:A fast and effective ellipse detector for embedde ...

  5. 基于opencV的动态背景下运动目标检测及跟踪(修改版)

    基于openCV的动态背景下的运动目标检测 from: http://www.mianfeiwendang.com/doc/89c6692a222a84b2ced0d502/1 摘要:介绍在动态背景下 ...

  6. 基于Python利用OpenCV实现Hough变换的形状检测

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 今天我们将学习如何借助霍夫变换技术来检测图像中的直线和圆. 什么是 ...

  7. 利用霍夫变换做直线检测的原理及OpenCV代码实现

    图像处理开发需求.图像处理接私活挣零花钱,请加微信/QQ 2487872782 图像处理开发资料.图像处理技术交流请加QQ群,群号 271891601 霍夫变换的原理大家可以参考博文 霍夫变换_tie ...

  8. matlab利用霍夫,基于matlab的霍夫变换

    霍夫变换(限量版) 基于matlab的霍夫变换 一.简单介绍 Hough变换是图像处理中从图像中识别几何形状的基本方法之一. Hough变换的基本原理在于利用点与线的对偶性,将原始图像空间的给定的曲线 ...

  9. 利用OpenCV的霍夫变换线检测函数HoughLines()得到直线的ρ和θ值后绘制直线的原理详解

    为了更好地看懂本文,大家可以先看下我写的另一篇博文,链接如下: https://blog.csdn.net/wenhao_ir/article/details/51774444 OpenCV的霍夫变换 ...

  10. 利用SIFT和RANSAC算法(openCV框架)实现物体的检测与定位,并求出变换矩阵(findFundamentalMat和findHomography的比较)

    本文目标是通过使用SIFT和RANSAC算法,完成特征点的正确匹配,并求出变换矩阵,通过变换矩阵计算出要识别物体的边界(文章中有部分源码,整个工程我也上传了,请点击这里). SIFT算法是目前公认的效 ...

最新文章

  1. postgreSQL外键引用查询 查询外键被那些表占用
  2. GET和POST的真正区别
  3. react 项目实战(三)表单验证
  4. ROS2学习(一).Ubuntu 20.04安装ROS2 Galactic
  5. 地图上探测器扫描到的范围(洛谷P3717题题解,Java语言描述)
  6. python导入数据画柱状图代码_在Linux下使用Python的matplotlib绘制数据图的教程
  7. 信息学奥赛一本通 1116:最长平台 | OpenJudge NOI 1.9 12:最长平台 | 洛谷 B2097 最长平台
  8. D3D游戏关于窗口中如何精确确定鼠标位置的相关讨论
  9. kmz转换为dwg_徐州网站设计_seo优化常用技巧:什么软件可以将PDF文件转换为DWG文件...
  10. wdr7660虚拟服务器设置,TP-Link TL-WDR7660无线桥接怎么设置?
  11. 链表Dummy Node
  12. maven dependency problem 问题
  13. nmap学习记录(未完待续)
  14. python requests post 二进制流_Python的requests如何同时post图片二进制流和json数据application/octet-stream...
  15. Ubuntu切换中文语言
  16. Javaweb基础配置模板(mybatis+javaweb)
  17. MongoDB 写安全(Write Concern)
  18. 小程序毕设作品之微信积分商城小程序毕业设计成品(5)任务书
  19. 列表解析python_python列表解析式
  20. pretty-errors:美化python异常输出以使其清晰易读

热门文章

  1. Django-进阶 分页,中间件
  2. linux hg(mercurial)入门
  3. Spring pom配置详解(转)
  4. EF入门 IQueryable和IEnumberable的区别
  5. 优雅的实现Activiti动态调整流程(自由跳转、前进、后退、分裂、前加签、后加签等),含范例代码!...
  6. 显示MSSQL SQL语句执行的时间
  7. C#信息采集系统,常见控件练习
  8. springboot项目自定义注解实现的多数据源切换--亲测有效
  9. CSS侦测方法(侦测是否支持某个CSS属性)
  10. GDAL根据Shape文件切图(java)