霍夫变换是图像处理中从图像中识别几何形状的基本方法之一,应用很广泛,也有很多改进算法。主要用来从图像中分离出具有某种相同特征的几何形状(如,直线,圆等)。最基本的霍夫变换是从黑白图像中检测直线(线段)。
霍夫空间
霍夫变换的关键是霍夫空间。
左边的图像空间中的一条线,我们要构造的是霍夫空间,右边是霍夫空间。在这个表述中,我们有两个参数 m 和 b 。所以,对于这条直线
,它在Hough空间中的
处表示:
这就是霍夫空间的意义所在。这里的关键思想是:图像中的直线对应于霍夫空间中的一点。
现在我们要做一些不同的事情。假设在图像空间中只有一个点,我们把这个点放在(x,y) 处:
那么,经过这一点的直线的方程是什么呢?在图像空间中,我们知道经过这一点的直线满足 m 和 b。它会满足方程
,为了通过点(
,
)它必须有一个 m 和一个 b 使这个方程成立。通过一些简单的代数重排,就得到
,这是m b空间中直线的方程:
实际上,它是一条直线:
这里的思想是:图像空间中的一点是霍夫空间中的一条线。如果增加一个点,这是另一条直线:
,
这是一条直线 :
。那么什么样的直线是跟这两点的情况符合的? 它是Hough空间中的这两条线相交的地方:
因为交点是相同的m和b,它与穿过(
,
)的线 和 穿过(
,
)的线是一致的,这就是我们如何从点中找到直线的方法。
算法:
基本上,每个点都给了Hough空间中的一条直线:创建一个表格,这里是m和b,它是由一组箱子组成的:
每一个点都对经过的箱子进行投票,所以它会对经过的每个箱子投一票。收集了每个箱子的票数情况。每一个点都投过票,哪一个票数最多,那就是你的路线:
因此,基本上,我们将把选票投进箱子,然后找到最多选票的箱子。求这个点坐标就求出直线了。但一般是用极坐标表示,因为极坐标更加细化。
交点怎么求解呢?
在理论上,一个点对应无数条直线或者说任意方向的直线(在参数空间中坐标轴θ有无数个),但在实际应用中,我们必须限定直线的数量(即有限数量的方向)才能够进行计算。因此,我们将直线的方向θ离散化为有限个等间距的离散值,参数ρ也就对应离散化为有限个值,于是参数空间不再是连续的,而是被离散量化为一个个等大小网格单元。将图像空间(直角坐标系)中每个像素点坐标值变换到参数空间(极坐标系)后,所得值会落在某个网格内,使该网格单元的累加计数器加1。当图像空间中所有的像素都经过霍夫变换后,对网格单元进行检查,累加计数值最大的网格,其坐标值(ρ0, θ0)就对应图像空间中所求的直线。
from:
from:
一、直线检测
1.直线的表示方式
对于平面中的一条直线,在笛卡尔坐标系中,常见的有点斜式,两点式两种表示方法。然而在hough变换中,考虑的是另外一种表示方式:使用(r,theta)来表示一条直线。其中r为该直线到原点的距离,theta为该直线的垂线与x轴的夹角。如下图所示。
也就是霍夫变换中表示一条直线的参数变成了(r,theta)。如果对位于同一直线上的n个点进行变换,原图像空间的n个点在参数空间对应得到有n条正弦曲线,并且这些曲线相交于一点。
2.如何判断多个点是否在同一直线上
当我们的对象变成点时,我们知道一个点可以发射出无数条直线,根据霍夫变换的直线表达形式,假设这个点为 i, 则通过这个点的直线我们用(ri,thetai)表示。再假设一个点为 j,则通过点 j 的一系列直线我们用(rj,thetaj)表示。我们知道两点决定一条直线,所以这两个点的直线必定有ri=rj,thetai= thetaj的时候。那如果是三个点呢,假设第三个点是k,则通过k点的一系列直线为(rk,thetak),如果三点在一条直线上,那必定有某个ri=rj=rk = r,thetai = thetaj= thetak = theta。
在霍夫变换检测直线时我们需要找到这样一样直线,如何找到这条直线呢?
3.如何检测出直线
假设有N个点,我们要检测其中的直线,也就是我们要找到具体的r和theata。对于上面所说的每个点可以通过无数条直线,这里我们设为n条(通常 n = 180),则我们一起可以找到N*N个(r, theata),对这N*N个(r,theata),我们可以利用统计学,统计到在theta=某个值theta_i时,多个点的r近似相等于r_i。也就是说这多个点都在直线(r_i,theta_i)上。
4.举例说明
如果空间中有3个点,如何判断这三个点在不在一个直线上,如果在,这条直线的位置为?这个例子中,对于每个点均求过该点的6条直线的(r,theta)坐标,共求了3*6个(r,theta)坐标。可以发现在theta=60时,三个点的r都近似为80.7,由此可判定这三个点都在直线(80.7,60)上。通过 r-o-theta 坐标系可更直观表示这种关系,如下图:图中三个点的(r,theta)曲线汇集在一起,该交点就是同时经过这三个点的直线。
在实际的直线检测情况中,如果超过一定数目的点拥有相同的(r,theta)坐标,那么就可以判定此处有一条直线。在r-O-theta 坐标系图中,明显的交汇点就标示一条检测出的直线。如下图,可以判定出平面上的点共构成了两条直线,即检测出两条直线。
下面给出函数HoughLines的霍夫变换直线检测的步骤:
如前所述,霍夫直线检测就是把图像空间中的直线变换到参数空间中的点,通过统计特性来解决检测问题。具体来说,如果一幅图像中的像素构成一条直线,那么这些像素坐标值(x, y)在霍夫空间对应的曲线一定相交于一个点,所以我们只需要将图像中的所有像素点(坐标值)变换成霍夫空间的曲线,并在霍夫空间检测曲线交点就可以确定直线了。
1、对边缘图像进行霍夫空间变换;
2、在4邻域内找到霍夫空间变换的极大值;
3、对这些极大值安装由大到小顺序进行排序,极大值越大,越有可能是直线;
4、输出直线。
函数原型:
void HoughLines( InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double srn=0, double stn=0 );
//! finds line segments in the black-n-white image using probabilistic Hough transform
void HoughLinesP( InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double minLineLength=0, double maxLineGap=0 );
第一个参数,InputArray类型的image,输入图像,即源图像,需为8位的单通道二进制图像。
第二个参数,InputArray类型的lines,是 vector<Vec2f> lines 类型;经过调用HoughLines函数后储存了霍夫线变换检测到线条的输出矢量。每一条线由具有两个元素的矢量表示,其中,是离坐标原点((0,0)(也就是图像的左上角)的距离。 是弧度线条旋转角度(0~垂直线,π/2~水平线)。
第三个参数,double类型的rho,以像素为单位的距离精度。另一种形容方式是直线搜索时的进步尺寸的单位半径 。
第四个参数,double类型的theta,以弧度为单位的角度精度。另一种形容方式是直线搜索时的进步尺寸的单位角度。
第五个参数,int类型的threshold,累加平面的阈值参数,即识别某部分为图中的一条直线时它在累加平面中必须达到的值。大于阈值threshold的线段才可以被检测通过并返回到结果中。在步骤2中,只有大于该值的点才有可能被当作极大值,即至少有多少条正弦曲线交于一点才被认为是直线。
第六个参数,double类型的srn,有默认值0。对于多尺度的霍夫变换,这是第三个参数进步尺寸rho的除数距离。粗略的累加器进步尺寸直接是第三个参数rho,而精确的累加器进步尺寸为rho/srn。
第七个参数,double类型的stn,有默认值0,对于多尺度霍夫变换,srn表示第四个参数进步尺寸的单位角度theta的除数距离。且如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
HoughLinesP原函数:功能:将输入图像按照给出参数要求提取线段,放在lines中。
lines:是一个vector<Vec4i>,Vec4i是一个包含4个int数据类型的结构体,[x1,y1,x2,y2],可以表示一个线段。
rho:就是一个半径的分辨率。 theta:角度分辨率。 threshold:判断直线点数的阈值。
minLineLength:线段长度阈值。 minLineGap:线段上最近两点之间的阈值。
实例:
用opencv进行霍夫变化检测直线的例子:http://blog.sina.com.cn/s/blog_60b330b801018md4.html
先进行边缘提取,再设置一些线段的容忍长度,可以检测到直线。
#include "opencv2/opencv.hpp"
#define PI 3.1415926
int main(int argc, char *argv[])
{
cv::Mat image = cv::imread ("road.jpg");
cv::Mat result;
cv::cvtColor (image,result,CV_BGRA2GRAY);
cv::Mat contours;
//边缘检测
cv::Canny (result,contours,125,350);
std::vector<cv::Vec2f> lines;
//霍夫变换,获得一组极坐标参数(rho,theta),每一对对应一条直线,保存到lines
//第3,4个参数表示在(rho,theta)坐标系里横纵坐标的最小单位,即步长
cv::HoughLines (contours,lines,1,PI/180,80);
std::vector<cv::Vec2f>::const_iterator it = lines.begin ();
std::cout<<lines.size ()<<std::endl;
while(it != lines.end()){
float rho = (*it)[0];
float theta = (*it)[1];
if(theta<PI/4.||theta>3.*PI/4){
//画交点在上下两边的直线
cv::Point pt1(rho/cos(theta),0);
cv::Point pt2((rho-result.rows*sin(theta))/cos(theta),result.rows);
cv::line(image,pt1,pt2,cv::Scalar(255),1);
}
else {
//画交点在左右两边的直线
cv::Point pt1(0,rho/sin(theta));
cv::Point pt2(result.cols,(rho-result.cols*cos(theta)/sin(theta)));
cv::line(image,pt1,pt2,cv::Scalar(255),1);
}
++it;
}
cv::namedWindow ("hough");
cv::imshow("hough",image);
cv::waitKey (0);
}
二、圆检测
霍夫圆变换的基本思路是认为图像上每一个非零像素点都有可能是一个潜在的圆上的一点,跟霍夫线变换一样,也是通过投票,生成累积坐标平面,设置一个累积权重来定位圆。
1.圆的表示方式
在笛卡尔坐标系中圆的方程为:
其中(a,b)是圆心,r是半径,也可以表述为:
,即:
不在一条直线上的三点确定一个圆,使用(a,b,r)来确定一个圆心为(a,b)半径为 r 的圆。所以在abr组成的三维坐标系中,一个点可以唯一确定一个圆。
2.如何判断多个点是否在一个圆上
同一个圆上的所有点的圆方程是一样的,它们映射到abr坐标系中的是同一个点,所以在abr坐标系中该点就应该有圆的总像素N0个曲线相交。通过判断abr中每一点的相交(累积)数量,大于一定阈值的点就认为是圆。
。Opencv霍夫圆变换对标准霍夫圆变换做了运算上的优化。它采用的是“霍夫梯度法”。它的检测思路是去遍历累加所有非零点对应的圆心,对圆心进行考量。如何定位圆心呢?圆心一定是在圆上的每个点的模向量上,即在垂直于该点并且经过该点的切线的垂直线上,这些圆上的模向量的交点就是圆心。
3.如何检测圆
要将圆检测出来,也就是要将上述的(a,b,r)求解出来。假设r确定,此时点(x,y)已知,根据(x-a)^2 +(y-b)^ 2=r ^ 2 ,则(a,b)的轨迹在几何上则变成了以(x,y)为圆心,r为半径的圆;而r不确定时,(a,b,r)的轨迹变成了以(x,y)为顶点的一个圆锥。则(ai,bi,ri),(aj,bj,rj), (ak,bk,rk)的圆为下图中圆锥面的交点A。
原则上,霍夫变换可以检测任何形状。但复杂的形状需要的参数就多,霍夫空间的维数就多,因此在程序实现上所需的内存空间以及运行效率上都不利于把标准霍夫变换应用于实际复杂图形的检测中。所以一些改进的霍夫变换就相继提出,它们的基本原理就是尽可能减小霍夫空间的维数。
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步骤,得到所有的圆半径。
具体步骤为:
1)首先对图像进行边缘检测,调用opencv自带的cvCanny()函数,将图像二值化,得到边缘图像。
2)对边缘图像上的每一个非零点【即边缘点】。采用cvSobel()函数,计算x方向导数和y方向的导数,从而得到梯度。从边缘点,沿着梯度和梯度的反方向,对参数指定的min_radius到max_radium的每一个像素,在累加器中被累加(对于在min_radius到max_radium范围内的每一个像素:计数加1)。同时记下边缘图像中每一个非0点的位置。
3)从(二维)累加器中这些点中选择候选中心。这些中心都大于给定的阈值和其相邻的四个邻域点的累加值。
4)对于这些候选中心按照累加值降序排序,以便于最支持的像素的中心首次出现。
5)对于每一个中心,考虑到所有的非0像素(非0,梯度不为0),这些像素按照与其中心的距离排序,从最大支持的中心的最小距离算起,选择非零像素最支持的一条半径。
6)如果一个中心受到边缘图像非0像素的充分支持,并且到前期被选择的中心有足够的距离。则将圆心和半径压入到序列中,得以保留。
函数原型:
//! finds circles in the grayscale image using 2+1 gradient Hough transform
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,vector<Vec3f> circles; 经过调用HoughCircles函数后此参数存储了检测到的圆的输出矢量,每个矢量由包含了3个元素的浮点矢量(x, y, radius)表示。
第三个参数,int类型的method,即使用的检测方法,目前OpenCV中就霍夫梯度法一种可以使用,它的标识符为CV_HOUGH_GRADIENT,在此参数处填这个标识符即可。
第四个参数,double类型的dp,用来检测圆心的累加器图像的分辨率于输入图像之比的倒数,且此参数允许创建一个比输入图像分辨率低的累加器。例如,dp=1时表示霍夫空间与输入图像空间的大小一致,dp=2时霍夫空间是输入图像空间的一半,以此类推
第五个参数,minDist为圆心之间的最小距离,如果检测到的两个圆心之间距离小于该值,则认为它们是同一个圆心。即让我们的算法能明显区分的两个不同圆之间的最小距离。这个参数如果太小的话,多个相邻的圆可能被错误地检测成了一个重合的圆。反之,这个参数设置太大的话,某些圆就不能被检测出来了。
第六个参数,double类型的param1,有默认值100。它是第三个参数method设置的检测方法的对应的参数。对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示传递给边缘检测时使用Canny算子的高阈值,而低阈值为高阈值的一半。
第七个参数,double类型的param2,也有默认值100。它是第三个参数method设置的检测方法的对应的参数。为步骤1.5和步骤2.5中所共有的阈值对当前唯一的方法霍夫梯度法CV_HOUGH_GRADIENT,它表示在检测阶段圆心的累加器阈值。它越小的话,就可以检测到更多根本不存在的圆,而它越大的话,能通过检测的圆就更加接近完美的圆形了。
第八个参数,int类型的minRadius,有默认值0,表示圆半径的最小值。
第九个参数,int类型的maxRadius,也有默认值0,表示圆半径的最大值。需要注意的是,使用此函数可以很容易地检测出圆的圆心,但是它可能找不到合适的圆半径。
from:
例子用一个opencv的霍夫变换检测圆:
找到灰度图后进行双边滤波,利用houghcircle检测圆,再利用circle将圆显示出来。
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
const int kvalue = 15;//双边滤波邻域大小
int main()
{
Mat src_color = imread("1.png");//读取原彩色图
Mat dst(src_color.size(), src_color.type());
dst = Scalar::all(0);
Mat src_gray;
cvtColor(src_color, src_gray, COLOR_BGR2GRAY);
imshow("原图-灰度", src_gray);
Mat bf;//对灰度图像进行双边滤波
bilateralFilter(src_gray, bf, kvalue, kvalue*2, kvalue/2);
imshow("灰度双边滤波处理", bf);
vector<Vec3f> circles;//声明一个向量,保存检测出的圆的圆心坐标和半径
HoughCircles(bf, circles, CV_HOUGH_GRADIENT, 1.5, 20, 130, 38, 10, 50);//霍夫变换检测圆
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( dst, center, 0, Scalar(0, 255, 0), -1, 8, 0 );
circle( dst, center, radius, Scalar(0, 0, 255), 1, 8, 0 );
cout << cvRound(circles[i][0]) << "\t" << cvRound(circles[i][1]) << "\t"
<< cvRound(circles[i][2]) << endl;//在控制台输出圆心坐标和半径
}
imshow("特征提取", dst);
waitKey();
}