Hough 变换原理与应用
前言: 详细介绍了 Hough 变换的基本思想、基本原理和应用等。其中大多都是自己的理解,难免有偏差,仅供参考。
文章目录
- Hough 变换原理与应用
-
- 1. 基本概述
-
- 1.1 一些基本问题
- 1.2 以例子说明
-
- 1.2.1 例子1:直线 y = k x + b y = kx + by=kx+b 到参数空间的变换(k,b为定值,如k=2,b=4)
- 1.2.2 例子2:极坐标下的直线到参数空间的变换
- 1.2.3 例子3:圆到参数空间的变换
- 2. Hough 直线检测
-
- 2.1 问题
- 2.2 思路
- 2.3 总结
- 2.4 提升
-
- 2.4.1 实际问题
- 2.4.2 求解思路
- 2.4.3 自编程实现
- 2.5 openCV对应代码解读
1. 基本概述
1.1 一些基本问题
Hough 变换简单概括就是原空间到参数空间的变换。
以一条直线为例,常用直线方程为 y = kx + by = kx+by=kx+b ,这个是原空间。在这个直线方程里,x 和 y 是变量,k和b为直线方程参数。注意参数不是固定的,数量也不是固定的,比如这个直线不一定非要用 k,b (斜率和截距)这两个参数,下面会有举例说明。
何为到参数空间的变换呢?
直接了当地说就是,把参数当作变量(即坐标轴)。上面直线例子而言,就是把 k,b 当作横纵坐标构建的空间。注意,这一变换并非坐标系的变换(如:不是笛卡尔坐标向极坐标的变换;两者本质就不一样),Hough变换是始终以笛卡尔坐标系为基础,只是换了坐标轴(新空间中,坐标轴换成了原空间表达式中的参数,也是因此新空间叫参数空间。)
为何要变换到参数空间呢?
原空间中一些事物具有很强的关联性,但在原空间不好观察和把握,因此考虑从别的视角来表述它们关联性。参数空间就是一种,还有类似的傅里叶变换等等。所以,可以说,我们变换此空间,是为了更直观地揪出原坐标系下一系列点间的共同特点(或者说联系)。至于为什么选择参数空间,别问,问就是第一个想出这点的数学家的智慧和创新。并且,”很巧”的是,它在处理一些问题上确实很管用。
既然说参数选择不是固定的,如何选择最佳参数来构成参数空间?
这个不用操心,针对一些特定任务和问题,前人已经给出了答案了。
1.2 以例子说明
下面以三个例子说明上述自己理解得出的重点。
1.2.1 例子1:直线 y = k x + b y = kx + by=kx+b 到参数空间的变换(k,b为定值,如k=2,b=4)
事实上,使用这样例子并不好,容易给人带来误解,但又不得不从该例子说起。之所以说容易带来误解,原因在于:
我们遇到的问题,往往都是考察原坐标系下所给出的一系列点的关系问题。即基于点,去将这些点变换到参数空间;这个例子的表述,恰恰与此过程相反,该例子表述是已知结果(这些点是在一条直线上),去说明参数变换这回事。
两个是因果倒置的问题。这么说有些难以理解,下面例子会说明上面两句话要表达的含义。
我们考察直线到其参数空间的变换,这里参数就选择 k,b,变换如下图(a)(b)所示。
我们这样表述:左侧原空间下,对于直线 y = k x + b ,将 k , b当作坐标轴的话,得到右侧参数空间,该直线就对应于右侧空间一点(k,b) 。这样表述有些别扭,因为常规上,我们都是把问题落脚在点上,毕竟计算机存储和处理的都是离散点数据。
因此修改说: 左侧原空间下,直线 y = k x + b 上的点 对应 右侧参数空间中一点( k , b ) ,如图©到(b)所示。 但这样说的话,同样别扭,因为这句话是错的。
Hough 变换不是常规的映射,不是点对点的对应关系,不是函数式的 f(x)。这点也可以说明它跟坐标系间变换有本质区别。
左侧原空间上的任意一点如 ( x 0 , y 0 ) ,对应到右侧,不会是一个点,事实上是一条线。因为右侧表示的是直线的斜率和截距,而经过点 ( x 0 , y 0 ) 的直线有无数条,也就对应着无数的 k 和 b。那是否意味着左侧这 ( x 0 , y 0 )点,对应到右侧,就是充满坐标轴的所有点?当然不是,因为 k,b 虽然是随机的,取值可以 [ − ∞ , ∞ ] [-\infty, \infty][−∞,∞],但它俩不是各自随机,两者是有关系的。因为我们前面前提是经过 ( x 0 , y 0 ) (x_0,y_0)(x0,y0) 点的直线,即
y = k ( x − x 0 ) + y 0
说明:用这个 y = kx + b 这个式子不好,因为后面例子都是把 x、y 放到一侧,两者式子下,过点限制条件是由差别的。使用 y − k x = b 或y − k x − b = 0 理解更好,这样的话,限制条件就是
y − k x = y 0 − k x 0 或 y − k x − b = y 0 − k x 0 − b
也即
y − k x = ( y 0 − k x )
这里会衍生出一个问题,即好像一个参数 k 就可以确定这一直线了。到这节最后会说到这个问题。先忽略之,或自己加以理解。
依据上式,也就是以 k, y 0 − k x 0 为参数,即 k 当横轴,y 0 − k x 0当纵轴,或者写为我们更常见的
b = − k x 0 + y 0
其中,b为纵轴,k为横轴。整个过程如下图(a)(b)所示
反过来,参数空间一点,就对应于原空间里 斜率为 k,截距为 b 的直线。
总结一下,这两个空间的关系可以这样表述:左侧原空间的直线对应右侧参数空间一点;左侧原空间一点对应右侧空间一条直线。这是比较有意思的相互关系,圆形变换其实也有这个特点。参数即变量,变量即参数 的感觉。
基于此,我们就把刚才别扭的表述,说得准确些:对于左侧直线,我们用斜率 k 和截距 b 就可以描述它,并且唯一确定它,因此若建立一个斜率、截距空间,点(k,b)就可以表征一条原空间下斜率为 k 截距为 b 的直线。
那么是不是参数只能选择 k,b 呢?显然不是,比如你就可以选择 参数空间里横坐标为 k+b, 纵坐标为 b,同等效果。但一般,参数选择要为计算、理解方便服务。
假设,我们发挥了自己的想象,采取了如下两个参数,即圆心到直线的距离 d 和 这个垂线的角度 θ \thetaθ,如下图所示。首先,我们明确的是,这两个参数是否可以唯一确定地表示这一直线?答案当时是肯定的,那么这个参数就能用。
那么,问题就来了,如果以 d , θ为参数空间横纵坐标,那应该是怎样的。我们下面来考察一下:
首先,我们先把这条直线用 d 和 θ 表示出来,这个是高中数学题了,就不推导了(求解思路就是用 d 和θ 表征线上任意一点)。直接给出答案,这条线可以这么表示(这里没有常规地把 y 放在左侧,为了美观)
d = x c o s θ + y s i n θ
同样的方法,对于无数 d dd 和 θ \thetaθ ,我们要满足原空间里经过( x 0 , y 0 ),即
x 0 cos θ + y 0 sin θ = x cos θ + y sin θ
也即有
d = x 0 cos θ + y 0 sin θ
这里体现了 d 是有取值范围的。
根据三角函数的和差角公式,可以写成
也就是说,在原空间里的任意一点 ( x 0 , y 0 ) ,如果变换到上述 d , θ 参数空间里,就是一条正弦曲线。同样是点对应线的关系。
上面是直接给出了使用 d 和 θ 参数,一个是截距一个是角度,显得很突兀。但实际上,使用 d 和θ 参数,应用十分广泛,本质上,它是参考极坐标的表达,另一方面,其应用广泛的原因在于 d 和 θ 都是有范围的,后面图像直线检测实例中会讲到。
上述只是直接给出了结果,是为了避免一些人将它与直角坐标系到极坐标系变换相混淆。下面给出推导:
使用极坐标系表示直角坐标里的点,有个对应关系,即
变换,即
两等式累加,即
也就是上面我们有提到的用 d 和 θ 表示的直线公式。注意,这里的 ρ 如果要是用于变换到参数空间,表示的不是直角坐标系里的一点到原点距离,而是 过该点且垂线倾角为 θ 的直线距原点的距离,θ 同理。 ρ , θ 要是用于点变换到极坐标系下,就是我们常规表示,而且此时是点对点关系。再次强调两者不同点。
总结以上:原空间一点变换到参数空间的结果会随参数选择而变化,参数选择往往是基于计算方便、基于自己目的。
1.2.2 例子2:极坐标下的直线到参数空间的变换
问题来了,极坐标下直线一般方程是什么?同样是高中问题。
直角坐标下
使用常规推导,即y = ρ sin θ , x = ρ cos θ 带入
看着别扭,把 k 和 b 换下,即变成形如下式的形式
其中,m,n 为定值。也即 m,n 知道后就可以确定极坐标系下唯一直线,因此可以用 m,n 构建该极坐标系下直线的参数空间,即对应于一点(m,n)。只是它的物理意义在坐标系(极坐标系)里不那么看得出来。m,n 的物理意义是,该直线(在极坐标系下)映射到直角坐标系下的斜率和截距。贴个极坐标系下直线图
上式也可以利用三角函数的和差公式变换下,即得
也即
换下参数,即
其中,α , b 为常数,同样,α , b 知道后就可以确定极坐标系下唯一直线,同样可以用它构建参数空间。
三个小问题:
-
上面以α , b 为参数,变换到参数空间后,形状是什么?好奇的自行探究吧。
-
这里的α , b可能不能随便取值,换句话说,并不是随意两个α , b \alpha,bα,b 都可以表示一条直线。因为从公式演化来看,两者都是受 m,n 控制,自由度受控,可能有些 α , b 是无效的,但可以确定的是,极坐标内任意直线都可以找到一组 α , b 与之唯一对应。感觉是如此,不再去印证。
-
是否可以考虑 参数 α , b 用极坐标系表示?即用极坐标系表示参数空间。我想应该是可以的。这是个有意思的问题。但常规上,都是在笛卡尔坐标系下。同样不再去深究。
1.2.3 例子3:圆到参数空间的变换
有了上面的经验,我们考察一下圆。前面已经说过,表达形式不同,可选参数也是多样的。这里只给出常用的参数选择。
圆的一般方程(只考虑笛卡尔坐标系下):
即圆心O坐标为 ( a , b ) ,半径为 r 。
选择 a , b , r为参数,注意,这里验证了前面提到的 对于一些问题参数数量也是不一样。
按照前面对直线考察相同的步骤,参数空间里 一点 (a,b,r) 就可以表示原空间里这一圆。不同之处在于,这里上升到了三维坐标。
同样考察对于原空间圆上一点 ( x 0 , y 0 ) ,与直线相同的思路,经过该点有无数的圆,不同圆心O、半径r就对应着不同的过该点的圆。但同样(a,b,r)不是随意的,而是满足过( x 0 , y 0 )这一基本条件,即
这个也就是选择 a,b,r 作为坐标轴的参数空间下方程。写得顺眼点就是
更顺眼点,即
这个方程不就是圆锥方程吗。
参考直线时的表述,我们就可以说:过原空间一点的所有圆 对应于 参数空间里的一个圆锥。
如下图(a)(b)所示
【参考Fig.2自己想象吧,用python画太费劲了,懒。想象:左图(a)是一个点,然后有无数过该点的圆(用虚线表示);右图(b)是一个圆锥。当然也可以有志之士帮忙画一下】
事实上,不如我们把两者综合起来说,即
对于原空间坐标中的一点,如果我们考察对象是 过该点的直线,那么对应于参数空间里,就是一条直线;如果我们考察对象是 过该点的圆,那么对应于参数空间里,就是一个圆锥
两个小问题:
- 维度相较于考察对象为直线时增加了一个,是不是很神奇?自行见解。
- 一个点对应了参数空间的一个圆锥,后面应用时(Hough圆检测),计算量会非常非常大。因此一般不会直接应用,会有改进或别的思路。下面圆形检测部分会详解。
一个有意思的东西:
换个思路表示圆形,即
x = a + rcosθ
y = b + rsinθ
上面消去了θ ,会变成了圆一般方程(x-a)^2+(x-b)^2 = r^2, 这里我们消去r试试,即变成
y−b=(x−a)tanθ
如果以 a , b , θ 为参数会怎样呢?一个更有意思的东西:
对于原空间中一点,考察经过该点的所有圆,事实上使用 (a,b) 就可以唯一表示一个圆了,即圆心为(a,b),且经过该点,就已经确定唯一圆了,即只考虑参数 a,b 不就够了吗?
换个更简单的,对于原空间一点,考察经过该点的所有直线,事实上使用斜率 k 就可以唯一表示一条线了,即斜率为k,且经过该点,截距自然而然就确定了。那只考虑参数 k 不就够了吗?为什么参数空间里还要使用两个参数 k,b 来确定经过该点唯一直线。
其实这个问题有些诡辩的意味。稍微思考一下就可以反驳,如果仅用 k 表达,也即参数空间是个一维坐标轴,反推一下,参数空间里一个 k 能表达一条唯一的线吗?显然不能。加入b的原因,就可以简单理解为 是为了把经过该点的信息囊括进去。
更简单直白点就是,参考例子 1 ,实际上 b 并不是单独参数,它只是 k的函数,即f(k),如例子1中的纵轴其实是 y 0 − k x 0。其目的就是为了表达它有个限制条件,即要经过( x 0 , y 0 ) 。
圆形Hough下,同样的原因。
至此,或许对 Hough 变换有了那么一点理解,或者说,有那么点印象了。
下面就直入主题,Hough变换的直线检测和圆检测,以及改进后的其它检测。
2. Hough 直线检测
将前面的结论再说一遍:
对于原空间里的一点,过该点有无数的直线,每条直线唯一对应着参数空间中的一点,而所有直线就对应参数空间里无数的点,这些无数点连起来就是一条直线o r oror一条正弦线o r . . . or...or...(取决于你选择的参数)。
先从解决一个简单问题入手。
2.1 问题
如何基于上述 Hough 变换的特点,检测三个点,即 a ( 1 , 2 ) , b ( 3 , 4 ) , c ( − 1 , 0 )是否在一条直线上?
2.2 思路
根据上面 Hough 直线变换的特点,这里使用 k,b 参数进行分析说明。
先考察 a(-1, 0)点,经过该点有无数的线,映射到 k,b 参数空间是一条直线。而 k,b 空间上该线一点也就对应于原空间这无数线中的其中一条。如下图(b)中红点,就对应于原空间红线。
下面给出参数空间曲线求解过程(可参考 例子1 过程):
经过 a(1,2) 点的所有直线可表示为:
y = k ( x − 1 ) + 2
即
y = k x − k + 2
则参数空间中,横坐标为 k,表示原空间的直线斜率;纵坐标为 -k+2 ,表示原空间的直线截距,记为b。则参数空间中,线方程就是
y = − x + 2
即 Fig.4 中图(b)所示。该线上每一点,对应左侧过点 a(1,2) 一条线。如红色所示
同理方法,对于 b,c 点做一样的参数空间变换,最后结果如 Fig.5 所示
右侧参数空间里,有个特殊点 (1,1),参数空间里三个线都经过这点。也即有
- 对于 a ,该特殊点含义是,原空间中,有个 斜率为 k=1,截距 b=1 的直线经过它。
- 对于 b ,该特殊点含义是,原空间中,有个 斜率为 k=1,截距 b=1 的直线经过它。
- 对于 c ,该特殊点含义是,原空间中,有个 斜率为 k=1,截距 b=1 的直线经过它。
换句话说,原空间里有一条直线经过了这三点。即 Fig.5 (a) 中的红色线。
至此,我们达到了目的。即三点在一条线上。且该线的斜率为1,截距为1。也即图fig.5(b)中参数空间的红色交点。
2.3 总结
所以对于直线检测,我们只需对每个点进行参数空间的直线变换,然后查看参数空间的交点情况。
如果没有交点,就是不共线。如果有 N 个直线交于一点,对应原空间里就是 N 个点共线。我们可以把 N 称作叠加度,或者说 参数空间中该点的亮度(名字乱起的)。亮度越高,共线的点越多,在原图中能直接观察到直线的可能性越大。
有意思的一点是,原空间两个点对应到参数空间会怎样?毕竟原空间中两点必共线,那参数空间是否必相交?参数空间里仅有两条线,且平行,对应原空间的两点是什么情况?
2.4 提升
2.4.1 实际问题
有了以上思路,就可以用 Hough 直线检测来解决实际问题了。
Hough 直线检测一般用于二值图中的直线检测,通常是一张图经过边缘算子后,检测边缘二值图是否存在直线。
如,利用 Hough 直线检测,检测 Lena 图中存在的直线。
2.4.2 求解思路
如果没有参考其他已有代码,而是基于上述一些知识,自己实现编程可能会遇到一些问题(比如我就是)。
即,如果我用 k,b 当参数空间,我可以很容易得到经过一点( x 0 , y 0 ) (x_0,y_0)(x0,y0),变换到参数空间里的直线为
但问题是,k 可以取无穷大,b 也是,毕竟是条直线。我怎么编程表示出这条直线,因为后续还要计算各点对应线的交点,叠加度。直线无法表示出,怎么求交点?
有一种思路是解方程,考虑所有点变换到参数空间后的线方程,即
问题转化为,得到一组组解(k,b),该解满足的方程数即为交点叠加度(或亮度)。具体解法,先将方程转化为参数矩阵,利用矩阵,然后慢慢折腾吧。(这是遇到问题后自己想到的,可行与否,未知,且不探究。感觉可行,实在不济,两两方程求解,把解带入其他方程验证。但计算复杂度可能会特别特别高。)
第二种思路就是使用之前提到的有取值范围的 d , θ 参数。 即 原点到直线距离 d 和 直线的法向量倾角 θ 。好处是,它俩都是有范围的。其中
这似乎就可以用编程来实现了。
编程实现如下:
def getLine(x0,y0,angs_resolution= 100):"""x0: 原空间下点横坐标y0: 原空间下点纵坐标angs_resolution: \theta 划分精度功能:原空间(x0,y0)点,变换到 d, \theta 参数空间的曲线。说明:因为计算机存储是离散值,所以只是 \theta 取到一些值下的直线。当然,\theta 取值越多,越精细。"""angs = np.linspace(0, 2*np.pi, angs_resolution) # 定义\theta 取到的离散值d = x0 * np.cos(angs) + y0 * np.sin(angs)return angs,d
测试一下,点 (x0, y0) 设为 (1, 1),运行程序,可以得到如下结果
-
原空间中一点,对应到 θ , d 参数空间是一条正弦线。
-
求原空间所有点对应的正弦线,计算亮度
事实上,
getLine()
输出的是一系列离散点,点横坐标是 θ ,纵坐标是 d。我们查找所有输出值中 ( θ , d ) 重复次数就行了。重复次数,就是亮度。而且还有一点方便的是,对于每组getLine()
输出值,θ \thetaθ 都是相同的,因此我们只需检查每组对应位 d 是否相同(或小于某一阈值)即可。
2.4.3 自编程实现
完整代码:
import numpy as np
import matplotlib.pyplot as plt
import cv2def getLine(x0,y0,angs_resolution= 100):"""x0: 原空间下点横坐标y0: 原空间下点纵坐标angs_resolution: \theta 划分精度功能:原空间(x0,y0)点,变换到 d, \theta 参数空间的曲线。说明:因为计算机存储是离散值,所以只是 \theta 取到一些值下的直线。当然,\theta 取值越多,越精细。"""angs = np.linspace(0, 2*np.pi, angs_resolution) # 定义\theta 取到的离散值d = x0 * np.cos(angs) + y0 * np.sin(angs)return angs,ddef Hough(edgeImg, angsDiv = 500, dDiv=1000):# 获取图像尺寸ySize, xSize = edgeImg.shape# 得到二值图中所有点坐标(x,y)y, x = np.where(edgeImg != 0)# 大致确定 d 范围dMax = np.sqrt(np.max(y)**2+np.max(x)**2)# 分辨率d_res = 2*dMax/(dDiv-1)ang_res = 2*np.pi/(angsDiv-1)# 亮度模板,起初为全黑,当经过某点,亮度 +1template = np.zeros((dDiv, angsDiv), dtype = np.uint8)# 亮度叠加计算for xx in range(len(x)):_, _d = getLine(x[xx], y[xx], angsDiv)_n = ((_d+dMax)/d_res).astype(np.int64)angle = np.arange(0, angsDiv)for p in zip(angle, _n):template[p[1], p[0]] = template[p[1], p[0]] + 1return templateif __name__ == "__main__":# 读取图片imgPath = "C:\\Users\\zhangwei156\\Desktop\\figure\\lena.bmp"grayImg = cv2.imread(imgPath, 0)# 提取边缘edgeImg = cv2.Canny(grayImg, 300, 500)# 设置离散的精度angsDiv = 500dDiv = 1000# 霍夫变换forceImg = Hough(edgeImg, angsDiv, dDiv)plt.imshow(edgeImg)plt.show()plt.imshow(forceImg)plt.show()
结果如下
另!验证代码也贴上吧:
# 一些后面要用到的常数
y, x = np.where(edgeImg != 0)
dMax = np.sqrt(np.max(y)**2+np.max(x)**2)
d_res = 2*dMax/(dDiv-1)
ang_res = 2*np.pi/(angsDiv-1)# 这是只考察了最大点,即亮度最大的点
ind = np.where(forceImg == np.max(forceImg))# theta 和 d 的真实值
theta = (ind[1])* ang_res
d = -dMax + (ind[0]) * d_res# 对应原空间的直线
xx = np.arange(512)
i = 0
yy = (d[i] - xx*np.cos(theta[i]))/np.sin(theta[i])plt.plot(xx, yy, "r", linewidth = 0.3)
plt.imshow(edgeImg)
plt.show()
结果:
看起来,好像是那么回事! 换个图片试试:
Nice!
说明:
2.5 openCV对应代码解读
先省略之,有空再读。基本原理清楚了,应该很好理解源码。
参考文献:
霍夫圆检测另起一篇:详解 Hough 变换(下)圆形检测
以及Hough 直线检测的拓展:Radon 变换原理与应用