概述:
霍夫变换可以用来检测直线或圆。它的思想简单且奇妙,给定一个物件,要辨别的形状的种类,算法会在参数空间中执行投票来决定物体的参数,而这是由累加器里的局部最大值决定。因此霍夫变换也被视为一种投票机制。
霍夫变换最初被设计成用来检测能够精确的解析定义的形状(例如直线,圆等)。在这些情况下,我们可以通过对于形状信息的充分了解来找出他们在图像中的位置。而广义霍夫变换在霍夫变换的基础上根据模板匹配的原理进行了调整,广义霍夫变换不要求能够给出需要检测的形状的解析式,它可以检测任意给定的形状。本文我们不讨论广义的霍夫变换,只讨论它是如何检测直线和圆的。
直线检测
基本原理
考虑如图1所示的问题,在一个笛卡尔坐标系中,存在着这些坐标已知点,我们希望在这些点中找到如图所示的一条直线。实际上是要确定该直线的斜率和截距。
图1
霍夫变换将已知点的坐标带入直线方程,形成一个以参数和为未知数的方程,如:将点代入,得到:
这是一条以和为未知数的直线,而他所在的空间也就被成为参数空间(霍夫空间)。不难看出,图像空间上的一个点被映射到参数空间就变成了一条直线,那么两个点自然会产生两条直线。如图2所示,两条直线的交点便是参数和的值,于是我们要检测的那条直线方程便确定了。
图2
同样的,图像空间上的多个点在参数空间上就是多条直线,这些直线的交点就是图像空间上要找的那条直线。
图3
在实际操作过程中,我们的参数空间实际上是离散化表示的(一个一个小方格,毕竟图像的表示也是一个一个小方格),如图3所示。参数空间上,每个小方格初始值都为0,每条直线所经过的小方格均加1。当存在多条直线时,如图4,便会找到一个数值最大的小方格,这个小方格对应的和的值便是我们要找的直线参数值。
图3
图4
这个执行加1的空间也被称为累加器空间,由于最终我们要找的点是权值最大的点,因此霍夫变换也被视为一种投票机制。
但这也引出一个新问题:假设我们要找的直线是一条垂直x轴的直线,它的斜率是无穷,即的值会被取到无穷大,而累加器不可能做到无限大。于是在霍夫变换中,直线方程都被写成极坐标的形式。如下,参数空间变成了关于和的空间,这里,的范围可控。在极坐标的参数空间中,直线上的一个点会被映射成为一条正(余)弦曲线,但这并不影响我们在累加器上做加1操作。(图5中,直线上的两个点再参数空间产生了两个交点,这两个交点只是相差了180度,表示的仍然是同一条直线)
图5
最后补充一些问题:
1、如果参数空间中不只存在一个交点?
选择的是累加器里局部值最大的那个点,如果有多个极大值点,那就意味着找到了多条直线。
2、累加器空间的方格应该怎么设置?
如果每个小方格设置的范围太大,可能会将不同的线融合在一起,精确度下降。如果范围设置的太小,可能对噪声更敏感,噪声点产生的线也会产生新交点。
图6展示了霍夫变换实际应用的一个示例,首先对物体图像计算了每个点的梯度得到梯度图,增加一个阈值得到较为清晰的边缘部分,将这些边缘点转换到参数空间,寻找累加器权值极大值点,最终拟合出所示的直线。
图6
Python实现
假设有一个100x100像素的图片,现在使用霍夫变换检测图片中的直线:
1、创建一个二维数组(累加器空间),初始化所有值为0。数组的行表示,列表示。数组尺寸的大小决定着结果的准确性。如果我们希望直线的角度的精度为1度,那这个数组的列就设置为180列,每列是一度,依次从0度到180度。如果我们希望直线的精度达到像素级别的,的最大值就应该等于图像的对角线距离,所以r就从0取到图像的对角线长度值。
2、遍历原图中的每一个点,计算每个点在取0-180之间的每个,如果这个数值在上述累加器中存在相应的位置,则在该位置上加1。
3、搜索累加器中的最大值,并找到这个最大值对应的和,就可以将图像中直线表示出来了。
API
- 1、霍夫变换
lines = cv2.HoughLines(img, rho, theta, threshold)
img:源图像,必须是8位的单通道二值图像。所以在进行霍夫变换之前要先把源图像二值化,或者进行边缘检测。
rho:就是,一般情况下使用的精度是1。 theta:就是, 一般情况下使用pi除以180,表示要搜索所有可能的角度。
threshold:是阈值,是判断要多少个点落在直线上,这个参数和我们前面的累加器里面的结果相对应。如果我们想检测多条直线,这个阈值就设置的小一点,反之设置的大一点。
返回值lines是(,),是一对儿浮点数,是numpy.ndarray类型的。- 2、概率霍夫变换
上面的霍夫变换检测直线时,非常容易出现误检测,并且检测出很多重复的结果,为了解决这些缺点,科学家们又提出了霍夫变换的改进版——概率霍夫变换,是霍夫变换算法的优化。
概率霍夫变换没有考虑所有的点,它只需要一个足以进行线检测的随机点子集即可。此外概率霍夫变换算法还对选取直线的方法进行了2点改进:
- 所接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。
- 接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原始图像中并不存在这条直线。
- lines = cv2.HoughLinesP(img, rho, theta, threshold, minLineLength, maxLineGap)
img:必须是二值图像。
rho:r
theta:θ
threshold:阈值,该值越小,判定出的直线越多,反之,判断出的直线越少。
minLineLength:默认值是0,用来控制"接受直线的最小长度"。
maxLineGap:默认值是0,用来控制接受公共线段之间的最小间隔,即在一条线中两点的最大间隔。如果两点间的间隔超过了该参数值,就认为这两个点不在一条直线上。
返回值lines是两个点的坐标。
import cv2
import numpy as np
from matplotlib import pyplot as plt
img_color = cv2.imread('keyboard.jpg')
img = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY) # 转化为灰度图像用于边缘检测
edges = cv2.Canny(img, 50, 150)
# 霍夫变换
lines1 = cv2.HoughLines(edges, 1, np.pi / 180, 200)
# 概率霍夫变换
lines2 = cv2.HoughLinesP(edges, 1, np.pi / 180, 1, minLineLength=10, maxLineGap=10)
# 绘制霍夫变换的图像
result1 = img_color.copy()
if lines1 is not None:
for i in lines1:
rho, theta = i[0]
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv2.line(result1, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 绘制概率霍夫变换的图像
result2 = img_color.copy()
if lines2 is not None:
for i in lines2:
x1, y1, x2, y2 = i[0]
cv2.line(result2, (x1, y1), (x2, y2), (0, 0, 255), 2)
# 可视化
plt.figure(figsize=(12, 3))
plt.subplot(141), plt.imshow(cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB))
plt.title('Original Image')
plt.subplot(142), plt.imshow(edges, cmap='gray')
plt.title('Edge Map')
plt.subplot(143), plt.imshow(cv2.cvtColor(result1, cv2.COLOR_BGR2RGB))
plt.title('Hough Lines')
plt.subplot(144), plt.imshow(cv2.cvtColor(result2, cv2.COLOR_BGR2RGB))
plt.title('Probabilistic Hough Lines')
plt.show()
圆形检测
基本原理
圆的检测基本思路同直线检测一致,依然是将图像空间上的点映射到参数空间。与直线映射不同的是,圆上的点映射到参数空间不再是一条直线,这分成圆的半径已知和未知两种情况。
假设圆的半径是已知的,那么检测该圆就只需要知道圆心即可,如图7所示,图像空间上的圆映射到参数空间仍然是一个圆。这个参数空间是一个关于参数、的空间,参数空间圆的半径仍然是。多个点产生的多个圆也会出现一个权值最大的交点。
图7
图8
假设圆的半径是未知的,那么图像空间圆的点映射到参数空间应该是一个三维的圆锥体,这个参数空间由参数、和半径构成。
Python实现
在霍夫圆变换中,需要考虑圆的半径和圆形(x坐标和y坐标)三个参数。 在opencv中采用的策略是两轮筛选,第一轮筛选是找出可能存在圆的位置(也就是圆心),第二轮筛选是根据第一轮的结果筛选出半径。
与用来决定是否接受直线的两个参数"接受直线的最小长度(minLineLength)"和"接受直线时允许的最大像素点间距(MaxLineGap)"类似,霍夫圆变换也有几个用于决定是否接受圆的参数:圆心间的最小距离、圆的最小半径、圆的最大半径。
API
- circles = cv2.HoughCircles(img, method, dp, minDist, param1, param2, minRadius, maxRadius)
img:8位单通道灰度图像
method:检测方法。目前的版本只有HOUGH_GRADIENT是唯一可用的参数值,就是上面说的两轮检测的方法。
dp:累计器分辨率,用来指定图像分辨率与圆心累加器分辨率的比例。例如,pd=1表示输入图像和累加器具有相同的分辨率。
minDist:圆心间的最小间距。如果该参数太大,可能会在检测时漏掉一些圆,如果该参数太小,会有多个临近的圆被检测出来。
param1:该参数时缺省的,在缺省时默认值是100。它对应的是canny边缘检测器的高阈值(低阈值是高阈值的二分之一)
param2:圆心位置必须收到的投票数。只有在第一轮筛选过程中,投票数超过该值的圆,才有资格进入第二轮的筛选。因此,该值越大,检测到的圆越少,该值越小,检测到的圆越多。这个参数也是缺省的,缺省时的默认值是100。
minRadius:圆半径的最小值,小于该值的圆不会被检测出来。该参数也是缺省的,在缺省时默认值是0,此时这个参数不起作用。
maxRadius:圆半径的最大值,大于该值的圆不会被检测出来。该参数也是缺省的,在缺省时默认值是0,此时这个参数不起作用。
返回值circles是由圆心坐标和半径构成的numpy.ndarray
说明:在调用这个api之前,要对源图像进行平滑操作,以减少图像中的噪声,避免发生误判。
import cv2
import numpy as np
import matplotlib.pyplot as plt
img_color = cv2.imread('coin.jpg')
img = cv2.cvtColor(img_color, cv2.COLOR_BGR2GRAY) # 转换为灰度图像
img_blur = cv2.medianBlur(img, 5) # 中值滤波平滑
circles = cv2.HoughCircles(img_blur, cv2.HOUGH_GRADIENT, 1, 300,
param1=100, param2=100, minRadius=200, maxRadius=400)
if circles is not None:
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
center = (i[0], i[1])
radius = i[2]
# 绘制圆轮廓
cv2.circle(img_color, center, radius, (0, 0, 255), 3) # 红色圆轮廓
# 绘制圆心
cv2.circle(img_color, center, 2, (0, 255, 0), 3) # 绿色圆心
# 可视化
plt.figure(figsize=(10, 3))
plt.subplot(131), plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Original Image')
plt.subplot(132), plt.imshow(cv2.cvtColor(img_blur, cv2.COLOR_BGR2RGB))
plt.title('Blurred Image')
plt.subplot(133), plt.imshow(cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB))
plt.title('Detected Circles')
plt.show()