Python下opencv使用笔记(十一)(详解hough变换检测直线与圆)

在数字图像中,往往存在着一些特殊形状的几何图形,像检测马路边一条直线,检测人眼的圆形等等,有时我们需要把这些特定图形检测出来,hough变换就是这样一种检测的工具。

Hough变换的原理是将特定图形上的点变换到一组参数空间上,根据参数空间点的累计结果找到一个极大值对应的解,那么这个解就对应着要寻找的几何形状的参数(比如说直线,那么就会得到直线的斜率k与常熟b,圆就会得到圆心与半径等等)。

关于hough变换,核心以及难点就是关于就是有原始空间到参数空间的变换上。以直线检测为例,假设有一条直线L,原点到该直线的垂直距离为p,垂线与x轴夹角为θ,那么这条直线是唯一的,且直线的方程为 ρ=xcosθ+ysinθ, 如下图所示:

可以看到的是这条直线在极坐标系下只有一个(ρ,θ)与之对应,随便改变其中一个参数的大小,变换到空间域上的这个直线将会改变。好了,再回来看看这个空间域上的这条直线上的所有点吧,你会发现,这条直线上的所有点都可以是在极坐标为(ρ,θ)所表示的直线上的,为什么说是都可以在,因为其中随便的一个点也可以在其他的(ρ,θ)所表示的直线上,就比如上述的(x,y)吧,它可以再很多直线上,准确的说,在经过这个点的直线上,随便画两条如下:

可以看到,光是空间上的一个点在极坐标系下就可能在很多极坐标对所对应的直线上,具体有多少个极坐标对呢?那得看你的θ的步长了,我们可以看到θ无非是从0-360度(0?2π)变化,假设我们没10度一走取一个直线(这个点在这个直线上),那么我们走一圈是不是取了36条直线,也就对应36个极坐标对没错吧,那么这个极坐标对,画在坐标轴上是什么样子的呢?因为θ是从0?2π,并且一个点定了,如果一个θ也定了,你想想它对应的直线的ρ会怎么样,自然也是唯一的。那么这个点在极坐标下对应的(ρ,θ)画出来一个周期可能就是这样的,以θ为x轴的话:

ok前面说的是单单这一个点对应的极坐标系下的参数对,那么如果每个点都这么找一圈呢?也就是每个点在参数空间上都对应一系列参数对吧,现在把它们华仔同一个坐标系下会怎么样呢?为了方便,假设在这个直线上取3个点画一下:

那么可以看到,首先对于每一个点,在极坐标下,会存在一个周期的曲线来表示通过这个点,其次,这三个极坐标曲线同时经过一个点,要搞清楚的是,极坐标上每一个点对(ρ,θ)在空间坐标上都是对应一条直线的。好了,同时经过的这一个点有什么含义呢?它表示在空间坐标系下,有一条直线可以经过点1,经过点2,经过点3,这是什么意思?说明这三个点在一条直线上吧。反过来再来看这个极坐标系下的曲线,那么我们只需要找到交点最多的点,把它返回到空间域就是这个需要找的直线了。为什么是找相交最多的点,因为上面这只是三个点的曲线,当空间上很多点都画出来的时候,那么相交的点可能就不知上述看到的一个点了,可能有多个曲线相交点,但是有一点,势必是一条直线上的所有点汇成的交点是曲线相交次数最多的。

再来分析这个算法。可以看到hough变换就是参数映射变换。对每一个点都进行映射,并且每一个映射还不止一次,(ρ,θ)都是存在步长的,像一个点映射成一个(ρ,θ),以θ取步长为例,当θ取得步长大的时候,映射的(ρ,θ)对少些,反之则多,但是我们有看到,映射后的点对是需要求交点的,上述画出来的曲线是连续的,然而实际上因为θ步长的存在,他不可能是连续的,像上上个图一样,是离散的。那么当θ步长取得比较大的时候,你还想有很多交点是不可能的,因为这个时候是离散的曲线然后再去相交,所以说θ步长不能太大,理论上是越小效果越好,因为越小,越接近于连续曲线,也就越容易相交,但是越小带来的问题就是需要非常多的内存,计算机不会有那么多内存给你的,并且越小,计算量越大,想想一个点就需要映射那么多次,每次映射是需要计算的,耗时的。那么再想想对于一副图像所有点都进行映射,随便假设一副100*100的图像(很小吧),就有10000个点,对每个点假设就映射36组(ρ,θ)参数(此时角度的步长是10度了,10度,已经非常大的一个概念了),那么总共需要映射360000次,在考虑每次映射计算的时间吧。可想而知,hough是多么耗时耗力。所以必须对其进行改进。首先就是对图像进行改进,100*100的图像,10000个点,是不是每个点都要计算?大可不必,我们只需要在开始把图像进行一个轮廓提取,一般使用canny算子就可以,生成黑白二值图像,白的是轮廓,那么在映射的时候,只需要把轮廓上的点进行参数空间变换,为什么提轮廓?想想无论检测图像中存在的直线呀圆呀,它们都是轮廓鲜明的。那么需要变换的点可能就从10000个点降到可能1000个点了,这也就是为什么看到许多hough变换提取形状时为什么要把图像提取轮廓,变成二值图像了。

继续算法,分析这么多,可想而知那么一个hough变换在算法设计上就可以如下步骤:

(1)将参数空间(ρ,θ)量化,赋初值一个二维矩阵M,M(ρ,θ)就是一个累加器了。

(2)然后对图像边界上的每一个点进行变换,变换到属于哪一组(ρ,θ),就把该组(ρ,θ)对应的累加器数加1,这里的需要变换的点就是上面说的经过边缘提取以后的图像了。

(3)当所有点处理完成后,就来分析得到的M(ρ,θ),设置一个阈值T,认为当M(ρ,θ)>T,就认为存在一条有意义的直线存在。而对应的M(ρ,θ)就是这组直线的参数,至于T是多少,自己去式,试的比较合适为止。

(4)有了M(ρ,θ)和点(x,y)计算出来这个直线就ok了。

说了这么多,这就是原理上hough变换的最底层原理,事实上完全可以自己写程序去实现这些,然而,也说过,hough变换是一个耗时耗力的算法,自己写循环实现通常很慢,曾经用matlab写过这个,也有实际的hough变换例子可以看看:

虹膜识别(三):Hough变换检测内圆边缘

那么我们在实际中大可不必自己写,opencv已经集成了hough变换的函数,调用它的函数效率高,也很简单。

Opencv中检测直线的函数有cv2.HoughLines(),cv2.HoughLinesP()

函数cv2.HoughLines()返回值有三个(opencv 3.0),实际是个二维矩阵,表述的就是上述的(ρ,θ),其中ρ的单位是像素长度(也就是直线到图像原点(0,0)点的距离),而θ的单位是弧度。这个函数有四个输入,第一个是二值图像,上述的canny变换后的图像,二三参数分别是ρ和θ的精确度,可以理解为步长。第四个参数为阈值T,认为当累加器中的值高于T是才认为是一条直线。自己画了个图实验一下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread(‘line.jpg‘)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#灰度图像
#open to see how to use: cv2.Canny
#http://blog.csdn.net/on2way/article/details/46851451
edges = cv2.Canny(gray,50,200)
plt.subplot(121),plt.imshow(edges,‘gray‘)
plt.xticks([]),plt.yticks([])
#hough transform
lines = cv2.HoughLines(edges,1,np.pi/180,160)
lines1 = lines[:,0,:]#提取为为二维
for rho,theta in lines1[:]:
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(img,(x1,y1),(x2,y2),(255,0,0),1)

plt.subplot(122),plt.imshow(img,)
plt.xticks([]),plt.yticks([])

测试一个新的图,不停的改变 cv2.HoughLines最后一个阈值参数到合理的时候如下:

可以看到检测的还可以的。

函数cv2.HoughLinesP()是一种概率直线检测,我们知道,原理上讲hough变换是一个耗时耗力的算法,尤其是每一个点计算,即使经过了canny转换了有的时候点的个数依然是庞大的,这个时候我们采取一种概率挑选机制,不是所有的点都计算,而是随机的选取一些个点来计算,相当于降采样了。这样的话我们的阈值设置上也要降低一些。在参数输入输出上,输入不过多了两个参数:minLineLengh(线的最短长度,比这个短的都被忽略)和MaxLineCap(两条直线之间的最大间隔,小于此值,认为是一条直线)。输出上也变了,不再是直线参数的,这个函数输出的直接就是直线点的坐标位置,这样可以省去一系列for循环中的由参数空间到图像的实际坐标点的转换。

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread(‘room.jpg‘)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#灰度图像
#open to see how to use: cv2.Canny
#http://blog.csdn.net/on2way/article/details/46851451
edges = cv2.Canny(gray,50,200)
plt.subplot(121),plt.imshow(edges,‘gray‘)
plt.xticks([]),plt.yticks([])
#hough transform
lines = cv2.HoughLinesP(edges,1,np.pi/180,30,minLineLength=60,maxLineGap=10)
lines1 = lines[:,0,:]#提取为二维
for x1,y1,x2,y2 in lines1[:]:
    cv2.line(img,(x1,y1),(x2,y2),(255,0,0),1)

plt.subplot(122),plt.imshow(img,)
plt.xticks([]),plt.yticks([])

可以看到这个方法似乎更好些,速度还快,调参数得到较好的效果就ok了。

Ok说完了直线的检测,再来说说圆环的检测,我们知道圆的数学表示为:

(x?xcenter)2+(y?ycenter)2=r2

从而一个圆的确定需要三个参数,那么就需要三层循环来实现(比直线的多一层),从容把图像上的所有点映射到三维参数空间上。其他的就一样了,寻找参数空间累加器的最大(或者大于某一阈值)的值。那么理论上圆的检测将比直线更耗时,然而opencv对其进行了优化,用了一种霍夫梯度法,感兴趣去研究吧,我们只要知道它能优化算法的效率就可以了。关于函数参数输入输出:

cv2.HoughCircles(image, method, dp, minDist, circles, param1, param2, minRadius, maxRadius)

这个时候输入为灰度图像,同时最好规定检测的圆的最大最小半径,不能盲目的检测,否侧浪费时间空间。输出就是三个参数空间矩阵。

来个实际点的人眼图像,把minRadius和maxRadius调到大圆范围检测如下:

import cv2
import numpy as np
import matplotlib.pyplot as plt

img = cv2.imread(‘eye.jpg‘)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)#灰度图像 

plt.subplot(121),plt.imshow(gray,‘gray‘)
plt.xticks([]),plt.yticks([])
#hough transform
circles1 = cv2.HoughCircles(gray,cv2.HOUGH_GRADIENT,1,
100,param1=100,param2=30,minRadius=200,maxRadius=300)
circles = circles1[0,:,:]#提取为二维
circles = np.uint16(np.around(circles))#四舍五入,取整
for i in circles[:]:
    cv2.circle(img,(i[0],i[1]),i[2],(255,0,0),5)#画圆
    cv2.circle(img,(i[0],i[1]),2,(255,0,255),10)#画圆心

plt.subplot(122),plt.imshow(img)
plt.xticks([]),plt.yticks([])

把半径范围调小点,检测内圆:

至此圆的检测就是这样。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-06 00:16:29

Python下opencv使用笔记(十一)(详解hough变换检测直线与圆)的相关文章

Opencv图像识别从零到精通(22)-----hough变换检测直线与圆

今天要看的是霍夫变换,常用用来检测直线和圆,这里是把常见的笛卡尔坐标系转换成极坐标下,进行累计峰值的极大值,确定.HoughLines,HoughLinesP,HoughCircles,三个函数,首先先看看原理,最后会用漂亮的matlab图,来回归一下,霍夫直线变换. 霍夫线变换: 众所周知, 一条直线在图像二维空间可由两个变量表示. 例如: 在 笛卡尔坐标系: 可由参数:  斜率和截距表示. 在 极坐标系: 可由参数:  极径和极角表示 对于霍夫变换, 我们将用 极坐标系 来表示直线. 因此,

Python下opencv使用笔记(一)(图像简单读取、显示与储存)

写在之前 从去年開始关注python这个软件,途中间间断断看与学过一些关于python的东西.感觉python确实是一个简单优美.easy上手的脚本编程语言,众多的第三方库使得python异常的强大.能够处理很多不同的问题,同一时候它的很多开源免费的库使得python的使用也是十分的广泛. 在科学计算.数据处理与图像领域,自己以前一直在使用matlab.感觉matlab也是一个语言优美.简单方便的编程语言,都说matlab与python在某些领域是非常类似的,确实是这样,就科学计算.数据处理上真

Python下opencv使用笔记(十)(图像频域滤波与傅里叶变换)

前面曾经介绍过空间域滤波,空间域滤波就是用各种模板直接与图像进行卷积运算,实现对图像的处理,这种方法直接对图像空间操作,操作简单,所以也是空间域滤波. 频域滤波说到底最终可能是和空间域滤波实现相同的功能,比如实现图像的轮廓提取,在空间域滤波中我们使用一个拉普拉斯模板就可以提取,而在频域内,我们使用一个高通滤波模板(因为轮廓在频域内属于高频信号),可以实现轮廓的提取,后面也会把拉普拉斯模板频域化,会发现拉普拉斯其实在频域来讲就是一个高通滤波器. 既然是频域滤波就涉及到把图像首先变到频域内,那么把图

Python下opencv使用笔记(十二)(k均值算法之图像分割)

k均值(kmeans)聚类是一种最为简单的聚类方法,直接根据数据点之间的距离(欧氏距离,几何距离等等)来划分数据是属于哪一类的,当所有数据点所属的类别不在变化的时候,聚类也就完成了.详细原理可索引下面一个博客: 聚类分析笔记-K均值matlab算法(一) 关于kmeans再谈几点认识: 重要的一点:聚类数目的问题.有的聚类.分类问题已经限制好了要聚类成几类,也就是聚类数目一定,那么这种聚类通常简单些,直接规定聚类数就好了.而有的聚类问题不知道分成几类才好,这个时候怎么办?那么就需要找到一种评价指

Python下opencv使用笔记(四)(图像的阈值处理)

图像的阈值处理一般使得图像的像素值更单一.图像更简单.阈值可以分为全局性质的阈值,也可以分为局部性质的阈值,可以是单阈值的也可以是多阈值的.当然阈值越多是越复杂的.下面将介绍opencv下的三种阈值方法. (一)简单阈值 简单阈值当然是最简单,选取一个全局阈值,然后就把整幅图像分成了非黑即白的二值图像了.函数为cv2.threshold() 这个函数有四个参数,第一个原图像,第二个进行分类的阈值,第三个是高于(低于)阈值时赋予的新值,第四个是一个方法选择参数,常用的有: ? cv2.THRESH

Python下opencv使用笔记(五)(图像的平滑与滤波)

对于图形的平滑与滤波,但从滤波角度来讲,一般主要的目的都是为了实现对图像噪声的消除,增强图像的效果. 首先介绍二维卷积运算,图像的滤波可以看成是滤波模板与原始图像对应部分的的卷积运算.关于卷积运算,找到几篇相关的博客: 图像处理:基础(模板.卷积运算) 图像处理-模板.卷积的整理 对于2D图像可以进行低通或者高通滤波操作,低通滤波(LPF)有利于去噪,模糊图像,高通滤波(HPF)有利于找到图像边界. (一)统一的2D滤波器cv2.filter2D Opencv提供的一个通用的2D滤波函数为cv2

Python下opencv使用笔记(七)(图像梯度与边缘检测)

梯度简单来说就是求导,在图像上表现出来的就是提取图像的边缘(不管是横向的.纵向的.斜方向的等等),所需要的无非也是一个核模板,模板的不同结果也不同.所以可以看到,所有的这些个算子函数,归结到底都可以用函数cv2.filter2D()来表示,不同的方法给予不同的核模板,然后演化为不同的算子而已.并且这只是这类滤波函数的一个用途,曾经写过一个关于matlab下滤波函数imfilter()的扩展应用(等同于opencv的cv2.filter2D函数): 图像滤波函数imfilter函数的应用及其扩展

Python下opencv使用笔记(三)(图像的几何变换)

写在之前 二维与三维图像的几何变换在计算机图形学上有重要的应用,包括现在的许多图像界面的切换.二维与三维游戏画面控制等等都涉及到图像几何变换,就比如说在三维游戏中,控制角色三维移动的时候,画面是要跟着移动的,那么怎么移动,怎么让上一时刻的画面移动到这一时刻,这都是根据了你的移动量,然后找到三维坐标之间的对应关系,用这一时刻的坐标替换到上一时刻的坐标像素值实现图像的切换. 图像的几何变换主要包括:平移.扩大与缩小.旋转.仿射.透视等等.图像变换是建立在矩阵运算基础上的,通过矩阵运算可以很快的找到对

Python下opencv使用笔记(二)(简单几何图像绘制)

简单几何图像一般包括点.直线.矩阵.圆.椭圆.多边形等等.首先认识一下opencv对像素点的定义.图像的一个像素点有1或者3个值,对灰度图像有一个灰度值,对彩色图像有3个值组成一个像素值,他们表现出不同的颜色. 那么有了点才能组成各种多边形. (一)首先绘制直线 函数为:cv2.line(img,Point pt1,Point pt2,color,thickness=1,line_type=8 shift=0) 有值的代表有默认值,不用给也行.可以看到这个函数主要接受参数为两个点的坐标,线的颜色