霍夫变换是一种思想,用来检测任意能够用数学公式表达的形状,即使这个形状被破坏或者有点扭曲。
霍夫变换的原理是将特定图形上的点变换到一组参数空间上,根据参数空间点的累计结果找到一个极大值对应的解,那么这个解就对应着要寻找的几何形状的参数(比如说直线,那么就会得到直线的斜率k与常数b,圆就会得到圆心与半径等等)。
很容易想到,我们用k,b作为参数空间表示,那么直角坐标系的点就变成了新空间里的线;直角坐标系里的直线就变成了新空间里的点。
故找直角坐标系里 过最多点的直线 就相当于 找新空间里 多个直线相交次数最多的那个点。这个点的k,b值即为我们寻求的直角坐标系里那条线的k,b。
但实际并没有转化到k,b的空间来表示,而是转化到了极坐标系空间,用ρ,θ来表示。这个会在下面具体阐述原因。
在笛卡尔平面(x和y轴)中,直线由公式y=kx+b定义,其中x和y对应于直线上的一个特定点,k和b分别对应于斜率和y轴截距。
平面被绘制成x和y值的函数,这意味着我们显示的是这条直线有多少(x, y)对组成(有无穷多的x, y对组成任何一条线,这就是为什么线延伸到无穷远的原因)。
但是,可以用它的k和b值绘制直线,这种空间的转换叫做霍夫空间。为了理解Hough变换算法,我们需要了解Hough空间是如何工作的。
在我们的用例中,我们可以将霍夫空间总结为两行:
- 笛卡尔平面上的点在霍夫空间中变成直线
- 笛卡尔平面上的直线在霍夫空间上变成点
想想线的概念,一条线基本上是由一个接一个有序排列的无穷长的点组成的。因为在笛卡尔平面上,我们画的线是x和y的函数,线被显示为无限长因为有无限多的(x, y)对组成了这条线。
现在在霍夫空间中,我们画出直线作为k和b值的函数。因为每条笛卡尔直线上只有一个k和b值,所以这条直线可以表示为一个点。
例如,方程y=2x+1表示笛卡尔平面上的一条直线。它的k和b值分别是’ 2 ‘和’ 1 ',这是这个方程唯一可能的k和b值。另一方面,这个方程可以有很多x和y的值,使得这个方程成立(左边=右边)。
如果我们要用k和b的值来画这个方程,我们只会用点(2,1);如果我们要用x和y的值来画这个方程,我们将会有无穷多的选择因为有无穷多的(x, y)对。
现在我们考虑笛卡尔平面上的一点。笛卡尔平面上的一个点只有一个可能的(x, y)对可以表示它,因此它是一个点,不是无限长。关于一个点,还有一个事实就是有无限多的可能的线可以通过这个点,换句话说,这个点可以满足无穷多个种 k,b。
目前,在笛卡尔平面中,我们根据x和y值绘制这个点。但是在霍夫空间中,我们根据它的k和b值来画这个点,因为有无限条线穿过这个点,所以在霍夫空间中会得到一条无限长的线。
以点(3,4)为例,可以通过该点的直线有:y= -4x+16, y= -8/3x + 12和y= -4/3x + 8(直线有无穷多,但为了简单起见,我们用3条直线)。
如果你们在霍夫空间中绘制每一条直线([- 4,16],[-8/ 3,12],[-4/ 3,8]),在笛卡尔空间中代表每条直线的点将在霍夫空间中形成一条直线(这条直线对应于点(3,4))。
现在如果我们在个笛卡尔平面上放置另一个点呢?这在霍夫空间会有什么结果呢?通过霍夫空间,我们可以找到笛卡尔平面上最适合这两点的直线。
我们可以通过在霍夫空间中绘制与笛卡尔空间中两点相对应的直线,并找到这两条直线在霍夫空间中相交的点(POI,交叉点)。
总结上述内容:
- 笛卡尔平面上的直线在霍夫空间中表示为点
- 笛卡儿平面上的点在霍夫空间中表示为直线
- 通过求霍夫空间中与这两个点对应的两条直线的POI的m和b坐标,可以找到笛卡尔空间中两点的最佳拟合直线,然后根据这些m和b的值组成一条直线。
但是在垂直线上,斜率是无穷大的,我们不能在霍夫空间中表示无穷,这将导致程序崩溃。所以我们不用y=kx+b来表示直线方程,我们用ρ和θ来定义直线,这也被称为极坐标系统。
具体直角坐标系与极坐标系的转化,可参考此博客
以直线检测为例,假设有一条直线L,原点到该直线的垂直距离为ρ,垂线与x轴夹角为θ ,那么这条直线是唯一的,且直线的方程为 ρ=xcosθ+ysinθ , 如下图所示:
同样的,一条直线在极坐标系下只有一个(ρ,θ) 与之对应
(和k,b类似,只是换了一种鄙表示方式),随便改变ρ或θ任何一个参数的大小,变换到空间域上的这个直线将会改变。
L直线上的所有点都必然是在极坐标为(ρ,θ) 所表示的直线上的,但是,它们也可以出现在其他的(ρ,直线上((ρ1,θ1),(ρ2,θ2)…),就比如L直线上的点(x,y)吧,它可以在很多直线上,准确的说,在经过这个点的直线上,随便画两条如下:
我们可以看到θ 无非是从0-360度(0−2π )变化,假设我们每1度取一个直线并保证(x,y)在这个直线上,那么(x,y)会在360条直线上出现。现在我们把这个情况画在极坐标轴上是什么样子的呢?
这个图体现了空间域上的一个点(x,y),可以出现的每一条可能的直线。每一个红点代表了一个(ρ,θ),也就代表着一条通过(x,y)点的直线。根据θ的变换步长不同而数量不同,如果θ的步长变化值为1,就有360个红点,如果θ的步长变化值为10,就有36个红点。
那么如果把空间域中每个点都这么找一圈并绘制在极坐标系下呢?也就是每个点在参数空间上都对应一系列的(ρ,θ)。现在把它们画在同一个坐标系下会怎么样呢?
为了方便,假设在这个直线上取3个点画一下:
在极坐标下,空间域中的每一个点都会存在一个周期曲线来表示通过这个点的直线。可以发现这三个极坐标系曲线同时经过一个点(ρ’,θ’)。极坐标上每一个点对应空间坐标上一条直线的,这就表示在空间坐标系下,有一条直线可以经过点1,经过点2,经过点3,也就说明这三个点是在一条直线上的,这条直线就是(ρ’,θ’)。反过来再来看这个极坐标系下的曲线,那么我们只需要找到交点最多的点,把它返回到空间域就是要找的直线了。一条直线上的所有点绘成的曲线交点势必是曲线相交次数最多的点
。
可以看到霍夫变换就是参数映射变换。对每一个点都进行映射,并且映射还不止一次。(ρ,θ) 是存在步长的,以θ 取步长为例,当θ 取得步长大的时候,映射的(ρ,θ) 对少些,反之则多。但是我们看到,映射后的点对是需要求交点的,上述画出来的曲线是连续的,然而实际上因为θ 步长的存在,他不可能是连续的,是离散的(相当于这个点转几度取一条直线
)。当θ 步长取得比较大的时候,你还想有很多交点是不可能的,所以说θ 步长不能太大,理论上是越小效果越好,因为越小,越接近于连续曲线,也就越容易相交。但是越小带来的问题就是计算量越大,假设一副100100的图像(很小吧),就有10000个点,对每个点假设就映射36组(θ步长值为10),那么总共需要映射360000次,在考虑每次映射计算的时间,可想而知霍夫的计算是耗时耗力的。所以必须对其进行改进。首先就是对图像进行改进,100100的图像,10000个点,是不是每个点都要计算?大可不必,我们只需要在开始把图像进行一个边缘提取,一般使用canny算子就可以,生成黑白二值图像,白的是边缘,那么在映射的时候,只需要把边缘上的点进行参数空间变换就可以。为什么提取边缘?想想无论检测图像中存在的直线呀圆呀,它们必然都是轮廓鲜明的。那么需要变换的点可能就从10000个点降到可能1000个点了,这也就是为什么看到许多霍夫变换提取形状时为什么要把图像提取边缘,变成二值图像了。
那么一个霍夫变换在算法设计上就可以如下步骤:
(1)将参数空间(ρ,θ) 量化,赋初值一个二维矩阵M,M(ρ,θ)
就是一个累加器了。这个二维数组的行代表不同的ρ,而列代表θ;初始时所有值均为0。数组的大小取决于算法的精度。假设所需角度的精度精确到1度,那么就需要360列。对于ρ,最大的可能距离是图像的对角长度,因此若需要一个像素的精度,那么行数就是图像对角线的长度。
(2)然后对图像边缘上的每一个点进行变换,变换到属于哪一组(ρ,θ) ,就把该组(ρ,θ)
对应的值增加1(这里的需要变换的点就是上面说的经过边缘提取以后的图像)。 (3)当所有点处理完成后,就来分析得到的M(ρ,θ)
,设置一个阈值T,认为当M(ρ,θ)>T ,就认为有一条直线存在。而对应的(ρ,θ)
就是这组直线的参数,至于T是多少,自己去式,试的比较合适为止。 (4)有了(ρ,θ) 和点(x,y)就可以计算出来这个直线了。
Opencv中使用霍夫变换检测直线的函数有cv2.HoughLines(),cv2.HoughLinesP()。
cv2.HoughLines()函数有四个输入,第一个是二值图像,也就是canny变换后的图像,二三参数分别是ρ和θ的精确度,也就是两者的步长,步长决定了累加器二维数组的大小。第四个参数为阈值T,累加器中的值高于T是才认为是一条直线。函数的返回值是一个numpy数组,shape为(N,1,2),表述的就是根据二值图像得到了N个(ρ,θ)。ρ的单位是像素长度(也就是直线到图像原点(0,0)点的距离),而θ的单位是弧度。
以下是代码示例:
import cv2
import numpy as np
'''图像中的直线检测
'''
img = cv2.imread('img/computer.jpg')
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray_img,50,150,apertureSize=3)
minLineLength = 30
maxLineGap = 5
print(np.pi/180)
lines = cv2.HoughLines(edges,1,np.pi/180,150)
for line in lines:
r,theta = line[0]
# Stores the value of cos(theta) in a
a = np.cos(theta)
# Stores the value of sin(theta) in b
b = np.sin(theta)
# x0 stores the value rcos(theta)
x0 = a*r
# y0 stores the value rsin(theta)
y0 = b*r
# x1 stores the rounded off value of (rcos(theta)-1000sin(theta))
x1 = int(x0 + 1000*(-b))
# y1 stores the rounded off value of (rsin(theta)+1000cos(theta))
y1 = int(y0 + 1000*(a))
# x2 stores the rounded off value of (rcos(theta)+1000sin(theta))
x2 = int(x0 - 1000*(-b))
# y2 stores the rounded off value of (rsin(theta)-1000cos(theta))
y2 = int(y0 - 1000*(a))
# cv2.line draws a line in img from the point(x1,y1) to (x2,y2).
# (0,0,255) denotes the colour of the line to be
#drawn. In this case, it is red.
cv2.line(img,(x1,y1), (x2,y2), (0,0,255),1)
cv2.imshow('edges',edges)
cv2.imshow('lines',img)
cv2.waitKey(-1)
cv2.destroyAllWindows()
运行结果:
可以修改cv2.HoughLines函数的第四个参数,也就是阈值T,会得到不同数量的直线检测效果。
函数cv2.HoughLinesP()是一种概率直线检测,我们知道,霍夫变换是一个耗时耗力的算法,尤其是每一个点计算,即使经过了canny转换了有的时候点的个数依然是庞大的。这个时候我们采取一种概率挑选机制,不是所有的点都计算,而是随机的选取一些个点来计算,相当于降采样了。这样的话我们的阈值设置上也要降低一些。在参数输入上多了两个参数:minLineLengh(线的最短长度,比这个短的都被忽略)和MaxLineCap(两条直线之间的最大间隔,小于此值,认为是一条直线)。输出上也变了,不再是直线参数的,这个函数输出的直接就是直线点的坐标位置,这样可以省去一系;列for循环中的由参数空间到图像的实际坐标点的转换。
import cv2
import numpy as np
img = cv2.imread('img/computer.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray,50,150,apertureSize =3)
lines = cv2.HoughLinesP(edges,1,np.pi/180,160,minLineLength=100,maxLineGap=10)
for line in lines:
x1,y1,x2,y2 = line[0]
cv2.line(img,(x1,y1),(x2,y2),(0,0,255),1)
cv2.imshow('edges',edges)
cv2.imshow('lines',img)
cv2.waitKey(-1)
cv2.destroyAllWindows()
本文参考:
https://blog.csdn.net/piglite/article/details/118312270