使用Opencv进行霍夫变化
在这篇博文中,我们将借助一种叫做霍夫变换的技术来检查图像中的直线和圆形。
什么是霍夫变换
霍夫变换是一种特征提取方法,用于检测图像中的简单图形,如圆形,直线等。简单的图形意味着可以使用几个参数来进行表示,例如,一条直线可以使用两个参数(斜率和截距)来表示,而一个圆可以使用三个参数(圆心和半径)来表示。可以这么说,霍夫变换在寻找图像中的图形方面比较优秀。使用霍夫变换的主要优点是,它对遮挡不敏感。
使用霍夫变换检测直线
图1: 为一条在极坐标下的直线。
极坐标下的直线方程
从高中数学课上我们知道,在极坐标下,直线的方程如下所示:
其中,
ρ
\rho
ρ为直线到原点到距离(以像素为单位),
θ
\theta
θ是直线与
Y
Y
Y轴的夹角(以弧度为单位)。
为什么我们不使用传统的斜率+截距的直线方程呢?原因是,传统直线方程的斜率可以从-∞取到+∞,但是对于霍夫变换来说,参数是需要存在边界的。而对于极坐标的直线方程,
θ
\theta
θ是有限的,
ρ
\rho
ρ看起来是无限的,但是由于图像是有限的,因此它也是有限的。
累加器
想象一个二维数组,其中x轴包含了所有
θ
\theta
θ的值,y轴包含了所有
ρ
\rho
ρ的值。这个二维数组的每一个格子都代表了一条直线。
图2:累加器。
图2的数组被称为累加器,因为我们将使用这个数组的格子来收集图像中存在的直线。图中的左上角对应
(
−
R
,
0
)
(-R,0)
(−R,0),右下角对应
(
R
,
π
)
(R,\pi)
(R,π),这个累加器数组会从图像中提取信息,并增加对应当格子的值。
第一步,初始化累加器
首先,我们需要创建一个累加器数组,而累加器数组的单元格数量是一个设计决策,即需要人为的考虑其大小。假如你的累加器是一个10x10的数组,那就意味着,你最多能检测到100条不同的直线。所以,图像的分辨率也是一个能够决定你设计累加器到底有多大的因素。
第二步,检测边缘
现在我们已经初始化好了累加器,接下来,就是要手机图像中的直线的信息(证据)。
我们怎么获取证据呢?具体想法是,如果图像中有一个可见的线,那么可以使用边缘检测器去检测这条线,并获得这条线所占有的像素数组
[
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
.
.
.
(
x
n
,
y
n
)
]
[(x_1,y_1),(x_2,y_2)...(x_n,y_n)]
[(x1,y1),(x2,y2)...(xn,yn)]。
第三步,按边缘像素投票
对于上诉像素数组中的每个边缘像素
(
x
,
y
)
(x,y)
(x,y),我们将
θ
\theta
θ值从0递增到
π
\pi
π,并将其带入到直线极坐标方程中,得到
ρ
\rho
ρ。下图给出了三个像素在不同
θ
\theta
θ值下的得到的不同
ρ
\rho
ρ值的曲线图(图中的r标错了,应该是
ρ
\rho
ρ)。
可以看到,这三条曲线相交与一点,即
θ
=
1
,
ρ
=
9.5
\theta=1,\rho=9.5
θ=1,ρ=9.5。通常每条边缘会有很多个像素,累加器用于找到由边缘像素生成的所有曲线的交点。例如,你构建的累加器为20x20,那么就相当于将
0
到
π
0到\pi
0到π分为20份,即有20种不同的
θ
\theta
θ。因此,对于每个边缘的像素
(
x
,
y
)
(x,y)
(x,y),我们都能够计算得到20个
(
ρ
,
θ
)
(\rho,\theta)
(ρ,θ)。然后再将累加器种对应位置的格子进行递增,较多值的格子即可视为交点。从上述表达可以知道,可以设定一个阈值,用来过滤某些交点较小的数据,即较弱的直线。
使用Opencv完成检测
没错,不用我们瞎操心啦,Opencv提供来两个函数来用于霍夫变换。分辨是HoughLines和HoughLinesP,他们的参数如下:
- edges:边缘检测器的输出。
- lines:一个向量,用来表示一条直线的开始点和结束点。
- rho:分辨率参数 ρ \rho ρ,单位为像素。
- theta:分辨率参数 θ \theta θ,单位是弧度。
- threshold:检测一条线的最小交点数。
import cv2
import numpy as np
import sys
def onTrackbarChange(max_slider):
global img
global dst
global gray
dst = np.copy(img)
th1 = max_slider
th2 = th1 * 0.4
# 边缘检测
edges = cv2.Canny(img, th1, th2)
# 使用概率霍夫变换
# 边缘图,线段以像素为单位的距离精度,线段以弧度为单位的角度精度
# 累加器的阈值,线段以像素为单位的最小长度,一方向上两条线段判定为一条线段的最大允许间隔
lines = cv2.HoughLinesP(edges, 2, np.pi/180.0, 50, minLineLength=10, maxLineGap=100)
# 把直线绘制到图中
for line in lines:
x1, y1, x2, y2 = line[0]
cv2.line(dst, (x1, y1), (x2, y2), (0,0,255), 1)
cv2.imshow("Result Image", dst)
cv2.imshow("Edges",edges)
if __name__ == "__main__":
# 读取图像
img = cv2.imread('lanes.jpg')
# 创建图像的深拷贝(防止数据污染)
dst = np.copy(img)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 创建显示窗口
cv2.namedWindow("Edges")
cv2.namedWindow("Result Image")
# 初始化的阈值
initThresh = 500
# 可选的最大阈值
maxThresh = 1000
# 滑杆取值控件
# onTrackbarChange为滑杆数据变化时的响应函数
cv2.createTrackbar("threshold", "Result Image", initThresh, maxThresh, onTrackbarChange)
# 主动调用
onTrackbarChange(initThresh)
while True:
key = cv2.waitKey(1)
if key == 27:
break
cv2.destroyAllWindows()
检测效果
使用Opencv中的霍夫变换检测圆形
由于一个圆形需要使用三个参数(圆心和半径)来表示,因此,我们需要构建一个三维的累加器。圆形公式定义如下:
检测圆形的步骤如下:
- 使用边缘检测器获取到图像中的边缘信息。
- 为了检测图像中的圆,为半径设置了最大值和最小值。
- 在三维累加器中,对不同中心和半径的圆进行累加计算。
Opencv中给出HoughCircles函数来检测图像中的圆,它的参数如下:
- image: 输入的图像。
- method: 检测的方法
- dp: 累加器分辨率与图像分辨率的反比。
- mindst: 所检测的圆的中心之间的最小距离。
- param_1和param_2: 这两个是特定方法的参数。
- min_Radius: 要检测的圆的最小半径。
- max_Radius: 要检测的圆的最大半径。
import cv2
import numpy as np
import sys
def onTrackbarChange(max_slider):
cimg = np.copy(img)
p1 = max_slider
p2 = max_slider * 0.4
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, cimg.shape[0]/64, param1=p1, param2=p2, minRadius=25, maxRadius=50)
# 判断是否有圆
if circles is not None:
# 获取圆的个数
cir_len = circles.shape[1]
# 取整
circles = np.uint16(np.around(circles))
for i in circles[0, :]:
# 绘制圆的外轮廓
cv2.circle(cimg, (i[0], i[1]), i[2], (0, 255, 0), 2)
# 绘制圆心小点
cv2.circle(cimg, (i[0], i[1]), 2, (0, 0, 255), 3)
else:
cir_len = 0
cv2.imshow('Image', cimg)
edges = cv2.Canny(gray, p1, p2)
cv2.imshow('Edges', edges)
if __name__ == "__main__":
# 读取图像
img = cv2.imread('brown-eyes.jpg', 1)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 创建显示窗口
cv2.namedWindow("Edges")
cv2.namedWindow("Image")
# 初始化的阈值和最大阈值
initThresh = 105
maxThresh = 200
# 滑杆取值控件
# onTrackbarChange为滑杆数据变化时的响应函数
cv2.createTrackbar("Threshold", "Image", initThresh, maxThresh, onTrackbarChange)
onTrackbarChange(initThresh)
while True:
key = cv2.waitKey(1)
if key == 27:
break
cv2.destroyAllWindows()