勉強しないとな~blog

ちゃんと勉強せねば…な電気設計エンジニアです。

OpenCVやってみる-11. ハフ変換

今回はハフ変換をやってみます。

ハフ変換による直線検出 — OpenCV-Python Tutorials 1 documentation

ハフ変換

ハフ変換は2値画像を直線のパラメータ(\rho, \theta)空間に持っていくような変換、 ということでいいのかな。
(x,y)空間中の直線は、以下の式で表すことができます。

\rho = x\cos\theta + y\sin\theta

変換後の(\rho, \theta)空間の各点には、元画像の中でその直線に乗った点の数が入ります。
これを使って、閾値以上の値を得たパラメータの直線が検出された直線、とすることができます。


実践

春のパン祭りの台紙外枠を検出してみたいと思います。
3種類用意しています。

import cv2
img1 = cv2.imread('harupan_200317_1.jpg')
img2 = cv2.imread('harupan_210402_1.jpg')
img3 = cv2.imread('harupan_210402_2.jpg')
img1 = cv2.resize(img1, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)
img2 = cv2.resize(img2, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)
img3 = cv2.resize(img3, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)

import numpy as np
img123 = np.hstack((img1,img2,img3))
cv2.imshow('harupan', img123)

f:id:nokixa:20210628224059p:plain

まずは輪郭検出して2値画像にします。
Canny法を使いますが、閾値は前にやったように100, 200とします。

OpenCVやってみる-8. エッジ検出 - 勉強しないとな~blog

img1_gray = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY)
img2_gray = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY)
img3_gray = cv2.cvtColor(img3, cv2.COLOR_BGR2GRAY)
img1_canny = cv2.Canny(img1_gray, 100, 200)
img2_canny = cv2.Canny(img2_gray, 100, 200)
img3_canny = cv2.Canny(img3_gray, 100, 200)
img123_canny = np.hstack((img1_canny, img2_canny, img3_canny))
cv2.imshow('Canny', img123_canny)

f:id:nokixa:20210629233056p:plain

これらに対してハフ変換を実施します。

直線とみなされるのに必要な最低限の投票数を意味するしきい値

というパラメータがありますが、今回は台紙が画像の縦横の半分ぐらい以上には写っているように見えるので、 横サイズの1/3ぐらい=100にしておこうと思います。

>>> img1.shape
(403, 302, 3)

ハフ変換実施。\rhoの刻みは1pixel、\thetaの刻みは1°です。

lines1 = cv2.HoughLines(img1_canny, 1, np.pi/180, 100)
lines2 = cv2.HoughLines(img2_canny, 1, np.pi/180, 100)
lines3 = cv2.HoughLines(img3_canny, 1, np.pi/180, 100)

結果は、float32型、3次元のndarrayになっていて、各次元の意味は

     lines[直線インデックス, ?, \rho (0) or \theta (1)]

となっています。 (2つ目の次元の意味は分かりません…)

>>> type(lines1)
<class 'numpy.ndarray'>
>>> lines1.shape
(1155, 1, 2)
>>> lines2.shape
(60, 1, 2)
>>> lines3.shape
(15, 1, 2)
>>> lines1[0]
array([[387.       ,   1.5882496]], dtype=float32)

検出した直線を元の画像に描画します。

imgs = [img1.copy(), img2.copy(), img3.copy()]
lines = [lines1, lines2, lines3]
for i in range(3):
    for line in lines[i]:
        rho = line[0,0]
        theta = line[0,1]
        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))
        imgs[i] = cv2.line(imgs[i], (x1,y1), (x2,y2), (0,0,255), 2)

imgs123 = np.hstack((imgs[0], imgs[1], imgs[2]))
cv2.imshow('Images', imgs123)

f:id:nokixa:20210629233627p:plain

これはあんまりですねー。
1つ目の画像で大量の線が出てしまっていますが、エッジ点が多過ぎるからか?
斜めに見たときに、通過する点の数が閾値(100)を超えてしまったものかと。
3つ目の画像では台紙の直線が検出されていません。ちょっと曲がっているから?

cv2.HoughLines()の3つ目の引数は角度の刻み(rad)ですが、これを粗くしたらどうだろう。
"直線に乗っている"の基準も緩めにならないか?


lines1 = cv2.HoughLines(img1_canny, 1, np.pi/90, 100)
lines2 = cv2.HoughLines(img2_canny, 1, np.pi/90, 100)
lines3 = cv2.HoughLines(img3_canny, 1, np.pi/90, 100)
lines = [lines1, lines2, lines3]
imgs = [img1.copy(), img2.copy(), img3.copy()]
for i in range(3):
    for line in lines[i]:
        rho = line[0,0]
        theta = line[0,1]
        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))
        imgs[i] = cv2.line(imgs[i], (x1,y1), (x2,y2), (0,0,255), 2)

imgs123 = np.hstack((imgs[0], imgs[1], imgs[2]))
cv2.imshow('Images', imgs123)

f:id:nokixa:20210630083010p:plain

そうはなりませんでした。
単純に検出される直線が減っています。


確率的ハフ変換

もう1つのハフ変換の関数cv2.HoughLinesP()(確率的ハフ変換)では 最小の直線長さ、最大の直線ギャップといったパラメータがあるので、うまくやってくれそうな気がします。
1枚目の画像は特にバラバラの点の集まりが直線と認識されてしまっている感じなので、 ギャップの制約で識別できるかも。

最小の直線長さ : 50
最大の直線ギャップ : 5

ぐらいでやってみようと思います。
※以下でcv2.HoughLinesP()関数を使ったとき、minLineLengthmaxLineGapのパラメータは指定しないと思ったようになりませんでした。実は順番が違うとか?他のパラメータが途中に入っているとか?

linesP1 = cv2.HoughLinesP(img1_canny, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
linesP2 = cv2.HoughLinesP(img2_canny, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
linesP3 = cv2.HoughLinesP(img3_canny, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
lines = [linesP1, linesP2, linesP3]
imgs = [img1.copy(), img2.copy(), img3.copy()]

for i in range(3):
    for line in lines[i]:
        imgs[i] = cv2.line(imgs[i], (line[0,0],line[0,1]), (line[0,2],line[0,3]), (0,0,255), 2)
imgs123 = np.hstack((imgs[0], imgs[1], imgs[2]))
cv2.imshow('Images', imgs123)

f:id:nokixa:20210701233405p:plain

うーん、
欲しくない直線は結構消えましたが、欲しい外枠直線があまり検出されていない…

一応、cv2.HoughLinesP()の結果のサイズも確認しておきます。

>>> linesP1.shape
(58, 1, 4)
>>> linesP2.shape
(21, 1, 4)
>>> linesP3.shape
(7, 1, 4)

思うに、"直線に乗っている"の判定がシビアなのでは?
エッジ画像を見るとかなり細い線になっているので、"乗っている"判定になりにくいのでは。

もしかしてモルフォロジー変換が使えないかな??


モルフォロジー変換併用

エッジ画像を膨張処理で太くしてみます。

kernel = np.ones((3,3), np.uint8)
img1_canny_dlt = cv2.dilate(img1_canny, kernel, iterations=1)
img2_canny_dlt = cv2.dilate(img2_canny, kernel, iterations=1)
img3_canny_dlt = cv2.dilate(img3_canny, kernel, iterations=1)
img123_canny = np.hstack((img1_canny_dlt, img2_canny_dlt, img3_canny_dlt))
cv2.imshow('Images_canny', img123_canny)

f:id:nokixa:20210630232655p:plain

さあこれでどうだぁ

linesP1 = cv2.HoughLinesP(img1_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
linesP2 = cv2.HoughLinesP(img2_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
linesP3 = cv2.HoughLinesP(img3_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=5)
lines = [linesP1, linesP2, linesP3]
imgs = [img1.copy(), img2.copy(), img3.copy()]
for i in range(3):
    for line in lines[i]:
        imgs[i] = cv2.line(imgs[i], (line[0,0],line[0,1]), (line[0,2],line[0,3]), (0,0,255), 2)

imgs123 = np.hstack((imgs[0], imgs[1], imgs[2]))
cv2.imshow('Images', imgs123)

f:id:nokixa:20210701234041p:plain

ありゃー、台紙内部の点が直線として認識されてしまったー

最大ライン間ギャップを短くしてみます。

linesP1 = cv2.HoughLinesP(img1_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=2)
linesP2 = cv2.HoughLinesP(img2_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=2)
linesP3 = cv2.HoughLinesP(img3_canny_dlt, 1, np.pi/180, 100, minLineLength=50, maxLineGap=2)
...

f:id:nokixa:20210703001143p:plain

これもダメ。
内部点はだいぶ減りましたがまだあって、外枠の直線も結局ちゃんと取れていません。

今回の画像は欲しい線が少し曲がっていて、かつ2値化した後に 直線以外のものも残ってしまっているのでいい結果にならなかったのかなと。

これをやるなら前々回の輪郭検出をやったほうがよさそうです。


まとめ

今回はうまくいかなかったパターンですね。
また使える機会があれば使ってみたい。

次回はハフ変換による円検出をやってみます。