BG67: OpenCV做Hough變換

隨著深度學習的流行,Hough變換似乎已經成為了一個古老的幾何形狀檢測方法。不過對於一些嵌入式應用而言,使用這種方法仍然是一種很不錯的選擇。還記得以前做工件尺寸測量、車道線檢測或者球類目標定位的時候都是使用Hough變換完成的。

相比較與深度學習模型迴歸參數,Hough變換求解參數的方法更具解釋性。理解這種樸素的參數求解方案或許會對開發出新的參數求解算法提供基礎。

1. Hough直線變換

Hough變換的基本思想一言以蔽之就是:使用輪廓點的位置在參數空間進行投票。比如一個直線的方程可以表述為:y = mx + c,其中的m和c就是參數,其取值的可能性就構成了一個二維的參數空間。xy座標系中的任何一條直線都對應這參數空間中的一個點,所以只需確定mc參數空間中的一個點即可求解出xy空間的一條直線。由於mc參數形式的表示不便於轉換成參數空間的直線方程,以上xy座標系下的直線方程還可以轉換為−參數座標系下的方程:=cos()+sin(), 其中表示直線到原點的距離,表示原點到直線的垂線與x軸所成的角度。從−參數空間的直線方程可知,xy座標系的一個點決定了−參數座標系下的一條直線。假設xy座標系下有1000個點,那就可以在−參數座標系下作出1000條直線。這些參數空間的直線會存在相交的情況,從那些相交點中挑選出投票率比較高的點就對應這xy座標系下的直線,也就是我們需要求解的直線。

對於一張普通的RGB圖,需要求解其中直線的方程則需以下步驟:(1)將RGB圖轉換成灰度圖,(2)從灰度圖產生邊緣圖,(3)使用邊緣圖上的輪廓點在−參數空間進行投票。下面的例程就是演示從一個RGB圖中找出直線。在Hough變換獲得直線方程的參數之後,將參數帶入方程就可以獲得xy座標系下的直線方程。要在圖像中明確繪製出直線的位置,只需取x=0和x=width這兩個特殊點即可。

<code>import cv2
import matplotlib
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
# 顯示原圖
plt.figure(figsize=(9.6, 9.6))
plt.subplot(2,2,1)
img = cv2.imread('../data/bg66/opencv-icon.png')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.title('Origin')
# 顯示灰度圖
plt.subplot(2,2,2)
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
plt.imshow(gray_img, cmap='gray')
plt.title('Gray image')
# 顯示邊緣圖
plt.subplot(2,2,3)
# 計算邊緣圖
edge = cv2.Canny(gray_img, threshold1=50, threshold2=150, apertureSize=3)
plt.imshow(edge, cmap='gray')
plt.title('Edge image')
# 顯示結果直線圖
plt.subplot(2,2,4)
# hough變換檢測直線

lines = cv2.HoughLines(edge, rho=1, theta=np.pi/180, threshold=100)
# 繪製直線
paint_img = img.copy()
for row in range(lines.shape[0]):
params = lines[row][0]
rho, theta = params[0], params[1]
# rho = x\\cos(\\theta) + y\\sin(\\theta)
a, b = np.cos(theta), np.sin(theta)
x1 = 0
y1 = int(rho / b)
x2 = paint_img.shape[1]
y2 = int((rho - x2*a) / b)
# draw line
cv2.line(paint_img, (x1,y1), (x2,y2), (0,0,0), 2)
plt.imshow(cv2.cvtColor(paint_img, cv2.COLOR_BGR2RGB))
plt.title('Hough lines')
plt.savefig('../output/bg67/lines.png', dpi=300, bbox_inches='tight')/<code>

Output:

BG67: OpenCV做Hough變換

從例程可以看出邊緣檢測Canny算子和Hough直線變換函數都有很多參數需要調整。其中Canny算子的threshold1和threshold2是一組遲滯的閾值,apertureSize是Canny算子調用Sobel算子時的參數。對於HoughLines()函數中最重要的參數就是threshold,該參數為投票數量閾值,即超過該閾值的投票才是有效的參數解。

直接使用這種標準的Hough變換需要在一個二維空間內進行那麼多點的投票,其實還是很耗費時間的。如果能夠從邊緣圖中採樣一些點進行投票就能提高不少效率,但是代價就是可能存在漏檢,這種情況對應的投票數量閾值也許調小。OpenCV提供了cv2.HoughLinesP()函數完成這種概率Hough直線變換,大家可以通過手冊進一步詳細瞭解。

2. Hough圓形變換

圓形的方程可以表示為:(−0)2+(−0)2=2,其中(0,0)為圓心,為半徑。從方程可以看出圓形的Hough變換需要在一個3維的參數空間進行投票。可想而知,這種計算壓力是難以接受的,因此必須考慮使用一些手段。恰好OpenCV的cv2.HoughCircles()函數使用了一個有效的實現方法,即cv2.HOUGH_GRADIENT,Hough梯度法,使用邊緣的梯度信息來求解參數。

其中的param1和param2是求解器在調用Canny算子進行邊緣檢測時用到的參數。由於在函數內部進行邊緣即梯度的計算,因此傳入函數的圖像需要是灰度圖,這在手冊中已經明確說明。以下例程演示了使用OpenCV提供的接口完成Hough圓形變換。

<code>circles = cv2.HoughCircles(
gray_img, method=cv2.HOUGH_GRADIENT, dp=1, minDist=20,
param1=200, param2=50, minRadius=0, maxRadius=0)
# 顯示輸入圖像
plt.figure(figsize=(10.8, 5.4))
plt.subplot(1, 2, 1)
plt.imshow(gray_img, cmap='gray')
plt.title('Input grayscale image')
# 顯示結果圖
plt.subplot(1,2,2)
paint_img = img.copy()
for row in range(circles[0].shape[0]):
params = np.int32(circles[0][row])
x, y, r = params[0], params[1], params[2]
cv2.circle(paint_img, (x, y), r, (0,0,0), 3)
plt.imshow(paint_img)
plt.title('Results on origin image')
plt.savefig('../output/bg67/circles.png', dpi=300, bbox_inches='tight')/<code>

Output:

BG67: OpenCV做Hough變換

從求解結果來看,該方法能夠將準確得將圖像中的三個大圓檢測出來,但是小圓就漏掉了,這需要仔細調整相關的參數才能檢測得到。

Hough變換求解參數一般不適用於參數過多的情況,因為3個參數就已經讓算力捉襟見肘了,最好還是隻有兩個參數的情況。Hough方式求解直線或者圓形還有一個好處是不需要標註。因此在標註數據缺乏的情況下,可以先使用Hough變換程程一批粗陋的標註,然後人工篩選提供標註的質量,等數據充足時在使用深度學習進一步提升效果。


分享到:


相關文章: