一、引言
在图像处理和计算机视觉领域中,如何从当前的图像中提取所需要的特征信息是图像识别的关键所在。在许多应用场合中需要快速准确地检测出直线或者圆。其中一种非常有效的解决问题的方法是霍夫(Hough)变换,其为图像处理中从图像中识别几何形状的基本方法之一,应用很广泛,也有很多改进算法。最基本的霍夫变换是从黑白图像中检测直线(线段)。
二、霍夫变换概述
霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。
霍夫变换于1962年由PaulHough首次提出,最初的Hough变换是设计用来检测直线和曲线,起初的方法要求知道物体边界线的解析方程,但不需要有关区域位置的先验知识。这种方法的一个突出优点是分割结果的Robustness,即对数据的不完全或噪声不是非常敏感。然而,要获得描述边界的解析表达常常是不可能的。后于1972年由Richard Duda & Peter Hart推广使用,经典霍夫变换用来检测图像中的直线,后来霍夫变换扩展到任意形状物体的识别,多为圆和椭圆。霍夫变换运用两个坐标空间之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间的一个点上形成峰值,从而把检测任意形状的问题转化为统计峰值问题。
三、霍夫线变换
1、OpenCV中的霍夫线变换
我们知道,霍夫线变换是一种用来寻找直线的方法. 在使用霍夫线变换之前, 首先要对图像进行边缘检测的处理,也即霍夫线变换的直接输入只能是边缘二值图像.
OpenCV支持三种不同的霍夫线变换,它们分别是:
1 标准霍夫变换(Standard Hough Transform,SHT)
2 多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)
3 累计概率霍夫变换(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函数调用。
2、HoughLines( )函数详解
此函数可以找出采用标准霍夫变换的二值图像线条。在OpenCV中,我们可以用其来调用标准霍夫变换SHT和多尺度霍夫变换MSHT的OpenCV内建算法。其函数原型如下:
1 C++: void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0 )
1 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像,可以将任意的源图载入进来后由函数修改成此格式后,再填在这里。
2 第二个参数,InputArray类型的lines,经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。
3 第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径。
4 第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
5 第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。
6 第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
7 第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
关于霍夫变换的原理及详细解释参见:
https://blog.csdn.net/poem_qianmo/article/details/26977557
http://homepages.inf.ed.ac.uk/rbf/HIPR2/hough.htm
【示例】
1 //霍夫线性变换
2 #include <opencv2/opencv.hpp>
3 #include <opencv2/imgproc/imgproc.hpp>
4
5
6 using namespace cv;
7
8 int main( )
9 {
10 //【1】载入原始图和Mat变量定义
11 Mat srcImage = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图
12 Mat midImage,dstImage;//临时变量和目标图的定义
13
14 //【2】进行边缘检测和转化为灰度图
15 Canny(srcImage, midImage, 50, 200, 3);//进行canny边缘检测
16 cvtColor(midImage,dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图
17
18 //【3】进行霍夫线变换
19 vector<Vec2f> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合
20 HoughLines(midImage, lines, 1, CV_PI/180, 150, 0, 0 );
21
22 //【4】依次在图中绘制出每条线段
23 for( size_t i = 0; i < lines.size(); i++ )
24 {
25 float rho = lines[i][0], theta = lines[i][1];
26 Point pt1, pt2;
27 double a = cos(theta), b = sin(theta);
28 double x0 = a*rho, y0 = b*rho;
29 pt1.x = cvRound(x0 + 1000*(-b));
30 pt1.y = cvRound(y0 + 1000*(a));
31 pt2.x = cvRound(x0 - 1000*(-b));
32 pt2.y = cvRound(y0 - 1000*(a));
33 line( dstImage, pt1, pt2, Scalar(55,100,195), 1, CV_AA);
34 }
35
36 //【5】显示原始图
37 imshow("【原始图】", srcImage);
38
39 //【6】边缘检测后的图
40 imshow("【边缘检测后的图】", midImage);
41 imwrite("midImage.jpg", midImage);
42
43 //【7】显示效果图
44 imshow("【效果图】", dstImage);
45 imwrite("dstImage.jpg", dstImage);
46 waitKey(0);
47
48 return 0;
49 }
效果展示(从桌子有依次是原图,边缘检测图,霍夫变换图):
PS:可以通过调节line(dstImage, pt1, pt2, Scalar(55,100,195), 1, CV_AA);一句Scalar(55,100,195)参数中G、B、R颜色值的数值,得到图中想要的线条颜色。
3、HoughLinesP( )函数详解
此函数在HoughLines的基础上末尾加了一个代表Probabilistic(概率)的P,表明它可以采用累计概率霍夫变换(PPHT)来找出二值图像中的直线。
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,允许将同一行点与点之间连接起来的最大的距离。
【示例】
1 //累计概率霍夫变换
2 #include <opencv2/opencv.hpp>
3 #include <opencv2/imgproc/imgproc.hpp>
4
5 using namespace cv;
6
7 int main( )
8 {
9 //【1】载入原始图和Mat变量定义
10 Mat srcImage = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图
11 Mat midImage,dstImage;//临时变量和目标图的定义
12
13 //【2】进行边缘检测和转化为灰度图
14 Canny(srcImage, midImage, 50, 200, 3);//进行一此canny边缘检测
15 cvtColor(midImage,dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图
16
17 //【3】进行霍夫线变换
18 vector<Vec4i> lines;//定义一个矢量结构lines用于存放得到的线段矢量集合
19 HoughLinesP(midImage, lines, 1, CV_PI/180, 80, 50, 10 );
20
21 //【4】依次在图中绘制出每条线段
22 for( size_t i = 0; i < lines.size(); i++ )
23 {
24 Vec4i l = lines[i];
25 line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(186,88,255), 1, CV_AA);
26 }
27
28 //【5】显示原始图
29 imshow("【原始图】", srcImage);
30
31 //【6】边缘检测后的图
32 imshow("【边缘检测后的图】", midImage);
33 imwrite("midImage.jpg", midImage);
34
35 //【7】显示效果图
36 imshow("【效果图】", dstImage);
37 imwrite("dstImage.jpg", dstImage);
38
39 waitKey(0);
40
41 return 0;
42 }
四、霍夫圆变换
霍夫圆变换的基本原理和上面讲的霍夫线变化大体上是很类似的,只是点对应的二维极径极角空间被三维的圆心点x, y还有半径r空间取代。说“大体上类似”的原因是,如果完全用相同的方法的话,累加平面会被三维的累加容器所代替:在这三维中,一维是x,一维是y,另外一维是圆的半径r。这就意味着需要大量的内存而且执行效率会很低,速度会很慢。
1、霍夫梯度法的原理
霍夫梯度法的原理是这样的。
【1】首先对图像应用边缘检测,比如用canny边缘检测。
【2】然后,对边缘图像中的每一个非零点,考虑其局部梯度,即用Sobel()函数计算x和y方向的Sobel一阶导数得到梯度。
【3】利用得到的梯度,由斜率指定的直线上的每一个点都在累加器中被累加,这里的斜率是从一个指定的最小值到指定的最大值的距离。
【4】同时,标记边缘图像中每一个非0像素的位置。
【5】然后从二维累加器中这些点中选择候选的中心,这些中心都大于给定阈值并且大于其所有近邻。这些候选的中心按照累加值降序排列,
以便于最支持像素的中心首先出现。
【6】接下来对每一个中心,考虑所有的非0像素。
【7】这些像素按照其与中心的距离排序。从到最大半径的最小距离算起,选择非0像素最支持的一条半径。8.如果一个中心收到边缘图像非0像素最充分的支持,
并且到前期被选择的中心有足够的距离,那么它就会被保留下来。
这个实现可以使算法执行起来更高效,或许更加重要的是,能够帮助解决三维累加器中会产生许多噪声并且使得结果不稳定的稀疏分布问题。
2、霍夫梯度法的缺点
<1>在霍夫梯度法中,我们使用Sobel导数来计算局部梯度,那么随之而来的假设是,其可以视作等同于一条局部切线,并这个不是一个数值稳定的做法。
在大多数情况下,这样做会得到正确的结果,但或许会在输出中产生一些噪声。
<2>在边缘图像中的整个非0像素集被看做每个中心的候选部分。因此,如果把累加器的阈值设置偏低,算法将要消耗比较长的时间。第三,因为每一个中心
只选择一个圆,如果有同心圆,就只能选择其中的一个。
<3>因为中心是按照其关联的累加器值的升序排列的,并且如果新的中心过于接近之前已经接受的中心的话,就不会被保留下来。且当有许多同心圆或者是近
似的同心圆时,霍夫梯度法的倾向是保留最大的一个圆。可以说这是一种比较极端的做法,因为在这里默认Sobel导数会产生噪声,若是对于无穷分辨率
的平滑图像而言的话,这才是必须的。
3、HoughCircles( )函数详解
HoughCircles函数可以利用霍夫变换算法检测出灰度图中的圆。它和之前的HoughLines和HoughLinesP比较明显的一个区别是它不需要源图是二值的,而HoughLines和HoughLinesP都需要源图为二值图像。
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 )
1 第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的灰度单通道图像。
2 第二个参数,InputArray类型的circles,经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的
浮点矢量(x, y, radius)表示。
3 第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参
数处填这个标识符即可。
4 第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。
上述文字不好理解的话,来看例子吧。例如,如果dp= 1时,累加器和输入图像具有相同的分辨率。如果dp=2,累加器便有输入图像一半那么大的宽度和高度。
5 第五个参数,double类型的minDist,为霍夫变换检测到的圆的圆心之间的最小距离,即让我们的算法能明显区分的两个不同圆之间的最小距离。
这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
6 第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度
法CV_HOUGH_GRADIENT,它表示传递给canny边缘检测算子的高阈值,而低阈值为高阈值的一半。
7 第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法
CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加
接近完美的圆形了。
8 第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。
9 第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。
需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。我们可以通过第八个参数minRadius和第九个参数maxRadius指定最小和最大的圆半径,来辅助圆检测的效果。或者,我们可以直接忽略返回半径,因为它们都有着默认值0,单单用HoughCircles函数检测出来的圆心,然后用额外的一些步骤来进一步确定半径。
【示例】
1 //霍夫圆变换
2 #include <opencv2/opencv.hpp>
3 #include <opencv2/imgproc/imgproc.hpp>
4
5 using namespace cv;
6
7 int main( )
8 {
9 //【1】载入原始图和Mat变量定义
10 Mat srcImage = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图
11 Mat midImage,dstImage;//临时变量和目标图的定义
12
13 //【2】显示原始图
14 imshow("【原始图】", srcImage);
15
16 //【3】转为灰度图,进行图像平滑
17 cvtColor(srcImage,midImage, CV_BGR2GRAY);//转化边缘检测后的图为灰度图
18 GaussianBlur( midImage, midImage, Size(9, 9), 2, 2 );
19
20 //【4】进行霍夫圆变换
21 vector<Vec3f> circles;
22 HoughCircles( midImage, circles, CV_HOUGH_GRADIENT,1.5, 10, 200, 100, 0, 0 );
23
24 //【5】依次在图中绘制出圆
25 for( size_t i = 0; i < circles.size(); i++ )
26 {
27 Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
28 int radius = cvRound(circles[i][2]);
29 //绘制圆心
30 circle( srcImage, center, 3, Scalar(0,255,0), -1, 8, 0 );
31 //绘制圆轮廓
32 circle( srcImage, center, radius, Scalar(155,50,255), 3, 8, 0 );
33 }
34
35 //【6】显示效果图
36 imshow("【效果图】", srcImage);
37 imwrite("效果图.jpg", srcImage);
38
39 waitKey(0);
40
41 return 0;
42 }
五、综合示例
1 //霍夫变换综合示例 2 #include <opencv2/opencv.hpp> 3 #include <opencv2/highgui/highgui.hpp> 4 #include <opencv2/imgproc/imgproc.hpp> 5 #include <iostream> 6 7 using namespace std; 8 using namespace cv; 9 10 Mat g_srcImage, g_dstImage,g_midImage;//原始图、中间图和效果图 11 vector<Vec4i> g_lines;//定义一个矢量结构g_lines用于存放得到的线段矢量集合 12 //变量接收的TrackBar位置参数 13 int g_nthreshold=100; 14 15 static void on_HoughLines(int, void*);//回调函数 16 static void ShowHelpText(); 17 18 int main( ) 19 { 20 ShowHelpText(); 21 22 //载入原始图和Mat变量定义 23 Mat g_srcImage = imread("1.jpg"); //工程目录下应该有一张名为1.jpg的素材图 24 25 //显示原始图 26 imshow("【原始图】", g_srcImage); 27 28 //创建滚动条 29 namedWindow("【效果图】",1); 30 createTrackbar("值", "【效果图】",&g_nthreshold,200,on_HoughLines); 31 32 //进行边缘检测和转化为灰度图 33 Canny(g_srcImage, g_midImage, 50, 200, 3);//进行一次canny边缘检测 34 cvtColor(g_midImage,g_dstImage, CV_GRAY2BGR);//转化边缘检测后的图为灰度图 35 36 //调用一次回调函数,调用一次HoughLinesP函数 37 on_HoughLines(g_nthreshold,0); 38 HoughLinesP(g_midImage, g_lines, 1, CV_PI/180, 80, 50, 10 ); 39 40 //显示效果图 41 imshow("【效果图】", g_dstImage); 42 imwrite("累计概率霍夫变换.jpg", g_dstImage); 43 44 45 waitKey(0); 46 47 return 0; 48 49 } 50 51 static void on_HoughLines(int, void*) 52 { 53 //定义局部变量储存全局变量 54 Mat dstImage=g_dstImage.clone(); 55 Mat midImage=g_midImage.clone(); 56 57 //调用HoughLinesP函数 58 vector<Vec4i> mylines; 59 HoughLinesP(midImage, mylines, 1, CV_PI/180, g_nthreshold+1, 50, 10 ); 60 61 //循环遍历绘制每一条线段 62 for( size_t i = 0; i < mylines.size(); i++ ) 63 { 64 Vec4i l = mylines[i]; 65 line( dstImage, Point(l[0], l[1]), Point(l[2], l[3]), Scalar(23,180,55), 1, CV_AA); 66 } 67 //显示图像 68 imshow("【效果图】",dstImage); 69 imwrite("dstImage.jpg", dstImage); 70 } 71 72 static void ShowHelpText() 73 { 74 //输出一些帮助信息 75 cout << "\n\n\n\t请调整滚动条观察图像效果~\n\n"; 76 cout << "\n\n\t\t\t\t\t\t"; 77 }