- 本文是个人快速入门OpenCV-Python的电子笔记,由于水平有限,难免出现错漏,敬请批评改正。
- 更多精彩内容,可点击进入OpenCV-Python快速入门专栏或我的个人主页查看
- 熟悉Python
- Python 3.x (面向对象的高级语言)
- OpenCV 4.0(python第三方库)
pip3 install opencv-python
- 以笛卡儿坐标系来引出霍夫变换基本原理。
- 笛卡儿空间中的一条直线在霍夫空间内的映射情况。
- 从上图可以看出,
- 笛卡儿空间内的一条直线确定了霍夫空间内的一个点。
- 霍夫空间内的一个点确定了笛卡儿空间内的一条直线。
- 笛卡儿空间中的一个点在霍夫空间内的映射情况。
- 从上图可以看出,
- 笛卡儿空间内的点 ( 0 , 0 ) (_0, _0) (x0,y0)映射到霍夫空间,就是直线 = − 0 + 0 = −_0 + _0 b=−x0k+y0。
- 霍夫空间内的直线 = − 0 + 0 = −_0 + _0 b=−x0k+y0映射到笛卡儿空间,就是点 ( 0 , 0 ) (_0, _0) (x0,y0)。
- 笛卡儿空间中的多个点映射到霍夫空间的情况。
- 从上面图片可以看出,在霍夫空间内,经过一个点的直线越多,说明其在笛卡儿空间内映射的直线,是由越多的点所构成(穿过)的。
- 两个点就能构成一条直线。但是,如果有一个点是因为计算错误而产生的,那么它和另外一个点,也会构成一条直线,此时就会凭空构造出一条实际上并不存在的直线。
- 这种情况是要极力避免的。因此,在计算中,我们希望用更多的点构造一条直线,以提高直线的可靠性。也就是说,如果一条直线是由越多点所构成的,那么它实际存在的可能性就越大,它的可靠性也就越高。因此,霍夫变换选择直线的基本思路是:选择有尽可能多直线交汇的点。
- 但是,笛卡儿空间有一定的局限性,在笛卡儿空间中,如下图所示情况,则无法映射到霍夫空间中,
- 此时,斜率 k k k 为无穷大,截距 b b b 无法取值。因此,上图中的直线无法映射到霍夫空间内。
- 为了解决上述问题,可以考虑将笛卡儿坐标系映射到极坐标系上,如下图所示。
- 在笛卡儿坐标系内使用的是斜率 k k k 和截距 b b b,即用 ( k , b ) (k, b) (k,b)表示一条直线。在极坐标系内,采用极径 r r r(有时也用 ρ \rho ρ表示)和极角 θ θ θ来表示,即 ( r , θ ) (r, θ) (r,θ)来表示。
- 极坐标系中的直线可表示为: x = r c o s θ , y = r s i n θ , r 2 = x 2 + y 2 x=rcosθ,y=rsinθ,r^2=x^2+y^2 x=rcosθ,y=rsinθ,r2=x2+y2 即 r = x c o s θ + y s i n θ r = xcos\theta + ysin\theta r=xcosθ+ysinθ
- 上图的直线 A A A,可以使用极坐标的极径 r r r 和极角 θ θ θ来表示。其中, r r r 是直线 A A A 与图像原点 O O O 之间的距离,参数 θ θ θ是直线 A A A 的直线 B B B 与 x x x 轴的角度。
- 在这种表示方法中,图像中的直线有一个 ( 0 , π ) (0,π) (0,π)的角 θ θ θ,而 r r r 的最大值是图像对角线的长度。用这种表示方法,可以很方便地表示三个点所构成的直线。
- 与笛卡儿空间和霍夫空间的映射关系类似:
- 极坐标系内的一个点映射为霍夫坐标系(霍夫空间)内的一条线(曲线)。
- 极坐标系内的一条线映射为霍夫坐标系内的一个点。
- 一般来说,在极坐标系内的一条直线能够通过在霍夫坐标系内相交于一点的线的数量来评估。在霍夫坐标系内,经过一个点的线越多,说明其映射在极坐标系内的直线,是由越多的点所构成(穿过)的。因此,霍夫变换选择直线的基本思路是:选择由尽可能多条线汇成的点。通常情况下,设置一个阈值,当霍夫坐标系内交于某点的曲线达到了阈值,就认为在对应的极坐标系内存在(检测到)一条直线。
- 上述内容是霍夫变换的原理,即使完全不理解上述原理,也不影响我们使用 OpenCV 提供的霍夫变换函数来进行霍夫变换。OpenCV 给我们提供了接口(参数、返回值),我们只需要掌握接口的正确使用方法,就可以正确地处理图像问题,无须掌握其内部工作原理。
- OpenCV 提供了函数 cv2.HoughLines()用来实现霍夫直线变换,该函数要求所操作的源图像是一个二值图像,所以在进行霍夫变换之前要先将源图像进行二值化,或者进行 Canny 边缘检测。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图片
img = cv2.imread('line.jpg')
# 灰度图
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 边缘检测
edges = cv2.Canny(gray,50,150,apertureSize = 3)
# BGR -> RGB
img_RGB=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
img_Show=img_RGB.copy()
# 霍夫直线变化
'''
lines=cv2.HoughLines(image, rho, theta, threshold)
参数:
image 是输入图像,即源图像,必须是 8 位的单通道二值图像。
如果是其他类型的图像,在进行霍夫变换之前,需要将其修改为指定格式。
rho 为以像素为单位的距离 r 的精度。一般情况下,使用的精度是 1。
theta 为角度的精度。一般情况下,使用的精度是π/180,表示要搜索所有可能的角度。
threshold 是阈值。该值越小,判定出的直线就越多。
识别直线时,要判定有多少个点位于该直线上。在判定直线是否存在时,对直线所穿过的点的数量进行评估,
如果直线所穿过的点的数量小于阈值,则认为这些点恰好(偶然)在算法上构成直线,但是在源图像中该直线并不存在;
如果大于阈值,则认为直线存在。所以,如果阈值较小,就会得到较多的直线;阈值较大,就会得到较少的直线。
返回值:
lines 中的每个元素都是一对浮点数,表示检测到的直线的参数,即(r, θ),是numpy.ndarray 类型。
需要注意的是,使用函数 cv2.HoughLines()检测到的是图像中的直线而不是线段,因此检测到的直线是没有端点的。
所以,我们在进行霍夫直线变换时所绘制的直线都是穿过整幅图像的。
'''
lines = cv2.HoughLines(edges,1,np.pi/180,140)
# 绘制检测到的直线
for line in lines:
# (r, θ)
rho,theta = line[0]
# 通过(r, θ)获取到(x0,y0)
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
# 获得(x1,y1),(x2,y2)
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
# 绘制直线
cv2.line(img_RGB,(x1,y1),(x2,y2),(255,0,0),2)
# 显示图片
plt.figure(figsize=(20, 20))
plt.subplot(121)
plt.title("Origin")
plt.imshow(img_Show)
plt.axis('off')
plt.subplot(122)
plt.title("Result")
plt.imshow(img_RGB)
plt.axis('off')
plt.show()
- 概率霍夫变换对基本霍夫变换算法进行了一些修正,是霍夫变换算法的优化。它没有考虑所有的点。相反,它只需要一个足以进行线检测的随机点子集即可。
- 为了更好地判断直线(线段),概率霍夫变换算法还对选取直线的方法作了两点改进:
- 所接受直线的最小长度。如果有超过阈值个数的像素点构成了一条直线,但是这条直线很短,那么就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原图中并不存在这条直线。
- 接受直线时允许的最大像素点间距。如果有超过阈值个数的像素点构成了一条直线,但是这组像素点之间的距离都很远,就不会接受该直线作为判断结果,而认为这条直线仅仅是图像中的若干个像素点恰好随机构成了一种算法上的直线关系而已,实际上原始图像中并不存在这条直线。
- OpenCV 提供了函数 cv2.HoughLinesP()用来实现概率霍夫变换。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图片
img = cv2.imread('line.jpg')
# 灰度图
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 边缘检测
edges = cv2.Canny(gray,50,150,apertureSize = 3)
# BGR -> RGB
img_RGB=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
img_Show=img_RGB.copy()
# 概率霍夫变换
'''
lines =cv2.HoughLinesP(image, rho, theta, threshold, minLineLength, maxLineGap)
参数:
image 是输入图像,即源图像,必须为 8 位的单通道二值图像。
对于其他类型的图像,在进行霍夫变换之前,需要将其修改为这个指定的格式。
rho 为以像素为单位的距离 r 的精度。一般情况下,使用的精度是 1。
theta 是角度的精度。一般情况下,使用的精度是 np.pi/180,表示要搜索可能的角度。
threshold 是阈值。该值越小,判定出的直线越多;值越大,判定出的直线就越少。
minLineLength 用来控制“接受直线的最小长度”的值,默认值为 0。
maxLineGap 用来控制接受共线线段之间的最小间隔,即在一条线中两点的最大间隔。
如果两点间的间隔超过了参数 maxLineGap 的值,就认为这两点不在一条线上。默认值为 0。
返回值:
lines 是由 numpy.ndarray 类型的元素构成的,其中每个元素都是一对浮点数,表示检测到的直线的参数,即(r, θ)。
'''
lines = cv2.HoughLinesP(edges,1,np.pi/180,1,minLineLength=100,maxLineGap=10)
for line in lines:
x1,y1,x2,y2 = line[0]
cv2.line(img_RGB,(x1,y1),(x2,y2),(255,0,0),2)
# 显示图片
plt.figure(figsize=(20, 20))
plt.subplot(121)
plt.title("Origin")
plt.imshow(img_Show)
plt.axis('off')
plt.subplot(122)
plt.title("Result")
plt.imshow(img_RGB)
plt.axis('off')
plt.show()
- 霍夫变换除了用来检测直线外,也能用来检测其他几何对象。实际上,只要是能够用一个参数方程表示的对象,都适合用霍夫变换来检测。
- 用霍夫圆变换来检测图像中的圆,与使用霍夫直线变换检测直线的原理类似。在霍夫圆变换中,需要考虑圆半径和圆心(x 坐标、y 坐标)共 3 个参数。在 OpenCV 中,采用的策略是两轮筛选。第 1 轮筛选找出可能存在圆的位置(圆心);第 2 轮再根据第 1 轮的结果筛选出半径大小。
- 与用来决定是否接受直线的两个参数“接受直线的最小长度(minLineLength)”和“接受直线时允许的最大像素点间距(MaxLineGap)”类似,霍夫圆变换也有几个用于决定是否接受圆的参数:圆心间的最小距离、圆的最小半径、圆的最大半径。
- OpenCV 中,函数 cv2.HoughCircles()用来实现霍夫圆变换,该函数将 Canny 边缘检测和霍夫变换结合。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图片
img = cv2.imread('circle.jpg')
# 灰度图
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# BGR -> RGB
img_RGB=cv2.cvtColor(imgo,cv2.COLOR_BGR2RGB)
img_Show=img_RGB.copy()
# 图像平滑:中值滤波 cv2.medianBlur(img, ksize)
img = cv2.medianBlur(img,5)
# 霍夫圆变换
'''
circles=cv2.HoughCircles(image, method, dp, minDist, param1, param2, minRadius, maxRadius)
参数:
image:输入图像,即源图像,类型为 8 位的单通道灰度图像。
method:检测方法。截止到 OpenCV 4.0.0-pre 版本,HOUGH_GRADIENT 是唯一可用的参数值。
该参数代表的是霍夫圆检测中两轮检测所使用的方法。
dp:累计器分辨率,它是一个分割比率,用来指定图像分辨率与圆心累加器分辨率的比例。
例如,如果 dp=1,则输入图像和累加器具有相同的分辨率。
minDist:圆心间的最小间距。该值被作为阈值使用,如果存在圆心间距离小于该值的多个圆,则仅有一个会被检测出来。
因此,如果该值太小,则会有多个临近的圆被检测出来;如果该值太大,则可能会在检测时漏掉一些圆。
param1:该参数是缺省的,在缺省时默认值为 100。它对应的是 Canny 边缘检测器的高阈值(低阈值是高阈值的二分之一)。
param2:圆心位置必须收到的投票数。只有在第 1 轮筛选过程中,投票数超过该值的圆,才有资格进入第 2 轮的筛选。
因此,该值越大,检测到的圆越少;该值越小,检测到的圆越多。
这个参数是缺省的,在缺省时具有默认值 100。
minRadius:圆半径的最小值,小于该值的圆不会被检测出来。
该参数是缺省的,在缺省时具有默认值 0,此时该参数不起作用。
maxRadius:圆半径的最大值,大于该值的圆不会被检测出来。
该参数是缺省的,在缺省时具有默认值 0,此时该参数不起作用。
返回值:
circles:由圆心坐标和半径构成的 numpy.ndarray。
需要特别注意,在调用函数 cv2.HoughLinesCircles()之前,要对源图像进行平滑操作,
以减少图像中的噪声,避免发生误判。该函数具有非常多的参数,在实际检测中可以根据需要设置不同的值。
'''
circles = cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,300,param1=50,param2=30,minRadius=700,maxRadius=900)
# 将数值转化为整数
circles = np.uint16(np.around(circles))
# 绘制检测到的圆
for i in circles[0,:]:
# 绘制圆 (i[0],i[1])为圆心坐标,i[2]为圆的半径
cv2.circle(img_RGB,(i[0],i[1]),i[2],(255,0,0),12)
# 绘制圆心
cv2.circle(img_RGB,(i[0],i[1]),2,(255,0,0),12)
# 显示图片
plt.figure(figsize=(20, 20))
plt.subplot(121)
plt.title("Origin")
plt.imshow(img_Show)
plt.subplot(122)
plt.title("Result")
plt.imshow(img_RGB)
plt.show()
[1] https://opencv.org/
[2] 李立宗. OpenCV轻松入门:面向Python. 北京: 电子工业出版社,2019
- 更多精彩内容,可点击进入OpenCV-Python快速入门专栏或我的个人主页查看