学习 opencv---(13)opencv霍夫变换:霍夫线变换,霍夫圆变换
在本篇文章中,我们将一起学习opencv中霍夫变换相关的知识点,以及了解opencv中实现霍夫变换的HoughLines,HoughLinesP函数的使用方法,实现霍夫圆变换的HoughCircles函数的使用方法。
先尝鲜一下其中一个示例程序的运行截图:
一、引言
在图像处理和计算机视觉领域中,如何从当前的图像中提取所需要的特征信息是图像识别的关键所在。在许多应用场合中需要快速准确的检测出直线或者圆。其中一种非常有效的解决问题的方法是霍夫(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 Probablilistic 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)在极坐标系:可由参数:极径和极角表示
对于霍夫变换,我们将采用第二种方式极坐标系来表示直线,因此,直线的表达式可为:
化简便可得到:
【2】一般来说对于点, 我们可以将通过这个点的一族直线统一定义为:
这就意味着每一对代表一条通过点的直线。
【3】如果对于一个给定点我们在极坐标对极径极角平面绘出所有通过它的直线, 将得到一条正弦曲线. 例如, 对于给定点X_0= 8 和Y_0= 6 我们可以绘出下图 (在平面):
只绘出满足下列条件的点 和 .
【4】我们可以对图像中所有的点进行上述操作. 如果两个不同点进行上述操作后得到的曲线在平面相交, 这就意味着它们通过同一条直线. 例如,接上面的例子我们继续对点 和点 绘图, 得到下图:
这三条曲线在平面相交于点 (0.925, 9.6), 坐标表示的是参数对 或者是说点, 点和点组成的平面内的的直线。
【5】以上的说明表明,一般来说, 一条直线能够通过在平面 寻找交于一点的曲线数量来检测。而越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成. 一般来说我们可以通过设置直线上点的阈值来定义多少条曲线交于一点我们才认为检测到了一条直线。
【6】这就是霍夫线变换要做的. 它追踪图像中每个点对应曲线间的交点. 如果交于一点的曲线的数量超过了阈值, 那么可以认为这个交点所代表的参数对在原图像中为一条直线。
3.3 HoughLines( )函数详解
此函数可以采用标准霍夫变换的二值图像线条。在opencv 中,我们可以用来调用标准霍夫变换SHT和多尺度霍夫变换MSHT的opencv内建算法
1 void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0 );
- 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
- 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量
- 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。PS:Latex中/rho就表示 。
- 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
- 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
- 第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
- 第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
在学完函数解析,看看浅墨为大家准备的以HoughLines为核心的示例程序,就可以全方位了解HoughLines函数的使用方法了:
1 #include <opencv2/opencv.hpp>
2 #include <opencv2/core/core.hpp>
3 #include <opencv2/highgui/highgui.hpp>
4 #include <iostream>
5
6 using namespace std;
7 using namespace cv;
8
9
10 /*----------------------------------------
11 (1)霍夫线变换:houghlines
12 -----------------------------------------*/
13 int main()
14 {
15 //载入原始图和Mat变量定义
16 Mat srcImage = imread("2.jpg");
17 Mat midImage, dstImage; //临时变量和目标图的定义
18
19 //进行边缘检测和转化为灰度图
20 Canny(srcImage,midImage,50,200,3); //进行一批canny边缘检测
21 cvtColor(midImage,dstImage,CV_GRAY2BGR); //转化边缘检测后的图为灰度图
22
23 //进行霍夫线变换
24 vector<Vec2f> lines; //定义一个矢量结构和lines用于存放得到的线段矢量集合
25 HoughLines(midImage,lines,1,CV_PI/180,150,0,0);
26
27 //依次在图中绘制出每条线段
28 for (size_t i = 0; i < lines.size(); i++)
29 {
30 float rho = lines[i][0];
31 float theta = lines[i][1];
32 Point pt1, pt2;
33 double a = cos(theta), b = sin(theta);
34 double x0 = a*rho, y0 = b*rho;
35 pt1.x = cvRound(x0 + 1000 * (-b)); //对double型的数据四舍五入
36 pt1.y = cvRound(y0 + 1000 * (a));
37 pt2.x = cvRound(x0 - 1000 * (-b));
38 pt2.y = cvRound(y0 - 1000 * (a));
39 line(dstImage, pt1, pt2, Scalar(55, 100, 195), 1, CV_AA); //绘图
40 }
41
42 //显示原始图
43 imshow("【原始图】", srcImage);
44
45 //边缘检测后的图
46 imshow("【边缘检测后的图】",midImage);
47
48 //显示效果图
49 imshow("【效果图】",dstImage);
50
51 waitKey();
52
53 return 0;
54 }
提示有内存错误,应该是图片大小的没问题。。。。。。。。。。。。。。用的是浅墨 大程序里的图片。。。。
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)来找出二值图像中的直线。
1 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,允许将同一行点与点之间连接起来的最大的距离。
1 /*----------------------------------------
2 (2)霍夫线变换:houghlinesP
3 -----------------------------------------*/
4 int main()
5 {
6 //载入原始图和Mat变量定义
7 Mat srcImage = imread("1.jpg");
8 Mat midImage, dstImage; //临时变量和目标图的定义
9
10 //进行边缘检测和转化为灰度图
11 Canny(srcImage, midImage, 50, 200, 3); //进行一批canny边缘检测
12 cvtColor(midImage, dstImage, CV_GRAY2BGR); //转化边缘检测后的图为灰度图
13
14 //进行霍夫线变换
15 vector<Vec4i> lines; //定义一个矢量结构和lines用于存放得到的线段矢量集合
16 HoughLinesP(midImage, lines, 1, CV_PI / 80, 50, 10);
17
18 //依次在图中绘制出每条线段
19 for (size_t i = 0; i < lines.size(); i++)
20 {
21 Vec4i l = lines[i];
22 line(dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186, 88, 255), 1, CV_AA);
23 }
24
25 //显示原始图
26 imshow("【原始图】", srcImage);
27
28 //边缘检测后的图
29 imshow("【边缘检测后的图】", midImage);
30
31 //显示效果图
32 imshow("【效果图】", dstImage);
33
34 waitKey(0);
35
36 return 0;
37 }
四、霍夫圆变换
霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代。说“大体上类似”的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替:在这三维中,一维是x,一维是y,另外一维是圆的半径r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。
对直线来说, 一条直线能由参数极径极角表示. 而对圆来说, 我们需要三个参数来表示一个圆, 也就是:
这里的 表示圆心的位置 (下图中的绿点) 而 r 表示半径, 这样我们就能唯一的定义一个圆了, 见下图:
在OpenCV中,我们一般通过一个叫做“霍夫梯度法”的方法来解决圆变换的问题。
4.1 霍夫梯度法的原理
【1】首先对图像应用边缘检测,比如用canny边缘检测。
【2】然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度。
【3】利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最小值到指定的最大值的距离。
【4】同时,标记边缘图像中每一个非0像素的位置。
【5】然后从二维累加器中de这些点中选择候选的中心,这些中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照累加值降序排列,以便于最支持像素的中心首先出现。
【6】接下来对每一个中心,考虑所有的非0像素。
【7】这些像素按照其与中心的距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径。
8.如果一个中心收到边缘图像非0像素最充分的支持,并且到前期被选择的中心有足够的距离,那么它就会被保留下来。
这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。
人无完人,金无足赤。同样,这个算法也并不是十全十美的,还有许多需要指出的缺点。
4.2 霍夫梯度法的缺点
<1>在霍夫梯度法中,我们使用Sobel导数来计算局部梯度,那么随之而来的假设是,其可以视作等同于一条局部切线,dan这个不是一个数值稳定的做法。在大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。
<2>在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。第三,因为每一个中心只选择一个圆,如果有同心圆,就只能选择其中的一个。
<3>因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是近似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,若是对于无穷分辨率的平滑图像而言的话,这才是必须的。
4.3 HoughCircles( )函数详解
HoughCircles函数可以利用霍夫变换算法检测出灰度图中的圆。它和之前的HoughLines和HoughLinesP比较明显的一个区别是它不需要源图是二值的,而HoughLines和HoughLinesP都需要源图为二值图像。
1 void HoughCirles(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函数检测出来的圆心,然后用额外的一些步骤来进一步确定半径。
1 /*----------------------------------------
2 (3)霍夫圆变换:houghcirles
3 -----------------------------------------*/
4 int main()
5 {
6 //(1)载入原始图
7 Mat srcImage = imread("1.jpg");
8 Mat midImage, dstImage; //临时变量和目标图的定义
9
10 //(2)显示原始图
11 imshow("【原始图】",srcImage);
12
13 //(3)转换为灰度图,进行图像平滑
14 cvtColor(srcImage,midImage,CV_BGR2GRAY); //转换边缘检测后的图为灰度图
15 GaussianBlur(midImage,midImage,Size(9,9),2,2);
16
17
18 //(4)进行霍夫圆变换
19 vector<Vec3f> circles;
20 HoughCircles(midImage,circles,CV_HOUGH_GRADIENT,1.5,10,200,100,0,0);
21
22 //(5)依次在图中绘制出圆
23 for (size_t i = 0; i < circles.size(); i++)
24 {
25 Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
26 int radius = cvRound(circles[i][2]);
27 //绘制圆心
28 circle(srcImage, center, 3, Scalar(0, 255, 0), -1, 8, 0);
29 //绘制圆轮廓
30 circle(srcImage, center, radius, Scalar(155, 50, 255), 3, 8, 0);
31 }
32
33 //(6)显示效果图
34 imshow("【效果图】", srcImage);
35
36 waitKey(0);
37
38 return 0;
39 }
五、综合示例部分
这次的综合示例,浅墨在HoughLinesP函数的基础上,为其添加了用于控制其第五个参数阈值threshold的滚动条。于是便能通过调节滚动条,改变阈值,动态地控制霍夫线变换检测的线条多少。
废话不多说,直接上详细注释的代码: