前回の続きになります。
おさらい
前回までのおさらいです。
以下のような手順で台紙外形取得を試みました。
- Cannyエッジ検出実施
- 輪郭検出(最外周のみ)実施
- 面積最大の輪郭を選ぶ
- 輪郭を単純な多角形で近似、四角形になっていればOK
問題だったのは、
- 背景が複雑なパターンになっていて、最外周輪郭が乱れる
- 四角形に近似できなかった
- なぜかよく分からない検出不良
の3つです。
まずは使用データと検出コードを示します。
import cv2 img1 = cv2.imread('harupan_190428_1.jpg') img2 = cv2.imread('harupan_190428_2.jpg') img3 = cv2.imread('harupan_200317_1.jpg') img4 = cv2.imread('harupan_210227_2.jpg') img5 = cv2.imread('harupan_210402_1.jpg') img6 = cv2.imread('harupan_210402_2.jpg') img7 = cv2.imread('harupan_210414_1.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) img4 = cv2.resize(img4, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA) img5 = cv2.resize(img5, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA) img6 = cv2.resize(img6, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA) img7 = cv2.resize(img7, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_AREA)
%matplotlib inline from matplotlib import pyplot as plt
def get_and_disp_card_corners(img): img_canny = cv2.Canny(img, 100, 200) img_contours, img_hierarchy = cv2.findContours(img_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) img_contours.sort(reverse = True, key = lambda x:cv2.contourArea(x)) eps = 0.05 * cv2.arcLength(img_contours[0], True) approx_contour = cv2.approxPolyDP(img_contours[0], eps, True) print('Number of corners : ', approx_contour.shape[0]) print(approx_contour) img_with_corners = cv2.drawContours(img.copy(), [img_contours[0]], -1, (255,0,0), 3) img_with_corners = cv2.drawContours(img_with_corners, [approx_contour], -1, (0,255,0), 3) for pt in approx_contour: cv2.circle(img_with_corners, pt[0], 5, (0,0,255), -1) plt.imshow(cv2.cvtColor(img_with_corners, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) if(approx_contour.shape[0] != 4): print('Not approximated as quadrangle') return approx_contour
問題だった検出結果を示します。
approx_contour1 = get_and_disp_card_corners(img1)
Number of corners : 4
[[[ 28 1]]
[[ 31 402]]
[[270 377]]
[[287 34]]]
approx_contour2 = get_and_disp_card_corners(img2)
Number of corners : 4
[[[ 23 0]]
[[ 34 361]]
[[281 375]]
[[289 17]]]
approx_contour3 = get_and_disp_card_corners(img3)
Number of corners : 5
[[[184 0]]
[[ 38 47]]
[[ 42 387]]
[[278 390]]
[[274 49]]]
Not approximated as quadrangle
approx_contour7 = get_and_disp_card_corners(img7)
Number of corners : 4
[[[218 86]]
[[164 91]]
[[193 90]]
[[174 140]]]
エッジ取得の確認
まずはエッジがきちんと取れているか確認してみます。
img1_canny = cv2.Canny(img1, 100, 200) plt.imshow(img1_canny, cmap='gray'), plt.xticks([]), plt.yticks([]) img1_contours, img1_hierarchy = cv2.findContours(img1_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) img1_contours.sort(reverse = True, key = lambda x:cv2.contourArea(x)) img1_with_contour = cv2.drawContours(img1.copy(), [img1_contours[0]], 0, (0,255,0), 3) plt.figure() plt.imshow(cv2.cvtColor(img1_with_contour, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
エッジ画像を見た感じ、カーペットの模様もエッジ認識されているので、最初にフィルタをかけておいたほうがよさそうかな。
いくつかのフィルタを試してみます。
処理時間も気になるので、測定してみます。
https://docs.opencv.org/4.5.2/dc/d71/tutorial_py_optimization.html
https://note.nkmk.me/python-timeit-measure/
%timeit img1_blur = cv2.blur(img1, (5,5))
453 µs ± 5.02 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit img1_blur2 = cv2.blur(img1, (3,3))
338 µs ± 2.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit img1_gaussian = cv2.GaussianBlur(img1, (5,5), 0)
121 µs ± 34.7 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit img1_gaussian2 = cv2.GaussianBlur(img1, (3,3), 0)
79.6 µs ± 24.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit img1_bilateral = cv2.bilateralFilter(img1, 9, 75, 75)
4.59 ms ± 94.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
ガウシアンフィルタが一番処理時間が短いのか?予想外。
エッジ検出をしてみます。
def plot_edge_and_contour(img, canny_th1, canny_th2): img_canny = cv2.Canny(img, canny_th1, canny_th2) plt.subplot(121), plt.imshow(img_canny, cmap='gray'), plt.xticks([]), plt.yticks([]) img_contours, img_hierarchy = cv2.findContours(img_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) img_contours.sort(reverse = True, key = lambda x:cv2.contourArea(x)) img_with_contour = cv2.drawContours(img.copy(), img_contours[1:], -1, (255,255,0), 2) cv2.drawContours(img_with_contour, [img_contours[0]], -1, (0,255,0), 3); plt.subplot(122), plt.imshow(cv2.cvtColor(img_with_contour, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) return img_contours[0]
img1_blur = cv2.blur(img1, (5,5)) plot_edge_and_contour(img1_blur, 100, 200);
img1_blur = cv2.blur(img1, (3,3)) plot_edge_and_contour(img1_blur, 100, 200);
img1_gaussian = cv2.GaussianBlur(img1, (5,5), 0) plot_edge_and_contour(img1_gaussian, 100, 200);
img1_gaussian = cv2.GaussianBlur(img1, (3,3), 0) plot_edge_and_contour(img1_gaussian, 100, 200);
img1_bilateral = cv2.bilateralFilter(img1, 9, 75, 75) plot_edge_and_contour(img1_bilateral, 100, 200);
ガウシアンフィルタ、バイラテラルフィルタがよさそうですが、処理時間を考えてガウシアンフィルタにしておこうかな。
cv2.blur
でうまくいっていないのはぼけ方が強すぎるからかと。
カーネルサイズもどうしようかという感じですが、3x3にしておこうかと思います。
ある程度のノイズはハフ変換での直線検出、というところで対応されるかと。
ヒストグラム平坦化
Cannyエッジ検出の閾値をどう設定すればいいか悩みます。
画像の全体的な明るさに依存するかと思うので、ヒストグラム平坦化でどうだろう?
まずはヒストグラムを見てみます。HSVで見るかな。
img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV) plt.hist(img1_hsv[:,:,0].ravel(),180,[0,180]); plt.figure() plt.hist(img1_hsv[:,:,1].ravel(),256,[0,256]); plt.figure() plt.hist(img1_hsv[:,:,2].ravel(),256,[0,256]);
Vの値がきちんと広がっているので、特に処理は不要かな。
一応暗いシーンで撮影された場合を想定してVの値のヒストグラム平坦化をやっておきます。
img1_hsv[:,:,2] = cv2.equalizeHist(img1_hsv[:,:,2]) img1_equalized = cv2.cvtColor(img1_hsv, cv2.COLOR_HSV2BGR) plt.subplot(121), plt.title('Original'), plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.title('Equalized'), plt.imshow(cv2.cvtColor(img1_equalized, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) plt.show()
CLAHEでは?
CLAHEは、おおざっぱに言うと画像の部分部分でヒストグラム平坦化をやるような感じです。
これを使ってみます。
上の画像では手の影が映っていて、明るさのばらつきがありますが、これが軽減されるかも。
img1_clahe = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV) clahe = cv2.createCLAHE(clipLimit=4.0, tileGridSize=(8,8)) img1_clahe[:,:,2] = clahe.apply(img1_clahe[:,:,2]) img1_clahe = cv2.cvtColor(img1_clahe, cv2.COLOR_HSV2BGR) plt.subplot(121), plt.title('Original'), plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.title('CLAHE'), plt.imshow(cv2.cvtColor(img1_clahe, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([]) plt.show() plt.figure() plt.hist(img1_clahe[:,:,2].ravel(), 256, [0,256]);
分かりやすい変化としては、カーペット部分の明るさの変化があります。
これはどちらかというと悪いほうに転がってしまったか。
改めて輪郭検出を実施します。
img1_equalized_gaussian = cv2.GaussianBlur(img1_equalized, (5,5), 0) img1_contour = plot_edge_and_contour(img1_equalized_gaussian, 100, 200)
img1_clahe_gaussian = cv2.GaussianBlur(img1_clahe, (5,5), 0) img1_contour = plot_edge_and_contour(img1_clahe_gaussian, 100, 200)
なかなかうまくいかない…
別案検討
一旦シールの領域と台紙の色が付いた部分も白で塗りつぶしてしまう案を考えましたが、どうだろう。
シール台紙上の色のついた部分が邪魔になっているのではと思ったので。
今まで見た春のパン祭り画像だと、シールの色は鮮やかなピンクになっていたので、色相と彩度で分離できそうな。
img1_hsv = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV) ret, img1_th_hue = cv2.threshold(img1_hsv[:,:,0], 150, 255, cv2.THRESH_BINARY) ret, img1_th_sat = cv2.threshold(img1_hsv[:,:,1], 128, 255, cv2.THRESH_BINARY) plt.subplot(121), plt.title('Thresholded(Hue)'), plt.imshow(img1_th_hue, cmap='gray'), plt.xticks([]), plt.yticks([]) plt.subplot(122), plt.title('Thresholded(Sat)'), plt.imshow(img1_th_sat, cmap='gray'), plt.xticks([]), plt.yticks([]) plt.show()
import numpy as np img1_th = cv2.bitwise_and(img1_th_hue, img1_th_sat) img1_seal_masked = np.zeros_like(img1) img1_seal_masked[:,:,0] = cv2.bitwise_or(img1[:,:,0], img1_th) img1_seal_masked[:,:,1] = cv2.bitwise_or(img1[:,:,1], img1_th) img1_seal_masked[:,:,2] = cv2.bitwise_or(img1[:,:,2], img1_th) plt.imshow(cv2.cvtColor(img1_seal_masked, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
img1_gaussian = cv2.GaussianBlur(img1_seal_masked, (3,3), 0) img1_contour = plot_edge_and_contour(img1_gaussian, 100, 200)
ヒストグラム平坦化も適用しておきます。
img1_seal_masked = np.zeros_like(img1) img1_seal_masked[:,:,0] = cv2.bitwise_or(img1_equalized[:,:,0], img1_th) img1_seal_masked[:,:,1] = cv2.bitwise_or(img1_equalized[:,:,1], img1_th) img1_seal_masked[:,:,2] = cv2.bitwise_or(img1_equalized[:,:,2], img1_th) img1_gaussian = cv2.GaussianBlur(img1_seal_masked, (3,3), 0) img1_contour = plot_edge_and_contour(img1_gaussian, 100, 200)
img1_seal_masked = np.zeros_like(img1) img1_seal_masked[:,:,0] = cv2.bitwise_or(img1_clahe[:,:,0], img1_th) img1_seal_masked[:,:,1] = cv2.bitwise_or(img1_clahe[:,:,1], img1_th) img1_seal_masked[:,:,2] = cv2.bitwise_or(img1_clahe[:,:,2], img1_th) img1_gaussian = cv2.GaussianBlur(img1_seal_masked, (3,3), 0) img1_contour = plot_edge_and_contour(img1_gaussian, 100, 200)
ダメな結果になってしまった…
一旦撤退
なんだかハマってきた感じがするので、一旦作戦を考え直したほうがよさそうです。
台紙外枠をロバストに検出をするのはかなり難しそう。
- まず台紙自体ぐにゃぐにゃしているので四角形に近似しづらい
- 台紙に部分的に色がついているので、これも形状認識を邪魔する
- 背景の模様に惑わされる
- 背景が台紙と同じような白色だったらアウト
外枠検出を諦めて、シール領域の検出から始めてしまったほうがよさそうに思います。
シール領域であれば、台紙とは明らかに色が違うはずなので。
極端に斜めから撮影していなければ、楕円→円の補正だけでいいのでは。
Cannyエッジ検出 → 輪郭検出 の流れもあまりよくなかったかと思います。
一応今回得られたこともいくつかあります。
- 今使用しているPCでは、だいたい解像度300x400の画像で、カーネルサイズ5x5のガウシアンフィルタは120us程度、Blurフィルタは450us程度、バイラテラルフィルタは4.5ms程度時間がかかる
- ヒストグラム平坦化の使い方、種類
次回は、シール領域検出からのスタートでやり直してみたいと思います。