勉強しないとな~blog

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

OpenCVやってみる - 26. 台紙外枠検出その2

前回の続きになります。

おさらい

前回までのおさらいです。
以下のような手順で台紙外形取得を試みました。

  • 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]]]



f:id:nokixa:20211126015528p:plain

approx_contour2 = get_and_disp_card_corners(img2)
Number of corners :  4
[[[ 23   0]]

 [[ 34 361]]

 [[281 375]]

 [[289  17]]]



f:id:nokixa:20211126020019p:plain

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



f:id:nokixa:20211126020102p:plain

approx_contour7 = get_and_disp_card_corners(img7)
Number of corners :  4
[[[218  86]]

 [[164  91]]

 [[193  90]]

 [[174 140]]]



f:id:nokixa:20211127030031p:plain

エッジ取得の確認

まずはエッジがきちんと取れているか確認してみます。

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([])

f:id:nokixa:20211127034740p:plain

f:id:nokixa:20211127034806p:plain

エッジ画像を見た感じ、カーペットの模様もエッジ認識されているので、最初にフィルタをかけておいたほうがよさそうかな。
いくつかのフィルタを試してみます。
処理時間も気になるので、測定してみます。

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

f:id:nokixa:20211127034827p:plain

img1_blur = cv2.blur(img1, (3,3))
plot_edge_and_contour(img1_blur, 100, 200);

f:id:nokixa:20211127034853p:plain

img1_gaussian = cv2.GaussianBlur(img1, (5,5), 0)
plot_edge_and_contour(img1_gaussian, 100, 200);

f:id:nokixa:20211127034913p:plain

img1_gaussian = cv2.GaussianBlur(img1, (3,3), 0)
plot_edge_and_contour(img1_gaussian, 100, 200);

f:id:nokixa:20211127034936p:plain

img1_bilateral = cv2.bilateralFilter(img1, 9, 75, 75)
plot_edge_and_contour(img1_bilateral, 100, 200);

f:id:nokixa:20211127034952p:plain

ガウシアンフィルタ、バイラテラルフィルタがよさそうですが、処理時間を考えてガウシアンフィルタにしておこうかな。
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]);

f:id:nokixa:20211127035054p:plain

f:id:nokixa:20211127035052p:plain

f:id:nokixa:20211127035056p:plain

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

f:id:nokixa:20211127035128p:plain

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]);

f:id:nokixa:20211127035244p:plain

f:id:nokixa:20211127035304p:plain

分かりやすい変化としては、カーペット部分の明るさの変化があります。
これはどちらかというと悪いほうに転がってしまったか。

改めて輪郭検出を実施します。

img1_equalized_gaussian = cv2.GaussianBlur(img1_equalized, (5,5), 0)
img1_contour = plot_edge_and_contour(img1_equalized_gaussian, 100, 200)

f:id:nokixa:20211127035448p:plain

img1_clahe_gaussian = cv2.GaussianBlur(img1_clahe, (5,5), 0)
img1_contour = plot_edge_and_contour(img1_clahe_gaussian, 100, 200)

f:id:nokixa:20211127035451p:plain

なかなかうまくいかない…

別案検討

一旦シールの領域と台紙の色が付いた部分も白で塗りつぶしてしまう案を考えましたが、どうだろう。
シール台紙上の色のついた部分が邪魔になっているのではと思ったので。
今まで見た春のパン祭り画像だと、シールの色は鮮やかなピンクになっていたので、色相と彩度で分離できそうな。

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

f:id:nokixa:20211127035559p:plain

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([])

f:id:nokixa:20211127035602p:plain

img1_gaussian = cv2.GaussianBlur(img1_seal_masked, (3,3), 0)
img1_contour = plot_edge_and_contour(img1_gaussian, 100, 200)

f:id:nokixa:20211127035604p:plain

ヒストグラム平坦化も適用しておきます。

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)

f:id:nokixa:20211127035607p:plain

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)

f:id:nokixa:20211127035609p:plain

ダメな結果になってしまった…

一旦撤退

なんだかハマってきた感じがするので、一旦作戦を考え直したほうがよさそうです。

台紙外枠をロバストに検出をするのはかなり難しそう。

  • まず台紙自体ぐにゃぐにゃしているので四角形に近似しづらい
  • 台紙に部分的に色がついているので、これも形状認識を邪魔する
  • 背景の模様に惑わされる
  • 背景が台紙と同じような白色だったらアウト

外枠検出を諦めて、シール領域の検出から始めてしまったほうがよさそうに思います。
シール領域であれば、台紙とは明らかに色が違うはずなので。
極端に斜めから撮影していなければ、楕円→円の補正だけでいいのでは。

Cannyエッジ検出 → 輪郭検出 の流れもあまりよくなかったかと思います。

一応今回得られたこともいくつかあります。

  • 今使用しているPCでは、だいたい解像度300x400の画像で、カーネルサイズ5x5のガウシアンフィルタは120us程度、Blurフィルタは450us程度、バイラテラルフィルタは4.5ms程度時間がかかる
  • ヒストグラム平坦化の使い方、種類


次回は、シール領域検出からのスタートでやり直してみたいと思います。