作者:毛星云(浅墨)    微博:​http://weibo.com/u/1723155442​

知乎:​http://www.zhihu.com/people/mao-xing-yun​

​​

写作当前博文时配套使用的OpenCV版本: 2.4.9




 本篇文章中,我们一起探讨了OpenCV中霍夫变换相关的知识点,以及了解了OpenCV中实现霍夫线变换的HoughLines、HoughLinesP函数的使用方法,实现霍夫圆变换的HoughCircles函数的使用方法。此博文一共有四个配套的简短的示例程序,其详细注释过的代码都在文中贴出,且文章最后提供了综合示例程序的下载。


先尝鲜一下其中一个示例程序的运行截图:


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换




一、引言




在图像处理和计算机视觉领域中,如何从当前的图像中提取所需要的特征信息是图像识别的关键所在。在许多应用场合中需要快速准确地检测出直线或者圆。其中一种非常有效的解决问题的方法是霍夫(Hough)变换,其为图像处理中从图像中识别几何形状的基本方法之一,应用很广泛,也有很多改进算法。最基本的霍夫变换是从黑白图像中检测直线(线段)。这篇文章就将介绍OpenCV中霍夫变换的使用方法和相关知识。





二、霍夫变换概述



霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。

霍夫变换于1962年由PaulHough首次提出,最初的Hough变换是设计用来检测直线和曲线,起初的方法要求知道物体边界线的解析方程,但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的Robustness,即对数据的不完全或噪声不是非常敏感。然而,要获得描述边界的解析表达常常是不可能的。 后于1972年由Richard Duda & Peter Hart推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题。


霍夫变换在OpenCV中分为霍夫线变换和霍夫圆变换两种,我们下面将分别进行介绍。






三、霍夫线变换





3.1  OpenCV中的霍夫线变换




我们知道,霍夫线变换是一种用来寻找直线的方法. 在使用霍夫线变换之前, 首先要对图像进行边缘检测的处理,也即霍夫线变换的直接输入只能是边缘二值图像.

OpenCV支持三种不同的霍夫线变换,它们分别是:标准霍夫变换(Standard Hough Transform,SHT)和多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)累计概率霍夫变换(Progressive Probabilistic Hough Transform ,PPHT)。


其中,多尺度霍夫变换(MSHT)为经典霍夫变换(SHT)在多尺度下的一个变种。累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。之所以称PPHT为“概率”的,是因为并不将累加器平面内的所有可能的点累加,而只是累加其中的一部分,该想法是如果峰值如果足够高,只用一小部分时间去寻找它就够了。这样猜想的话,可以实质性地减少计算时间。


在OpenCV中,我们可以用HoughLines函数来调用标准霍夫变换SHT和多尺度霍夫变换MSHT。

而HoughLinesP函数用于调用累计概率霍夫变换PPHT。累计概率霍夫变换执行效率很高,所有相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。


总结一下,OpenCV中的霍夫线变换有如下三种:


<1>标准霍夫变换(StandardHough Transform,SHT),由HoughLines函数调用。

<2>多尺度霍夫变换(Multi-ScaleHough Transform,MSHT),由HoughLines函数调用。

<3>累计概率霍夫变换(ProgressiveProbabilistic Hough Transform,PPHT),由HoughLinesP函数调用。





3.2 霍夫线变换的原理




【1】众所周知, 一条直线在图像二维空间可由两个变量表示. 如:


<1>在笛卡尔坐标系: 可由参数: 斜率和截距(m,b) 表示。

<2>在极坐标系: 可由参数: 极径和极角【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_02表示。


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_03


对于霍夫变换, 我们将采用第二种方式极坐标系来表示直线. 因此, 直线的表达式可为:


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_04


化简便可得到:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_05



【2】一般来说对于点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_06, 我们可以将通过这个点的一族直线统一定义为:


 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_07


这就意味着每一对【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_08代表一条通过点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_06的直线。


【3】如果对于一个给定点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_06我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点X_0= 8 和Y_0= 6 我们可以绘出下图 (在平面):


 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_11






只绘出满足下列条件的点 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_12 和  【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_13 .


【4】我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_14相交, 这就意味着它

们通过同一条直线. 例如,接上面的例子我们继续对点 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_15 和点 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_16 绘图, 得到下图:


 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_17



这三条曲线在平面相交于点 (0.925, 9.6), 坐标表示的是参数对 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_18 或者是说点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_06, 点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_20和点【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_21组成的平面内的的直线。


【5】以上的说明表明,一般来说, 一条直线能够通过在平面 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_18 寻找交于一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成. 一般来说我们可以通过设置直线上点的阈值来定义多少条曲线交于一点我们才认为检测到了一条直线。


【6】这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点. 如果交于一点的曲线的数量超过了阈值, 那么可以认为这个交点所代表的参数对【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_23在原图像中为一条直线。







3.3 HoughLines( )函数详解



此函数可以找出采用标准霍夫变换的二值图像线条。在OpenCV中,我们可以用其来调用标准霍夫变换SHT和多尺度霍夫变换MSHT的OpenCV内建算法。




[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. C++: void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0 )  









  • 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
  • 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_24表示,其中,【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_25是离坐标原点((0,0)(也就是图像的左上角)的距离。 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_26是弧度线条旋转角度(0~垂直线,π/2~水平线)。
  • 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。PS:Latex中/rho就表示 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_25
  • 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
  • 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
  • 第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
  • 第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。



另外,关于霍夫变换的详细解释,可以看此英文页面:

​http://homepages.inf.ed.ac.uk/rbf/HIPR2/hough.htm​​​


在学完函数解析,看看浅墨为大家准备的以HoughLines为核心的示例程序,就可以全方位了解HoughLines函数的使用方法了:





[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. //-----------------------------------【头文件包含部分】---------------------------------------  
  2. //      描述:包含程序所依赖的头文件  
  3. //----------------------------------------------------------------------------------------------   
  4. #include <opencv2/opencv.hpp>  
  5. #include <opencv2/imgproc/imgproc.hpp>  

  6. //-----------------------------------【命名空间声明部分】---------------------------------------  
  7. //      描述:包含程序所使用的命名空间  
  8. //-----------------------------------------------------------------------------------------------   
  9. using namespace cv;  
  10. //-----------------------------------【main( )函数】--------------------------------------------  
  11. //      描述:控制台应用程序的入口函数,我们的程序从这里开始  
  12. //-----------------------------------------------------------------------------------------------  
  13. int main( )  
  14. {  
  15.     //【1】载入原始图和Mat变量定义     
  16.     Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图  
  17.     Mat midImage,dstImage;//临时变量和目标图的定义  

  18.     //【2】进行边缘检测和转化为灰度图  
  19.     Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测  
  20.     cvtColor(midImage,dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图  

  21.     //【3】进行霍夫线变换  
  22.     vector<Vec2f> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合  
  23.     HoughLines(midImage, lines, 1, CV_PI/180, 150, 0, 0 );  

  24.     //【4】依次在图中绘制出每条线段  
  25.     for( size_t i = 0; i < lines.size(); i++ )  
  26.     {  
  27.         float rho = lines[i][0], theta = lines[i][1];  
  28.         Point pt1, pt2;  
  29.         double a = cos(theta), b = sin(theta);  
  30.         double x0 = a*rho, y0 = b*rho;  
  31.         pt1.x = cvRound(x0 + 1000*(-b));  
  32.         pt1.y = cvRound(y0 + 1000*(a));  
  33.         pt2.x = cvRound(x0 - 1000*(-b));  
  34.         pt2.y = cvRound(y0 - 1000*(a));  
  35.         line( dstImage, pt1, pt2, Scalar(55,100,195), 1, CV_AA);  
  36.     }  

  37.     //【5】显示原始图    
  38.     imshow("【原始图】", srcImage);    

  39.     //【6】边缘检测后的图   
  40.     imshow("【边缘检测后的图】", midImage);    

  41.     //【7】显示效果图    
  42.     imshow("【效果图】", dstImage);    

  43.     waitKey(0);    

  44.     return 0;    
  45. }  







运行截图:

 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_28


来一张大图:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_29


PS:可以通过调节line(dstImage, pt1, pt2, Scalar(55,100,195), 1, CV_AA);一句Scalar(55,100,195)参数中G、B、R颜色值的数值,得到图中想要的线条颜色。





3.4 HoughLinesP( )函数详解



此函数在HoughLines的基础上末尾加了一个代表Probabilistic(概率)的P,表明它可以采用累计概率霍夫变换(PPHT)来找出二值图像中的直线。




[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. C++: 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,允许将同一行点与点之间连接起来的最大的距离。



对于此函数,依然是为大家准备了示例程序:





[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. //-----------------------------------【头文件包含部分】---------------------------------------  
  2. //      描述:包含程序所依赖的头文件  
  3. //----------------------------------------------------------------------------------------------   
  4. #include <opencv2/opencv.hpp>  
  5. #include <opencv2/imgproc/imgproc.hpp>  

  6. //-----------------------------------【命名空间声明部分】---------------------------------------  
  7. //      描述:包含程序所使用的命名空间  
  8. //-----------------------------------------------------------------------------------------------   
  9. using namespace cv;  
  10. //-----------------------------------【main( )函数】--------------------------------------------  
  11. //      描述:控制台应用程序的入口函数,我们的程序从这里开始  
  12. //-----------------------------------------------------------------------------------------------  
  13. int main( )  
  14. {  
  15.     //【1】载入原始图和Mat变量定义     
  16.     Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图  
  17.     Mat midImage,dstImage;//临时变量和目标图的定义  

  18.     //【2】进行边缘检测和转化为灰度图  
  19.     Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测  
  20.     cvtColor(midImage,dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图  

  21.     //【3】进行霍夫线变换  
  22.     vector<Vec4i> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合  
  23.     HoughLinesP(midImage, lines, 1, CV_PI/180, 80, 50, 10 );  

  24.     //【4】依次在图中绘制出每条线段  
  25.     for( size_t i = 0; i < lines.size(); i++ )  
  26.     {  
  27.         Vec4i l = lines[i];  
  28.         line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186,88,255), 1, CV_AA);  
  29.     }  

  30.     //【5】显示原始图    
  31.     imshow("【原始图】", srcImage);    

  32.     //【6】边缘检测后的图   
  33.     imshow("【边缘检测后的图】", midImage);    

  34.     //【7】显示效果图    
  35.     imshow("【效果图】", dstImage);    

  36.     waitKey(0);    

  37.     return 0;    
  38. }  







运行截图:

 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_30


来一张大图:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_31





四、霍夫圆变换




霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代。说“大体上类似”的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替:在这三维中,一维是x,一维是y,另外一维是圆的半径r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。


对直线来说, 一条直线能由参数极径极角【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_32表示. 而对圆来说, 我们需要三个参数来表示一个圆, 也就是:


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_33


这里的 表示圆心的位置 (下图中的绿点) 而 r 表示半径, 这样我们就能唯一的定义一个圆了, 见下图:

 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_34 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_35



在OpenCV中,我们一般通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。




4.1 霍夫梯度法的原理



霍夫梯度法的原理是这样的。


【1】首先对图像应用边缘检测,比如用canny边缘检测。

【2】然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度。

【3】利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最小值到指定的最大值的距离。

【4】同时,标记边缘图像中每一个非0像素的位置。

【5】然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照累加值降序排列,以便于最支持像素的中心首先出现。

【6】接下来对每一个中心,考虑所有的非0像素。

【7】这些像素按照其与中心的距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径。8.如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择的中心有足够的距离,那么它就会被保留下来。


这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。

人无完人,金无足赤。同样,这个算法也并不是十全十美的,还有许多需要指出的缺点。




4.2 霍夫梯度法的缺点



<1>在霍夫梯度法中,我们使用Sobel导数来计算局部梯度,那么随之而来的假设是,其可以视作等同于一条局部切线,并这个不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。

<2>在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。第三,因为每一个中心只选择一个圆,如果有同心圆,就只能选择其中的一个。

<3>因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,若是对于无穷分辨率的平滑图像而言的话,这才是必须的。





4.3 HoughCircles( )函数详解



HoughCircles函数可以利用霍夫变换算法检测出灰度图中的圆。它和之前的HoughLines和HoughLinesP比较明显的一个区别是它不需要源图是二值的,而HoughLines和HoughLinesP都需要源图为二值图像。




[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







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






  • 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。
  • 第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。
  • 第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。
  • 第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
  • 第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
  • 第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
  • 第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
  • 第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。
  • 第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。



需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。我们可以通过第八个参数minRadius和第九个参数maxRadius指定最小和最大的圆半径,来辅助圆检测的效果。或者,我们可以直接忽略返回半径,因为它们都有着默认值0,单单用HoughCircles函数检测出来的圆心,然后用额外的一些步骤来进一步确定半径。


 依然是为大家准备了基于此函数的示例程序:




[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. //-----------------------------------【头文件包含部分】---------------------------------------  
  2. //      描述:包含程序所依赖的头文件  
  3. //----------------------------------------------------------------------------------------------   
  4. #include <opencv2/opencv.hpp>  
  5. #include <opencv2/imgproc/imgproc.hpp>  

  6. //-----------------------------------【命名空间声明部分】---------------------------------------  
  7. //      描述:包含程序所使用的命名空间  
  8. //-----------------------------------------------------------------------------------------------   
  9. using namespace cv;  
  10. //-----------------------------------【main( )函数】--------------------------------------------  
  11. //      描述:控制台应用程序的入口函数,我们的程序从这里开始  
  12. //-----------------------------------------------------------------------------------------------  
  13. int main( )  
  14. {  
  15.     //【1】载入原始图和Mat变量定义     
  16.     Mat srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图  
  17.     Mat midImage,dstImage;//临时变量和目标图的定义  

  18.     //【2】显示原始图  
  19.     imshow("【原始图】", srcImage);    

  20.     //【3】转为灰度图,进行图像平滑  
  21.     cvtColor(srcImage,midImage, CV_BGR2GRAY);//转化边缘检测后的图为灰度图  
  22.     GaussianBlur( midImage, midImage, Size(9, 9), 2, 2 );  

  23.     //【4】进行霍夫圆变换  
  24.     vector<Vec3f> circles;  
  25.     HoughCircles( midImage, circles, CV_HOUGH_GRADIENT,1.5, 10, 200, 100, 0, 0 );  

  26.     //【5】依次在图中绘制出圆  
  27.     for( size_t i = 0; i < circles.size(); i++ )  
  28.     {  
  29.         Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));  
  30.         int radius = cvRound(circles[i][2]);  
  31.         //绘制圆心  
  32.         circle( srcImage, center, 3, Scalar(0,255,0), -1, 8, 0 );  
  33.         //绘制圆轮廓  
  34.         circle( srcImage, center, radius, Scalar(155,50,255), 3, 8, 0 );  
  35.     }  

  36.     //【6】显示效果图    
  37.     imshow("【效果图】", srcImage);    

  38.     waitKey(0);    

  39.     return 0;    
  40. }  





运行截图:


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_36


【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_37





五、源码部分



这个部分就是贴出OpenCV中本文相关函数的源码实现细节,来给想了解实现细节的小伙伴们参考的,浅墨就暂时不在源码的细节上挖深作详细注释了。




5.1 OpenCV2.X中HoughLines( )函数源码







[cpp] ​​view plain​​​​copy​​​​print​​​​?​







  1. void cv::HoughLines( InputArray _image,OutputArray _lines,  
  2.                      double rho, double theta,int threshold,  
  3.                      double srn, double stn )  
  4. {  
  5.    Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);  
  6.    Mat image = _image.getMat();  
  7.    CvMat c_image = image;  
  8.    CvSeq* seq = cvHoughLines2( &c_image, storage, srn == 0 &&stn == 0 ?  
  9.                     CV_HOUGH_STANDARD :CV_HOUGH_MULTI_SCALE,  
  10.                     rho, theta, threshold, srn,stn );  
  11.    seqToMat(seq, _lines);  
  12. }  




可以发现其内部实现是基于OpenCV 1.X旧版的cvHoughLines2函数,我们再来看看其旧版cvHoughLines2的函数源码。




5.1.1 OpenCV2.X中cvHoughLines2()函数源码







[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. CV_IMPL CvSeq*  
  2. cvHoughLines2( CvArr* src_image, void*lineStorage, int method,  
  3.                double rho, double theta, intthreshold,  
  4.                double param1, double param2 )  
  5. {  
  6.    CvSeq* result = 0;  

  7.    CvMat stub, *img = (CvMat*)src_image;  
  8.    CvMat* mat = 0;  
  9.    CvSeq* lines = 0;  
  10.    CvSeq lines_header;  
  11.    CvSeqBlock lines_block;  
  12.    int lineType, elemSize;  
  13.    int linesMax = INT_MAX;  
  14.    int iparam1, iparam2;  

  15.    img = cvGetMat( img, &stub );  

  16.    if( !CV_IS_MASK_ARR(img))  
  17.        CV_Error( CV_StsBadArg, "The source image must be 8-bit,single-channel" );  

  18.    if( !lineStorage )  
  19.        CV_Error( CV_StsNullPtr, "NULL destination" );  

  20.    if( rho <= 0 || theta <= 0 || threshold <= 0 )  
  21.        CV_Error( CV_StsOutOfRange, "rho, theta and threshold must bepositive" );  

  22.    if( method != CV_HOUGH_PROBABILISTIC )  
  23.     {  
  24.        lineType = CV_32FC2;  
  25.        elemSize = sizeof(float)*2;  
  26.     }  
  27.    else  
  28.     {  
  29.        lineType = CV_32SC4;  
  30.        elemSize = sizeof(int)*4;  
  31.     }  

  32.    if( CV_IS_STORAGE( lineStorage ))  
  33.     {  
  34.        lines = cvCreateSeq( lineType, sizeof(CvSeq), elemSize,(CvMemStorage*)lineStorage );  
  35.     }  
  36.    else if( CV_IS_MAT( lineStorage ))  
  37.     {  
  38.        mat = (CvMat*)lineStorage;  

  39.        if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 &&mat->cols != 1) )  
  40.            CV_Error( CV_StsBadArg,  
  41.            "The destination matrix should be continuous and have a single rowor a single column" );  

  42.        if( CV_MAT_TYPE( mat->type ) != lineType )  
  43.            CV_Error( CV_StsBadArg,  
  44.            "The destination matrix data type is inappropriate, see themanual" );  

  45.        lines = cvMakeSeqHeaderForArray( lineType, sizeof(CvSeq), elemSize,mat->data.ptr,  
  46.                                         mat->rows + mat->cols - 1, &lines_header, &lines_block );  
  47.        linesMax = lines->total;  
  48.        cvClearSeq( lines );  
  49.     }  
  50.    else  
  51.        CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* norCvMat*" );  

  52.    iparam1 = cvRound(param1);  
  53.    iparam2 = cvRound(param2);  

  54.    switch( method )  
  55.     {  
  56.    case CV_HOUGH_STANDARD:  
  57.          icvHoughLinesStandard( img, (float)rho,  
  58.                (float)theta, threshold,lines, linesMax );  
  59.          break;  
  60.    case CV_HOUGH_MULTI_SCALE:  
  61.          icvHoughLinesSDiv( img, (float)rho, (float)theta,  
  62.                 threshold, iparam1, iparam2,lines, linesMax );  
  63.          break;  
  64.    case CV_HOUGH_PROBABILISTIC:  
  65.          icvHoughLinesProbabilistic( img, (float)rho, (float)theta,  
  66.                 threshold, iparam1, iparam2,lines, linesMax );  
  67.          break;  
  68.    default:  
  69.        CV_Error( CV_StsBadArg, "Unrecognized method id" );  
  70.     }  

  71.    if( mat )  
  72.     {  
  73.        if( mat->cols > mat->rows )  
  74.            mat->cols = lines->total;  
  75.        else  
  76.            mat->rows = lines->total;  
  77.     }  
  78.    else  
  79.        result = lines;  

  80.    return result;  
  81. }  








5.2 OpenCV2.X中HoughLinesP()函数源码







[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. void cv::HoughLinesP( InputArray _image,OutputArray _lines,  
  2.                       double rho, double theta,int threshold,  
  3.                       double minLineLength,double maxGap )  
  4. {  
  5.    Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);  
  6.    Mat image = _image.getMat();  
  7.    CvMat c_image = image;  
  8.     CvSeq*seq = cvHoughLines2( &c_image, storage, CV_HOUGH_PROBABILISTIC,  
  9.                     rho, theta, threshold,minLineLength, maxGap );  
  10.    seqToMat(seq, _lines);  
  11. }  






可以发现其内部内部实现依然是基于旧版OpenCV 1.X的cvHoughLines2函数的,上面我们已经将cvHoughLines2()贴出来了,这里就不再次贴出了。






5.3 OpenCV2.X中HoughCircles()函数源码







[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. void cv::HoughCircles( InputArray _image,OutputArray _circles,  
  2.                        int method, double dp,double min_dist,  
  3.                        double param1, doubleparam2,  
  4.                        int minRadius, int maxRadius )  
  5. {  
  6.    Ptr<CvMemStorage> storage = cvCreateMemStorage(STORAGE_SIZE);  
  7.    Mat image = _image.getMat();  
  8.    CvMat c_image = image;  
  9.    CvSeq* seq = cvHoughCircles( &c_image, storage, method,  
  10.                     dp, min_dist, param1,param2, minRadius, maxRadius );  
  11.    seqToMat(seq, _circles);  
  12. }  





可以发现其内部内部实现是基于旧版OpenCV 1.X的cvHoughCircles,我们再来看看其旧版cvHoughCircles( )的函数源码。





5.3.1 OpenCV2.X中cvHoughCircles()函数源码






[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. CV_IMPL CvSeq*  
  2. cvHoughCircles( CvArr* src_image, void*circle_storage,  
  3.                 int method, double dp, doublemin_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.    int canny_threshold = cvRound(param1);  
  15.    int acc_threshold = cvRound(param2);  

  16.    img = cvGetMat( img, &stub );  

  17.    if( !CV_IS_MASK_ARR(img))  
  18.        CV_Error( CV_StsBadArg, "The source image must be 8-bit,single-channel" );  

  19.    if( !circle_storage )  
  20.        CV_Error( CV_StsNullPtr, "NULL destination" );  

  21.    if( dp <= 0 || min_dist <= 0 || canny_threshold <= 0 ||acc_threshold <= 0 )  
  22.        CV_Error( CV_StsOutOfRange, "dp, min_dist, canny_threshold andacc_threshold must be all positive numbers" );  

  23.    min_radius = MAX( min_radius, 0 );  
  24.    if( max_radius <= 0 )  
  25.        max_radius = MAX( img->rows, img->cols );  
  26.    else if( max_radius <= min_radius )  
  27.        max_radius = min_radius + 2;  

  28.    if( CV_IS_STORAGE( circle_storage ))  
  29.     {  
  30.        circles = cvCreateSeq( CV_32FC3, sizeof(CvSeq),  
  31.            sizeof(float)*3, (CvMemStorage*)circle_storage );  
  32.     }  
  33.    else if( CV_IS_MAT( circle_storage ))  
  34.     {  
  35.        mat = (CvMat*)circle_storage;  

  36.        if( !CV_IS_MAT_CONT( mat->type ) || (mat->rows != 1 &&mat->cols != 1) ||  
  37.            CV_MAT_TYPE(mat->type) != CV_32FC3 )  
  38.            CV_Error( CV_StsBadArg,  
  39.            "The destination matrix should be continuous and have a single rowor a single column" );  

  40.        circles = cvMakeSeqHeaderForArray( CV_32FC3, sizeof(CvSeq),sizeof(float)*3,  
  41.                 mat->data.ptr, mat->rows +mat->cols - 1, &circles_header, &circles_block );  
  42.        circles_max = circles->total;  
  43.        cvClearSeq( circles );  
  44.     }  
  45.    else  
  46.        CV_Error( CV_StsBadArg, "Destination is not CvMemStorage* norCvMat*" );  

  47.    switch( method )  
  48.     {  
  49.    case CV_HOUGH_GRADIENT:  
  50.        icvHoughCirclesGradient( img, (float)dp, (float)min_dist,  
  51.                                 min_radius,max_radius, canny_threshold,  
  52.                                 acc_threshold,circles, circles_max );  
  53.          break;  
  54.    default:  
  55.        CV_Error( CV_StsBadArg, "Unrecognized method id" );  
  56.     }  

  57.    if( mat )  
  58.     {  
  59.        if( mat->cols > mat->rows )  
  60.            mat->cols = circles->total;  
  61.        else  
  62.            mat->rows = circles->total;  
  63.     }  
  64.    else  
  65.        result = circles;  

  66.    return result;  
  67. }  











五、综合示例部分




这次的综合示例,浅墨在HoughLinesP函数的基础上,为其添加了用于控制其第五个参数阈值threshold的滚动条。于是便能通过调节滚动条,改变阈值,动态地控制霍夫线变换检测的线条多少。

废话不多说,直接上详细注释的代码:




[cpp] ​​view plain​​​​copy​​​​print​​​​?​​​​​​​​​







  1. //-----------------------------------【程序说明】----------------------------------------------  
  2. //      程序名称::《【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑 》 博文配套源码   
  3. //      开发所用IDE版本:Visual Studio 2010  
  4. //          开发所用OpenCV版本:   2.4.9  
  5. //      2014年5月26日 Created by 浅墨  
  6. //----------------------------------------------------------------------------------------------  

  7. //-----------------------------------【头文件包含部分】---------------------------------------  
  8. //      描述:包含程序所依赖的头文件  
  9. //----------------------------------------------------------------------------------------------   
  10. #include <opencv2/opencv.hpp>  
  11. #include <opencv2/highgui/highgui.hpp>  
  12. #include <opencv2/imgproc/imgproc.hpp>  

  13. //-----------------------------------【命名空间声明部分】--------------------------------------  
  14. //      描述:包含程序所使用的命名空间  
  15. //-----------------------------------------------------------------------------------------------   
  16. using namespace std;  
  17. using namespace cv;  


  18. //-----------------------------------【全局变量声明部分】--------------------------------------  
  19. //      描述:全局变量声明  
  20. //-----------------------------------------------------------------------------------------------  
  21. Mat g_srcImage, g_dstImage,g_midImage;//原始图、中间图和效果图  
  22. vector<Vec4i> g_lines;//定义一个矢量结构g_lines用于存放得到的线段矢量集合  
  23. //变量接收的TrackBar位置参数  
  24. int g_nthreshold=100;  

  25. //-----------------------------------【全局函数声明部分】--------------------------------------  
  26. //      描述:全局函数声明  
  27. //-----------------------------------------------------------------------------------------------  

  28. static void on_HoughLines(int, void*);//回调函数  
  29. static void ShowHelpText();  


  30. //-----------------------------------【main( )函数】--------------------------------------------  
  31. //      描述:控制台应用程序的入口函数,我们的程序从这里开始  
  32. //-----------------------------------------------------------------------------------------------  
  33. int main( )  
  34. {  
  35.     //改变console字体颜色  
  36.     system("color 3F");    

  37.     ShowHelpText();  

  38.     //载入原始图和Mat变量定义     
  39.     Mat g_srcImage = imread("1.jpg");  //工程目录下应该有一张名为1.jpg的素材图  

  40.     //显示原始图    
  41.     imshow("【原始图】", g_srcImage);    

  42.     //创建滚动条  
  43.     namedWindow("【效果图】",1);  
  44.     createTrackbar("值", "【效果图】",&g_nthreshold,200,on_HoughLines);  

  45.     //进行边缘检测和转化为灰度图  
  46.     Canny(g_srcImage, g_midImage, 50, 200, 3);//进行一次canny边缘检测  
  47.     cvtColor(g_midImage,g_dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图  

  48.     //调用一次回调函数,调用一次HoughLinesP函数  
  49.     on_HoughLines(g_nthreshold,0);  
  50.     HoughLinesP(g_midImage, g_lines, 1, CV_PI/180, 80, 50, 10 );  

  51.     //显示效果图    
  52.     imshow("【效果图】", g_dstImage);    


  53.     waitKey(0);    

  54.     return 0;    

  55. }  


  56. //-----------------------------------【on_HoughLines( )函数】--------------------------------  
  57. //      描述:【顶帽运算/黑帽运算】窗口的回调函数  
  58. //----------------------------------------------------------------------------------------------  
  59. static void on_HoughLines(int, void*)  
  60. {  
  61.     //定义局部变量储存全局变量  
  62.      Mat dstImage=g_dstImage.clone();  
  63.      Mat midImage=g_midImage.clone();  

  64.      //调用HoughLinesP函数  
  65.      vector<Vec4i> mylines;  
  66.     HoughLinesP(midImage, mylines, 1, CV_PI/180, g_nthreshold+1, 50, 10 );  

  67.     //循环遍历绘制每一条线段  
  68.     for( size_t i = 0; i < mylines.size(); i++ )  
  69.     {  
  70.         Vec4i l = mylines[i];  
  71.         line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(23,180,55), 1, CV_AA);  
  72.     }  
  73.     //显示图像  
  74.     imshow("【效果图】",dstImage);  
  75. }  

  76. //-----------------------------------【ShowHelpText( )函数】----------------------------------  
  77. //      描述:输出一些帮助信息  
  78. //----------------------------------------------------------------------------------------------  
  79. static void ShowHelpText()  
  80. {  
  81.     //输出一些帮助信息  
  82.     printf("\n\n\n\t请调整滚动条观察图像效果~\n\n");  
  83.     printf("\n\n\t\t\t\t\t\t\t\t by浅墨"  
  84.         );  
  85. }  







放一些运行截图吧。


原始图:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_38

阈值为95时:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_边缘检测_39


 阈值为35时:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_40


阈值为200时:

【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_double类型_41



本篇文章的配套源代码请点击这里下载:


​【浅墨OpenCV入门教程之十四】配套源代码下载​


 【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_霍夫变换_42

OK,今天的内容大概就是这些,我们下篇文章见:)