霍夫变换
一、霍夫线变换
本小节主要介绍霍夫线变换的基本概念以及如何在图像中检测线条。包括以下函数:cv.HoughLines(), cv.HoughLinesP()
- 用法如下:
- cv.HoughLines( image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]] ) -> lines
- cv.HoughLinesP( image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]] ) -> lines
1. 基本概念
如果您可以用数学形式表示该形状,则霍夫变换是一种检测任何形状的流行技术。 即使形状有点破损或变形,它也可以检测到。 我们将看到它是如何在一条线上工作的。
一条线可以表示为 y=mx+c 或参数形式,如 ρ=xcosθ + ysinθ 其中 ρ 是从原点到直线的垂直距离,θ 是这条垂直线与水平轴形成的角度 逆时针(该方向因您表示坐标系的方式而异。此表示在 OpenCV 中使用)。如下所示:
因此,如果线在原点下方通过,它将有一个正 rho 和一个小于 180 的角度。如果它在原点上方,而不是采用大于 180 的角度,该角度小于 180,并且 rho 取负值。 任何垂直线将具有 0 度,水平线将具有 90 度。
现在让我们看看霍夫变换是如何作用于线条的。 任何一条线都可以用这两项来表示,(ρ,θ)。 所以首先它创建一个二维数组或累加器(保存两个参数的值),最初设置为 0。 让行表示 ρ,列表示 θ。 数组的大小取决于您需要的精度。 假设您希望角度的精度为 1 度,则需要 180 列。 对于 ρ,可能的最大距离是图像的对角线长度。 所以取一个像素精度,行数可以是图像的对角线长度。
考虑一个 100x100 的图像,中间有一条水平线。 取线的第一个点。 你知道它的 (x,y) 值。 现在在直线方程中,输入值 θ=0,1,2,....,180 并检查你得到的 ρ。 对于每个 (ρ,θ) 对,您在累加器中相应的 (ρ,θ) 单元格中将值加一。 所以现在在累加器中,单元格 (50,90) = 1 以及其他一些单元格。
现在取线上的第二个点。 执行与上述相同的操作。 增加与您获得的 (rho, theta) 对应的单元格中的值。 这一次,单元格 (50,90) = 2。您实际上所做的是对 (ρ,θ) 值进行投票。 您对线上的每个点继续此过程。 在每一点,单元格 (50,90) 将被增加或投票,而其他单元格可能会或可能不会被投票。 这样,最后,单元格 (50,90) 将获得最大票数。 因此,如果您在累加器中搜索最大票数,您会得到值 (50,90),表示该图像中有一条线距原点 50 度,角度为 90 度。 它在下面的动画中得到了很好的展示(图片提供:Amos Storkey)
这就是霍夫变换对线条起作用的方式。 这很简单,也许你可以自己使用 Numpy 来实现它。 下图显示了蓄能器。 某些位置的亮点表示它们是图像中可能线条的参数。 (图片提供:维基百科)
2. OpenCV中的霍夫变换
上面解释的所有内容都封装在 OpenCV 函数 cv.HoughLines() 中。 它只是返回一个 :math:(rho, theta) 值的数组。 ρ 以像素为单位,θ 以弧度为单位。 第一个参数,输入图像应该是二值图像,所以在应用霍夫变换之前应用阈值或使用canny边缘检测。 第二和第三个参数分别是 ρ 和 θ 精度。 第四个参数是阈值,这意味着它应该被视为一条线的最低权数。 请记住,权数取决于线上的点数。 所以它代表了应该被检测到的线的最小长度。
import cv2 as cv
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def cv_show(name, img):
cv.imshow(name, img)
cv.waitKey(0)
cv.destroyAllWindows()
def compare(imgs):
# for i in range(len(imgs)):
# imgs[i][:,-3:-1,:] = [255,255,255]
res = np.hstack(imgs)
cv_show('Compare', res)
# 读入图像
img = cv.imread('sudo.jpg')
# 转化为灰度图便于之后的阈值处理或边缘检测
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
# Canny边缘检测
edges = cv.Canny(gray,50,150,apertureSize = 3)
# Houghlines的参数分别为 二值图、 ρ 精度、 θ 精度、权数阈值(权数越大,越能表明这条线属于直线)
# 返回值为 lines 直线集合,每条直线的首个元素是对应的 ρ 和 θ
lines = cv.HoughLines(edges,1,np.pi/180,200)
res = img.copy()
for line in lines:
rho,theta = line[0]
# 利用参数方程找出对应的直线上一点
# x = ρcosθ
# y = ρsinθ
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))
cv.line(res,(x1,y1),(x2,y2),(0,0,255),2)
compare([img, res])
cv_show('Edges', edges)
3. 概率霍夫变换
在霍夫变换中,您可以看到即使对于具有两个参数的行,也需要大量计算。 概率霍夫变换是我们看到的霍夫变换的优化。 它没有考虑所有要点。 相反,它只需要一个足以进行线检测的随机点子集。 我们只需要降低门槛。 请参见下图,其中比较了霍夫空间中的霍夫变换和概率霍夫变换。 (图片提供:Franck Bettinger 的主页)
OpenCV 的实现基于 Matas, J. 和 Galambos, C. 和 Kittler, J.V. [167] 使用渐进概率霍夫变换对线条进行鲁棒检测。 使用的函数是 cv.HoughLinesP()。 它有两个新参数。
- minLineLength - 线的最小长度。 比这短的线段被抑制。
- maxLineGap - 线段之间的最大允许间隙,若未超过将它们视为一条线。
最好的是,它直接返回线的两个端点。 在以前的情况下,你只得到线的参数,你必须找到所有的点。 在这里,一切都是直接而简单的。之前的简单霍夫变换需要自己解出直线上的两个点
img = cv.imread('sudo.jpg')
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
edges = cv.Canny(gray,50,150,apertureSize = 3)
# 倒数的两个参数分别为:最小线段长度(像素单位), 线段最大距离(像素单位)
lines = cv.HoughLinesP(edges,1,np.pi/180,100,minLineLength=100,maxLineGap=10)
res = img.copy()
for line in lines:
x1,y1,x2,y2 = line[0]
cv.line(res,(x1,y1),(x2,y2),(0,255,0),2)
compare([img, res])
二、霍夫圆变换
本小节介绍如何使用霍夫变换找出图像中的圆形,主要用到下列函数:cv.HoughCircles()
- 用法如下: cv.HoughCircles( image, method, dp, minDist[, circles[, param1[, param2[, minRadius[, maxRadius]]]]] ) -> circles
圆在数学上表示为 ,其中 是圆的中心,r 是圆的半径。 从方程中,我们可以看到我们有 3 个参数,所以我们需要一个 3D 累加器来进行霍夫变换,这将是非常低效的。 所以OpenCV使用了更复杂的方法,Hough Gradient Method,它使用边缘的梯度信息。
# img = cv.imread('xy.png')
# gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
# gray = cv.medianBlur(img,5)
# circles = cv.HoughCircles(gray,cv.HOUGH_GRADIENT,1,20,param1=50,param2=30,minRadius=0,maxRadius=0)
# circles = np.uint16(np.around(circles))
# res = img.copy()
# for i in circles[0,:]:
# # draw the outer circle
# cv.circle(res,(i[0],i[1]),i[2],(0,255,0),2)
# # draw the center of the circle
# cv.circle(res,(i[0],i[1]),2,(0,0,255),3)
# compare([img, res])
img = cv.imread('pie.png',0)
img = cv.medianBlur(img,5)
cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)
circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20,
param1=100,param2=60,minRadius=0,maxRadius=0)
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# draw the outer circle
cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
cv.imshow('detected circles',cimg)
cv.waitKey(0)
cv.destroyAllWindows()
三、补充资源
img = cv.imread('twitter.png',0)
img = cv.resize(img, (0,0), fx = 0.3, fy = 0.3)
img = cv.medianBlur(img,5)
cimg = cv.cvtColor(img,cv.COLOR_GRAY2BGR)
circles = cv.HoughCircles(img,cv.HOUGH_GRADIENT,1,20,
param1=50,param2=35,minRadius=0,maxRadius=0)
circles = np.uint16(np.around(circles))
img = cimg.copy()
for i in circles[0,:]:
# draw the outer circle
cv.circle(cimg,(i[0],i[1]),i[2],(0,255,0),2)
# draw the center of the circle
cv.circle(cimg,(i[0],i[1]),2,(0,0,255),3)
compare([img ,cimg])