在这一章当中,将学习
cv2.HoughLines()
,cv2.HoughLinesP()
如果可以用数学形式表示形状,则霍夫变换是检测任何形状的一种比较流行的技术。即使形状有些破损或变形,也可以检测出形状。本文将讲解如何将它何作用于一条线。
一条线可以表示为
或以参数形式表示为
$,其中ρ
是从原点到该线的垂直距离,而θ
是由该垂直线和水平轴形成的角度以逆时针方向测量(该方向随如何表示坐标系而变化。此表示形式在OpenCV中使用)。如下图所示:
因此,如果线在原点下方通过,则它将具有正的ρ
且角度小于180。如果线在原点上方,则将角度取为小于180,而不是大于180的角度。ρ
取负值。任何垂直线将具有0度,水平线将具有90度。
现在,看一下霍夫变换如何处理线条。任何一条线都可以用(ρ,θ)
这两个术语表示。因此,首先创建2D数组或累加器(以保存两个参数的值),并将其初始设置为0。让行表示 ρ
,列表示θ
。阵列的大小取决于所需的精度。假设希望角度的精度为1度,则需要180列。对于ρ
,最大距离可能是图像的对角线长度。因此,以一个像素精度为准,行数可以是图像的对角线长度。
考虑一个100x100的图像,中间有一条水平线。取直线的第一点。此时知道它的(x,y)
值。现在在线性方程式中,将值θ= 0,1,2,… 180
放进去,然后检查得到ρ
。对于每对( ρ, θ )
,在累加器中对应的(ρ,θ )
单元格将值增加1。假设此点是(50,90)
,则该点的值加1,其它点依此类推。
现在,对行的第二个点。执行与上述相同的操作。递增( ρ , θ )
对应的单元格中的值。这次,单元格(50,90)=2
。实际上,正在对( ρ, θ )
值进行投票。对线路上的每个点都继续执行此过程。在每个点上,单元格(50,90)
都会增加或投票,而其他单元格可能会或可能不会投票。这样一来,最后,单元格(50,90)
的投票数将最高。因此,如果在累加器中搜索最大票数,则将获得(50,90)
值,该值表示该图像中的一条线与原点的距离为50,角度为90度。
这就是霍夫变换对线条的工作方式,原理很简单。输入的图片中有两条粗直线,经过霍夫变换后的结果得到accumaltor矩阵,右图就是把accumaltor矩阵画出来,越亮值越大,越黑值越小。在右图中,有两个很明显的亮点, 这两个亮点分别代表两条不同参数的直线,与输入的图片(左图)吻合。然后读取矩阵的两个最大值就可以得出这两条线距画面中心距离以及角度。
上面说明的所有内容都封装在OpenCV函数cv2.HoughLines()
中。
lines=cv2.HoughLines(image,rho,theta,threshold) 返回的
lines
:表示的是一个具有两个或三个元素的数组,math:(rho,theta)
, 其中ρ
以像素为单位,θ
以弧度为单位 第一个参数,输入图像应该是二进制图像,因此在应用霍夫变换之前,请应用阈值或使用Canny边缘检测 第二和第三参数分别是ρ
和θ
精度 第四个参数是阈值,这意味着应该将其视为行的最低投票。请记住,票数取决于线上的点数。因此,它表示应检测到的最小线长。
import cv2
import numpy as np
img = cv2.imread('sudo.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLines(edges, 1, np.pi/180, 100) # 最后一个数是阈值控制,不同的值效果不一样
for line in lines:
rho, theta = line[0] # 最大的那一个
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
x1 = int(x0 + 1000*(-b)) # 以(x0, y0)为起点,将线段延长
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0,0,255), 2)
cv2.imshow('houglines', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
OpenCV HoughLines 中,它是定义线条的端点,以便它们在绘制线条时到达(并经过)图像的侧面。您使用的霍夫变换仅返回线与原始线的角度和距离。所以额外的计算是从原点垂直于这条线找到一条线的交点,这样它就可以识别这条线上的某个点。但它不知道这条线应该有多长。所以它沿着这条线从那个点延伸了这条线。由于它知道直线的角度和直线上的一个点,它只提供两个端点到直线上给定点的距离。如果您的图像尺寸大于约 21000 像素,那么如果您希望线条到达图像的两侧,则可能需要增加 1000 值。减号 (-b) 出现如下: 从原点到垂直于直线的方向由它的斜率给出b/a = sin(theta)/cos(theta)=tan(theta)。请参阅https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghlines/py_houghlines.html 上的图表。但是线本身的方向与该方向成 90 度,其角度由 给出-1/tan(theta) = -cos(theta)/sin(theta) = -a/b or a/-b。即它的斜率是 (a/-b)=(y-yo)/(x-xo)=delY/delX。请参阅https://byjus.com/maths/slope-of-line/。因此,要获得两个端点,您可以从 xo, yo 给定的任意点开始,然后沿线向任一方向移动,这样端点 X 分量为 xo +- 1000 delX = xo +- 1000 cos(perp_angle) = xo +- 1000(-b) 终点 Y 分量是 yo +- 1000 delY = yo +- 1000sin(perp_angle) = yo +- 1000*a。其中 perp_angle 是沿实际线的方向。
分享
检查下面的结果
在霍夫变换中,可以看到,即使对于带有两个参数的行,也需要大量计算。概率霍夫变换是霍夫变换的优化。它没有考虑所有要点。取而代之的是,它仅采用随机的点子集,足以进行直线检测。只是必须降低阈值。
OpenCV的实现基于Matas,J.和Galambos,C.和Kittler, J.V.使用渐进概率霍夫变换对行进行的稳健检测 。使用的函数是cv2.HoughLinesP()
。它有两个新的论点。
lines = cv.HoughLines( image, rho, theta, threshold[, lines[, srn[, stn[, min_theta[, max_theta]]]]] )
最好的是,它直接返回行的两个端点。在以前的情况下,仅获得线的参数,并且必须找到所有点。在这里,一切都是直接而简单的。
参见下图,比较了霍夫空间中的霍夫变换和概率霍夫变换。
实现
# p 霍夫变换
import cv2
import numpy as np
img = cv2.imread('sudo.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 100, minLineLength=100, maxLineGap=10)
for line in lines:
x1, y1, x2, y2 = line[0] # 这里直接返回的是坐标,不用变换
cv2.line(img, (x1, y1), (x2, y2), (0, 255,0), 2)
cv2.imshow('houghlinep', img)
cv2.waitKey()
cv2.destroyAllWindows()
看到如下结果:
可以看到在这张图上,cv2.HoughLines
效果比cv2.HoughLinesP
要好,但是不一定一直好,比如下面一张
# compare
# p 霍夫变换
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('building.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, 200, minLineLength=100, maxLineGap=10)
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(img, (x1, y1), (x2, y2), (0, 255,0), 2)
img1= cv2.imread('building.png')
gray1= cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
edges1 = cv2.Canny(gray1, 50, 150, apertureSize=3)
lines1 = cv2.HoughLines(edges1, 1, np.pi/180, 200)
for line in lines1:
rho, theta = line[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(img1, (x1, y1), (x2, y2), (0,0,255), 2)
plt.subplot(121)
plt.imshow(img1_rgb)
plt.title('HoughLines')
plt.xticks([])
plt.yticks([])
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img1_rgb = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
plt.subplot(122)
plt.imshow(img_rgb)
plt.title('HoughLinesP')
plt.xticks([])
plt.yticks([])
plt.show()
实际上,主要是HoughLines是一条直线而非线段,而那些小树叶又容易被误检测成图片,所以才会画的面目。
在本章中,将学习
cv2.HoughCircles()
圆在数学上表示为
,其中
是圆的中心,r
是圆的半径。从等式中,可以看到我们有3个参数,因此需要3D累加器进行霍夫变换,这是非常低效的。因此,OpenCV使用更加技巧性的方法,即使用边缘的梯度信息的Hough梯度方法。
OpenCv中实现的函数是cv2.HoughCircles()
。它有很多参数。
circles = cv.HoughCircles( image, method, dp, minDist[, circles[, param1[, param2[, minRadius[, maxRadius]]]]] )
例子:
import cv2
import numpy as np
img = cv2.imread('origin.png', 0)
img = cv2.medianBlur(img, 5)
cimg = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 10, param1=50, param2=30, minRadius=0, maxRadius=0)
# param2越小,能找到的圆更多,
circles = np.uint16(np.around(circles))
for i in circles[0,:]:
# draw the outer circle
cv2.circle(cimg, (i[0],i[1]), i[2], (0,255,0), 2)
# draw the center of the circle
cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255), 3)
cv2.imshow('circle', cimg)
cv2.waitKey(0)
cv2.destroyAllWindows()
结果如下: