詳解 Hough 變換(基本原理與直線檢測)

Gaowaly發表於2024-07-28

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為縱軸,θ 為橫軸),所表示的東西。是什麼呢?是一條正弦曲線。如下圖 (b) 所示

也就是說,在原空間裡的任意一點 ( 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試試,即變成
yb=(xa)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 cv2

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

def 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)

    # 亮度模板,起初為全黑,當經過某點,亮度 +1
    template = 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]] + 1
    return template

if __name__ == "__main__":
    # 讀取圖片
    imgPath = "C:\\Users\\zhangwei156\\Desktop\\figure\\lena.bmp"
    grayImg = cv2.imread(imgPath, 0)
    
    # 提取邊緣
    edgeImg = cv2.Canny(grayImg, 300, 500)
    
    # 設定離散的精度
    angsDiv = 500
    dDiv = 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 變換原理與應用

相關文章