霍夫圆变换

以下博客转载于:牧野的博客,zhaocj的博客和执剑天涯的博客
这三篇博客可能是最好的了。




霍夫圆变换的基本思路是认为图像上每一个非零像素点都有可能是一个潜在的圆上的一点,跟霍夫线变换一样,也是通过投票,生成累积坐标平面,设置一个累积权重来定位圆。


在笛卡尔坐标系中圆的方程为:





其中(a,b)是圆心,r是半径,也可以表述为:







所以在abr组成的三维坐标系中,一个点可以唯一确定一个圆。

而在笛卡尔的xy坐标系中经过某一点的所有圆映射到abr坐标系中就是一条三维的曲线:




经过xy坐标系中所有的非零像素点的所有圆就构成了abr坐标系中很多条三维的曲线。


在xy坐标系中同一个圆上的所有点的圆方程是一样的,它们映射到abr坐标系中的是同一个点,所以在abr坐标系中该点就应该有圆的总像素N0个曲线相交。通过判断abr中每一点的相交(累积)数量,大于一定阈值的点就认为是圆。


以上是标准霍夫圆变换实现算法,问题是它的累加面试一个三维的空间,意味着比霍夫线变换需要更多的计算消耗。Opencv霍夫圆变换对标准霍夫圆变换做了运算上的优化。它采用的是“霍夫梯度法”。它的检测思路是去遍历累加所有非零点对应的圆心,对圆心进行考量。如何定位圆心呢?圆心一定是在圆上的每个点的模向量上,即在垂直于该点并且经过该点的切线的垂直线上,这些圆上的模向量的交点就是圆心。


霍夫梯度法就是要去查找这些圆心,根据该“圆心”上模向量相交数量的多少,根据阈值进行最终的判断。



以下是Opencv中霍夫圆变换的实现:



  
  
  1. #include "opencv2/imgproc/imgproc.hpp"
  2. #include "opencv2/highgui/highgui.hpp"
  3. using namespace cv;
  4. using namespace std;
  5. int main( int argc, char* argv[] )
  6. {
  7. Mat image=imread(argv[ 1]);
  8. Mat imageGray;
  9. cvtColor(image,imageGray,CV_RGB2GRAY);
  10. GaussianBlur(imageGray,imageGray,Size( 3, 3), 1, 2);
  11. imshow( "Gaussian Image",imageGray);
  12. vector<Vec3f>circles;
  13. HoughCircles(imageGray,circles,CV_HOUGH_GRADIENT, 1, 1, 200, 160, 0);
  14. for( int i= 0;i<circles.size();i++)
  15. {
  16. Point circleCenter(circles[i][0],circles[i][1]);
  17. int radius=circles[i][ 2];
  18. circle(image,circleCenter,radius,Scalar( 0, 0, 255), 3); //做圆
  19. circle(image,circleCenter, 3,Scalar( 255, 0, 0), 3); //圆心
  20. }
  21. imshow( "Circle Image",image);
  22. waitKey();
  23. }


在当前参数下只有一个圆被检测出来,检测效果:



HoughCircles方法对参数比较敏感,很小的改动就可能导致差别很大的检测效果,需要针对不同图像的不同检测用途进行调试。


函数原型:


  
  
  1. void HoughCircles( InputArray image, OutputArray circles,
  2. int method, double dp, double minDist,
  3. double param1= 100, double param2= 100,
  4. int minRadius= 0, int maxRadius= 0 );


第一个参数image是输入图像矩阵,要求是灰度图像;

第二个参数 circles是一个包含检测到的圆的信息的向量,向量内第一个元素是圆的横坐标,第二个是纵坐标,第三个是半径大小;

第三个参数 methodmethod是所使用的圆检测算法,目前只有CV_HOUGH_GRADIENT一个可选;

第四个参数 dp是累加面与原始图像相比的分辨率的反比参数,dp=2时累计面分辨率是元素图像的一半,宽高都缩减为原来的一半,dp=1时,两者相同。(关于这个分辨率的概念没有理解透,按道理低分辨率应该意味着更快的检测速度,然而实测恰恰相反)

第五个参数 minDist定义了两个圆心之间的最小距离;

第六个参数param1是Canny边缘检测的高阈值,低阈值被自动置为高阈值的一半;

第七个参数param2是累加平面对是否是圆的判定阈值;

第八和第九个参数定义了检测到的圆的半径的最大值和最小值;


Here I’ll tell you how to detect circles (which are quite important in computer vision application) using a technique similar to the standard Hough transform. This article assumes you know how the Hough transform works, or you’ve understood the previous articles in this series (The Hough Transform).
The parameterization
A circle can be described completely with three pieces of information: the center (a, b) and the radius. (The center consists of two parts, hence a total of three)
x = a + Rcosθ
y = b + Rsinθ
When the θ varies from 0 to 360, a complete circle of radius R is generated.
So with the Circle Hough Transform, we expect to find triplets of (x, y, R) that are highly probably circles in the image. That is, we want to find three parameters. Thus, the parameter space is 3D… meaning things can get ugly if you don’t tread slowly. Out of memory errors are common even if your programming language uses virtual memory.
So we’ll start simple.
Assuming R is known
To begin, we’ll start with the assumption that you’re looking for circles of a particular radius, that is, R is known. The equation of each circle is:
x = a + Rcosθ
y = b + Rsinθ
So, every point in the xy space will be equivalent to a circle in the ab space (R isn’t a parameter, we already know it). This is because on rearranging the equations, we get:
a = x1 – Rcosθ
b = y1 – Rsinθ
for a particular point (x1, y1). And θ sweeps from 0 to 360 degrees.
So, the flow of events is something like this:
Load an image
Detect edges and generate a binary image
For every ‘edge’ pixel, generate a circle in the ab space
For every point on the circle in the ab space, cast ‘votes’ in the accumulator cells
The cells with greater number of votes are the centers
When R is not known
When the radius is not known, the simplest solution is to just guess. Assume R = 1, and then run the same algorithm. Then assume R = 2, and run it again. Assume R = 3…. and so on.
Whats the upper limit of R? A safe limit would be the length of the diagonal of the image. No possible circle on the image can have a radius greater than or equal to the diagonal.

图形可以用一些参数进行表示,标准霍夫变换的原理就是把图像空间转换成参数空间(即霍夫空间),例如霍夫变换的直线检测就是在距离-角度空间内进行检测。圆可以表示成:

(x-a)2+(y-b)2=r2                   (1)

其中a和b表示圆心坐标,r表示圆半径,因此霍夫变换的圆检测就是在这三个参数组成的三维空间内进行检测。

原则上,霍夫变换可以检测任何形状。但复杂的形状需要的参数就多,霍夫空间的维数就多,因此在程序实现上所需的内存空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂图形的检测中。所以一些改进的霍夫变换就相继提出,它们的基本原理就是尽可能减小霍夫空间的维数。

HoughCircles函数实现了圆形检测,它使用的算法也是改进的霍夫变换——2-1霍夫变换(21HT)。也就是把霍夫变换分为两个阶段,从而减小了霍夫空间的维数。第一阶段用于检测圆心,第二阶段从圆心推导出圆半径。检测圆心的原理是圆心是它所在圆周所有法线的交汇处,因此只要找到这个交点,即可确定圆心,该方法所用的霍夫空间与图像空间的性质相同,因此它仅仅是二维空间。检测圆半径的方法是从圆心到圆周上的任意一点的距离(即半径)是相同,只要确定一个阈值,只要相同距离的数量大于该阈值,我们就认为该距离就是该圆心所对应的圆半径,该方法只需要计算半径直方图,不使用霍夫空间。圆心和圆半径都得到了,那么通过公式1一个圆形就得到了。从上面的分析可以看出,2-1霍夫变换把标准霍夫变换的三维霍夫空间缩小为二维霍夫空间,因此无论在内存的使用上还是在运行效率上,2-1霍夫变换都远远优于标准霍夫变换。但该算法有一个不足之处就是由于圆半径的检测完全取决于圆心的检测,因此如果圆心检测出现偏差,那么圆半径的检测肯定也是错误的。2-1霍夫变换的具体步骤为:

第一阶段:检测圆心

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步骤,得到所有的圆半径。

HoughCircles函数的原型为:

void HoughCircles(InputArray image,OutputArray circles, int method, double dp, double minDist, double param1=100, double param2=100, int minRadius=0,int maxRadius=0 )

image为输入图像,要求是灰度图像

circles为输出圆向量,每个向量包括三个浮点型的元素——圆心横坐标,圆心纵坐标和圆半径

method为使用霍夫变换圆检测的算法,Opencv2.4.9只实现了2-1霍夫变换,它的参数是CV_HOUGH_GRADIENT

dp为第一阶段所使用的霍夫空间的分辨率,dp=1时表示霍夫空间与输入图像空间的大小一致,dp=2时霍夫空间是输入图像空间的一半,以此类推

minDist为圆心之间的最小距离,如果检测到的两个圆心之间距离小于该值,则认为它们是同一个圆心

param1为边缘检测时使用Canny算子的高阈值

param2为步骤1.5和步骤2.5中所共有的阈值

minRadius和maxRadius为所检测到的圆半径的最小值和最大值

HoughCircles函数在sources/modules/imgproc/src/hough.cpp文件内被定义:


   
   
  1. void cv::HoughCircles( InputArray _image, OutputArray _circles,
  2. int method, double dp, double min_dist,
  3. double param1, double param2,
  4. int minRadius, int maxRadius )
  5. {
  6. //定义一段内存
  7. Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);
  8. Mat image = _image.getMat(); //提取输入图像矩阵
  9. CvMat c_image = image; //矩阵转换
  10. //调用cvHoughCircles函数
  11. CvSeq* seq = cvHoughCircles( &c_image, storage, method,
  12. dp, min_dist, param1, param2, minRadius, maxRadius );
  13. //把序列转换为矩阵
  14. seqToMat(seq, _circles);
  15. }
cvHoughCircles函数为:


   
   
  1. CV_IMPL CvSeq*
  2. cvHoughCircles ( CvArr* src_image, void* circle_storage,
  3. int method, double dp, double min_dist,
  4. double param1, double param2,
  5. int min_radius, int max_radius )
  6. {
  7. CvSeq* result = 0;
  8. CvMat stub, *img = (CvMat*)src_image;
  9. CvMat* mat = 0;
  10. CvSeq* circles = 0;
  11. CvSeq circles_header;
  12. CvSeqBlock circles_block;
  13. int circles_max = INT_MAX; //输出最多圆形的数量,设为无穷多
  14. //canny边缘检测中双阈值中的高阈值
  15. int canny_threshold = cvRound(param1);
  16. //累加器阈值
  17. int acc_threshold = cvRound(param2);
  18. img = cvGetMat( img, &stub );
  19. //确保输入图像是灰度图像
  20. if( !CV_IS_MASK_ARR(img))
  21. CV_Error( CV_StsBadArg, "The source image must be 8-bit, single-channel" );
  22. //内存空间是否存在
  23. if( !circle_storage )
  24. CV_Error( CV_StsNullPtr, "NULL destination" );
  25. //确保参数的正确性
  26. if( dp <= 0 || min_dist <= 0 || canny_threshold <= 0 || acc_threshold <= 0 )
  27. CV_Error( CV_StsOutOfRange, "dp, min_dist, canny_threshold and acc_threshold must be all positive numbers" );
  28. //圆的最小半径要大于0
  29. min_radius = MAX( min_radius, 0 );
  30. //圆的最大半径如果小于0,则设最大半径为图像宽和长度的最大值,
  31. //如果最大半径小于最小半径,则设最大半径为最小半径加两个像素的宽度
  32. if( max_radius <= 0 )
  33. max_radius = MAX( img->rows, img->cols );
  34. else if( max_radius <= min_radius )
  35. max_radius = min_radius + 2;
  36. if( CV_IS_STORAGE( circle_storage ))
  37. {
  38. circles = cvCreateSeq( CV_32FC3, sizeof(CvSeq),
  39. sizeof( float)* 3, (CvMemStorage*)circle_storage );
  40. }
  41. else if( CV_IS_MAT( circle_storage ))
  42. {
  43. mat = (CvMat*)circle_storage;
  44. if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 && mat->cols != 1) ||
  45. CV_MAT_TYPE(mat->type) != CV_32FC3 )
  46. CV_Error( CV_StsBadArg,
  47. "The destination matrix should be continuous and have a single row or a single column" );
  48. circles = cvMakeSeqHeaderForArray( CV_32FC3, sizeof(CvSeq), sizeof( float)* 3,
  49. mat->data.ptr, mat->rows + mat->cols - 1, &circles_header, &circles_block );
  50. circles_max = circles->total;
  51. cvClearSeq( circles );
  52. }
  53. else
  54. CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* nor CvMat*" );
  55. //选择哪种算法检测圆,目前只有2-1霍夫变换
  56. switch( method )
  57. {
  58. case CV_HOUGH_GRADIENT:
  59. //调用icvHoughCirclesGradient函数
  60. icvHoughCirclesGradient( img, ( float)dp, ( float)min_dist,
  61. min_radius, max_radius, canny_threshold,
  62. acc_threshold, circles, circles_max );
  63. break;
  64. default:
  65. CV_Error( CV_StsBadArg, "Unrecognized method id" );
  66. }
  67. if( mat )
  68. {
  69. if( mat->cols > mat->rows )
  70. mat->cols = circles->total;
  71. else
  72. mat->rows = circles->total;
  73. }
  74. else
  75. result = circles;
  76. //输出圆
  77. return result;
  78. }
icvHoughCirclesGradient函数为:

   
   
  1. static void
  2. icvHoughCirclesGradient ( CvMat* img, float dp, float min_dist,
  3. int min_radius, int max_radius,
  4. int canny_threshold, int acc_threshold,
  5. CvSeq* circles, int circles_max )
  6. {
  7. //为了提高运算精度,定义一个数值的位移量
  8. const int SHIFT = 10, ONE = 1 << SHIFT;
  9. //定义水平梯度和垂直梯度矩阵的地址指针
  10. cv::Ptr<CvMat> dx, dy;
  11. //定义边缘图像、累加器矩阵和半径距离矩阵的地址指针
  12. cv::Ptr<CvMat> edges, accum, dist_buf;
  13. //定义排序向量
  14. std:: vector< int> sort_buf;
  15. cv::Ptr<CvMemStorage> storage;
  16. int x, y, i, j, k, center_count, nz_count;
  17. //事先计算好最小半径和最大半径的平方
  18. float min_radius2 = ( float)min_radius*min_radius;
  19. float max_radius2 = ( float)max_radius*max_radius;
  20. int rows, cols, arows, acols;
  21. int astep, *adata;
  22. float* ddata;
  23. //nz表示圆周序列,centers表示圆心序列
  24. CvSeq *nz, *centers;
  25. float idp, dr;
  26. CvSeqReader reader;
  27. //创建一个边缘图像矩阵
  28. edges = cvCreateMat( img->rows, img->cols, CV_8UC1 );
  29. //第一阶段
  30. //步骤1.1,用canny边缘检测算法得到输入图像的边缘图像
  31. cvCanny( img, edges, MAX(canny_threshold/ 2, 1), canny_threshold, 3 );
  32. //创建输入图像的水平梯度图像和垂直梯度图像
  33. dx = cvCreateMat( img->rows, img->cols, CV_16SC1 );
  34. dy = cvCreateMat( img->rows, img->cols, CV_16SC1 );
  35. //步骤1.2,用Sobel算子法计算水平梯度和垂直梯度
  36. cvSobel( img, dx, 1, 0, 3 );
  37. cvSobel( img, dy, 0, 1, 3 );
  38. /确保累加器矩阵的分辨率不小于 1
  39. if( dp < 1.f )
  40. dp = 1.f;
  41. //分辨率的倒数
  42. idp = 1.f/dp;
  43. //根据分辨率,创建累加器矩阵
  44. accum = cvCreateMat( cvCeil(img->rows*idp)+ 2, cvCeil(img->cols*idp)+ 2, CV_32SC1 );
  45. //初始化累加器为0
  46. cvZero(accum);
  47. //创建两个序列,
  48. storage = cvCreateMemStorage();
  49. nz = cvCreateSeq( CV_32SC2, sizeof(CvSeq), sizeof(CvPoint), storage );
  50. centers = cvCreateSeq( CV_32SC1, sizeof(CvSeq), sizeof( int), storage );
  51. rows = img->rows; //图像的高
  52. cols = img->cols; //图像的宽
  53. arows = accum->rows - 2; //累加器的高
  54. acols = accum->cols - 2; //累加器的宽
  55. adata = accum->data.i; //累加器的地址指针
  56. astep = accum->step/ sizeof(adata[ 0]); /累加器的步长
  57. // Accumulate circle evidence for each edge pixel
  58. //步骤1.3,对边缘图像计算累加和
  59. for( y = 0; y < rows; y++ )
  60. {
  61. //提取出边缘图像、水平梯度图像和垂直梯度图像的每行的首地址
  62. const uchar* edges_row = edges->data.ptr + y*edges->step;
  63. const short* dx_row = ( const short*)(dx->data.ptr + y*dx->step);
  64. const short* dy_row = ( const short*)(dy->data.ptr + y*dy->step);
  65. for( x = 0; x < cols; x++ )
  66. {
  67. float vx, vy;
  68. int sx, sy, x0, y0, x1, y1, r;
  69. CvPoint pt;
  70. //当前的水平梯度值和垂直梯度值
  71. vx = dx_row[x];
  72. vy = dy_row[x];
  73. //如果当前的像素不是边缘点,或者水平梯度值和垂直梯度值都为0,则继续循环。因为如果满足上面条件,该点一定不是圆周上的点
  74. if( !edges_row[x] || (vx == 0 && vy == 0) )
  75. continue;
  76. //计算当前点的梯度值
  77. float mag = sqrt(vx*vx+vy*vy);
  78. assert( mag >= 1 );
  79. //定义水平和垂直的位移量
  80. sx = cvRound((vx*idp)*ONE/mag);
  81. sy = cvRound((vy*idp)*ONE/mag);
  82. //把当前点的坐标定位到累加器的位置上
  83. x0 = cvRound((x*idp)*ONE);
  84. y0 = cvRound((y*idp)*ONE);
  85. // Step from min_radius to max_radius in both directions of the gradient
  86. //在梯度的两个方向上进行位移,并对累加器进行投票累计
  87. for( int k1 = 0; k1 < 2; k1++ )
  88. {
  89. //初始一个位移的启动
  90. //位移量乘以最小半径,从而保证了所检测的圆的半径一定是大于最小半径
  91. x1 = x0 + min_radius * sx;
  92. y1 = y0 + min_radius * sy;
  93. //在梯度的方向上位移
  94. // r <= max_radius保证了所检测的圆的半径一定是小于最大半径
  95. for( r = min_radius; r <= max_radius; x1 += sx, y1 += sy, r++ )
  96. {
  97. int x2 = x1 >> SHIFT, y2 = y1 >> SHIFT;
  98. //如果位移后的点超过了累加器矩阵的范围,则退出
  99. if( ( unsigned)x2 >= ( unsigned)acols ||
  100. ( unsigned)y2 >= ( unsigned)arows )
  101. break;
  102. //在累加器的相应位置上加1
  103. adata[y2*astep + x2]++;
  104. }
  105. //把位移量设置为反方向
  106. sx = -sx; sy = -sy;
  107. }
  108. //把输入图像中的当前点(即圆周上的点)的坐标压入序列圆周序列nz中
  109. pt.x = x; pt.y = y;
  110. cvSeqPush( nz, &pt );
  111. }
  112. }
  113. //计算圆周点的总数
  114. nz_count = nz->total;
  115. //如果总数为0,说明没有检测到圆,则退出该函数
  116. if( !nz_count )
  117. return;
  118. //Find possible circle centers
  119. //步骤1.4和1.5,遍历整个累加器矩阵,找到可能的圆心
  120. for( y = 1; y < arows - 1; y++ )
  121. {
  122. for( x = 1; x < acols - 1; x++ )
  123. {
  124. int base = y*(acols+ 2) + x;
  125. //如果当前的值大于阈值,并在4邻域内它是最大值,则该点被认为是圆心
  126. if( adata[base] > acc_threshold &&
  127. adata[base] > adata[base -1] && adata[base] > adata[base+ 1] &&
  128. adata[base] > adata[base-acols -2] && adata[base] > adata[base+acols+ 2] )
  129. //把当前点的地址压入圆心序列centers中
  130. cvSeqPush(centers, &base);
  131. }
  132. }
  133. //计算圆心的总数
  134. center_count = centers->total;
  135. //如果总数为0,说明没有检测到圆,则退出该函数
  136. if( !center_count )
  137. return;
  138. //定义排序向量的大小
  139. sort_buf.resize( MAX(center_count,nz_count) );
  140. //把圆心序列放入排序向量中
  141. cvCvtSeqToArray( centers, &sort_buf[ 0] );
  142. //对圆心按照由大到小的顺序进行排序
  143. //它的原理是经过icvHoughSortDescent32s函数后,以sort_buf中元素作为adata数组下标,adata中的元素降序排列,即adata[sort_buf[0]]是adata所有元素中最大的,adata[sort_buf[center_count-1]]是所有元素中最小的
  144. icvHoughSortDescent32s( &sort_buf[ 0], center_count, adata );
  145. //清空圆心序列
  146. cvClearSeq( centers );
  147. //把排好序的圆心重新放入圆心序列中
  148. cvSeqPushMulti( centers, &sort_buf[ 0], center_count );
  149. //创建半径距离矩阵
  150. dist_buf = cvCreateMat( 1, nz_count, CV_32FC1 );
  151. //定义地址指针
  152. ddata = dist_buf->data.fl;
  153. dr = dp; //定义圆半径的距离分辨率
  154. //重新定义圆心之间的最小距离
  155. min_dist = MAX( min_dist, dp );
  156. //最小距离的平方
  157. min_dist *= min_dist;
  158. // For each found possible center
  159. // Estimate radius and check support
  160. //按照由大到小的顺序遍历整个圆心序列
  161. for( i = 0; i < centers->total; i++ )
  162. {
  163. //提取出圆心,得到该点在累加器矩阵中的偏移量
  164. int ofs = *( int*)cvGetSeqElem( centers, i );
  165. //得到圆心在累加器中的坐标位置
  166. y = ofs/(acols+ 2);
  167. x = ofs - (y)*(acols+ 2);
  168. //Calculate circle's center in pixels
  169. //计算圆心在输入图像中的坐标位置
  170. float cx = ( float)((x + 0.5f)*dp), cy = ( float)(( y + 0.5f )*dp);
  171. float start_dist, dist_sum;
  172. float r_best = 0;
  173. int max_count = 0;
  174. // Check distance with previously detected circles
  175. //判断当前的圆心与之前确定作为输出的圆心是否为同一个圆心
  176. for( j = 0; j < circles->total; j++ )
  177. {
  178. //从序列中提取出圆心
  179. float* c = ( float*)cvGetSeqElem( circles, j );
  180. //计算当前圆心与提取出的圆心之间的距离,如果两者距离小于所设的阈值,则认为两个圆心是同一个圆心,退出循环
  181. if( (c[ 0] - cx)*(c[ 0] - cx) + (c[ 1] - cy)*(c[ 1] - cy) < min_dist )
  182. break;
  183. }
  184. //如果j < circles->total,说明当前的圆心已被认为与之前确定作为输出的圆心是同一个圆心,则抛弃该圆心,返回上面的for循环
  185. if( j < circles->total )
  186. continue;
  187. // Estimate best radius
  188. //第二阶段
  189. //开始读取圆周序列nz
  190. cvStartReadSeq( nz, &reader );
  191. for( j = k = 0; j < nz_count; j++ )
  192. {
  193. CvPoint pt;
  194. float _dx, _dy, _r2;
  195. CV_READ_SEQ_ELEM( pt, reader );
  196. _dx = cx - pt.x; _dy = cy - pt.y;
  197. //步骤2.1,计算圆周上的点与当前圆心的距离,即半径
  198. _r2 = _dx*_dx + _dy*_dy;
  199. //步骤2.2,如果半径在所设置的最大半径和最小半径之间
  200. if(min_radius2 <= _r2 && _r2 <= max_radius2 )
  201. {
  202. //把半径存入dist_buf内
  203. ddata[k] = _r2;
  204. sort_buf[k] = k;
  205. k++;
  206. }
  207. }
  208. //k表示一共有多少个圆周上的点
  209. int nz_count1 = k, start_idx = nz_count1 - 1;
  210. //nz_count1等于0也就是k等于0,说明当前的圆心没有所对应的圆,意味着当前圆心不是真正的圆心,所以抛弃该圆心,返回上面的for循环
  211. if( nz_count1 == 0 )
  212. continue;
  213. dist_buf->cols = nz_count1; //得到圆周上点的个数
  214. cvPow( dist_buf, dist_buf, 0.5 ); //求平方根,得到真正的圆半径
  215. //步骤2.3,对圆半径进行排序
  216. icvHoughSortDescent32s( &sort_buf[ 0], nz_count1, ( int*)ddata );
  217. dist_sum = start_dist = ddata[sort_buf[nz_count1 -1]];
  218. //步骤2.4
  219. for( j = nz_count1 - 2; j >= 0; j-- )
  220. {
  221. float d = ddata[sort_buf[j]];
  222. if( d > max_radius )
  223. break;
  224. //d表示当前半径值,start_dist表示上一次通过下面if语句更新后的半径值,dr表示半径距离分辨率,如果这两个半径距离之差大于距离分辨率,说明这两个半径一定不属于同一个圆,而两次满足if语句条件之间的那些半径值可以认为是相等的,即是属于同一个圆
  225. if( d - start_dist > dr )
  226. {
  227. //start_idx表示上一次进入if语句时更新的半径距离排序的序号
  228. // start_idx – j表示当前得到的相同半径距离的数量
  229. //(j + start_idx)/2表示j和start_idx中间的数
  230. //取中间的数所对应的半径值作为当前半径值r_cur,也就是取那些半径值相同的值
  231. float r_cur = ddata[sort_buf[(j + start_idx)/ 2]];
  232. //如果当前得到的半径相同的数量大于最大值max_count,则进入if语句
  233. if( (start_idx - j)*r_best >= max_count*r_cur ||
  234. (r_best < FLT_EPSILON && start_idx - j >= max_count) )
  235. {
  236. r_best = r_cur; //把当前半径值作为最佳半径值
  237. max_count = start_idx - j; //更新最大值
  238. }
  239. //更新半径距离和序号
  240. start_dist = d;
  241. start_idx = j;
  242. dist_sum = 0;
  243. }
  244. dist_sum += d;
  245. }
  246. // Check if the circle has enough support
  247. //步骤2.5,最终确定输出
  248. //如果相同半径的数量大于所设阈值
  249. if( max_count > acc_threshold )
  250. {
  251. float c[ 3];
  252. c[ 0] = cx; //圆心的横坐标
  253. c[ 1] = cy; //圆心的纵坐标
  254. c[ 2] = ( float)r_best; //所对应的圆的半径
  255. cvSeqPush( circles, c ); //压入序列circles内
  256. //如果得到的圆大于阈值,则退出该函数
  257. if( circles->total > circles_max )
  258. return;
  259. }
  260. }
  261. }
下面是用HoughCircles函数进行霍夫变换圆检测的实例。由于HoughCircles函数内是调用Canny函数进行边缘检测,opencv的Canny函数是不包括平滑滤波这一步的,因此为了增强抗干扰能力,在使用HoughCircles函数之前,我们先对原图进行滤波处理,我们使用的是高斯模糊方法。


   
   
  1. #include "opencv2/core/core.hpp"
  2. #include "opencv2/highgui/highgui.hpp"
  3. #include "opencv2/imgproc/imgproc.hpp"
  4. #include <iostream>
  5. using namespace cv;
  6. using namespace std;
  7. int main( int argc, char** argv )
  8. {
  9. Mat src, gray;
  10. src=imread( "coins.jpg");
  11. if( !src.data )
  12. return -1;
  13. cvtColor( src, gray, CV_BGR2GRAY );
  14. //高斯模糊平滑
  15. GaussianBlur( gray, gray, Size( 9, 9), 2, 2 );
  16. vector<Vec3f> circles;
  17. //霍夫变换
  18. HoughCircles( gray, circles, CV_HOUGH_GRADIENT, 1, gray.rows/ 20, 100, 60, 0, 0 );
  19. //在原图中画出圆心和圆
  20. for( size_t i = 0; i < circles.size(); i++ )
  21. {
  22. //提取出圆心坐标
  23. Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
  24. //提取出圆半径
  25. int radius = cvRound(circles[i][ 2]);
  26. //圆心
  27. circle( src, center, 3, Scalar( 0, 255, 0), -1, 8, 0 );
  28. //圆
  29. circle( src, center, radius, Scalar( 0, 0, 255), 3, 8, 0 );
  30. }
  31. namedWindow( "Circle", CV_WINDOW_AUTOSIZE );
  32. imshow( "Circle", src );
  33. waitKey( 0);
  34. return 0;
  35. }
下图为圆检测的结果。

从实际运行的结果来看,我们发现HoughCircles函数不足之处是所需要的参数较多,而且每个参数的改变对结果影响都很大,即漏检和错检的几率很大。

执剑天涯:https://blog.csdn.net/qq_35759050/article/details/71158200
牧野: https://blog.csdn.net/dcrmg/article/details/52506538
zcj:https://blog.csdn.net/zhaocj/article/details/50454847



猜你喜欢

转载自blog.csdn.net/du_shuang/article/details/81193315