勉強しないとな~blog

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

OpenCVやってみる- 25. 台紙外枠検出

予告通り、春のパン祭り台紙の外枠検出です。
今後は基本的にJupyter notebookで試した後、markdownでダウンロードしてブログに貼り付ける形になると思います。


21.11.22 修正
cv2.drawContour関数の使い方に間違いがありました…
第2引数は輪郭のリストにしないといけないので、面積最大の輪郭を取得した後は[]で囲っておかないといけませんでした。
この修正で、輪郭の描画結果が変わりました。

台紙外枠の取得

まずは台紙外枠の取得から始めてみたいと思います。 改めてサンプル画像を示します。

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)
img1.shape
(403, 302, 3)
%matplotlib inline
from matplotlib import pyplot as plt
plt.subplot(241), plt.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)), plt.title('Image1')
plt.subplot(242), plt.imshow(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)), plt.title('Image2')
plt.subplot(243), plt.imshow(cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)), plt.title('Image3')
plt.subplot(244), plt.imshow(cv2.cvtColor(img4, cv2.COLOR_BGR2RGB)), plt.title('Image4')
plt.subplot(245), plt.imshow(cv2.cvtColor(img5, cv2.COLOR_BGR2RGB)), plt.title('Image5')
plt.subplot(246), plt.imshow(cv2.cvtColor(img6, cv2.COLOR_BGR2RGB)), plt.title('Image6')
plt.subplot(247), plt.imshow(cv2.cvtColor(img7, cv2.COLOR_BGR2RGB)), plt.title('Image7')
plt.show()

f:id:nokixa:20211121023052p:plain

方針

前に輪郭検出でいい感じに外枠が取れましたが、1つ心配なのは台紙の色と背景色が似たような色になった場合。
2値化の閾値設定が難しくなります。

やっぱりエッジ情報から外枠を取っていきたいところ。
前にエッジ情報+ハフ変換で外枠を取ろうとしましたが、そのときはうまくいかず。

OpenCVチュートリアルを再度見直していると、輪郭を単純な形状で近似する方法があるとのこと。
https://docs.opencv.org/4.5.2/dd/d49/tutorial_py_contour_features.html

これも踏まえて考えると、エッジ情報、輪郭検出、輪郭近似、を組み合わせるとどうだろう。
エッジ情報取得 → 輪郭検出で外枠の輪郭だけ取り出し → 四角形で近似

最終的に得られた四角形の頂点が台紙の角ということになります。

5番目の画像で試してみます。

img5_canny = cv2.Canny(img5, 100, 200)
plt.imshow(img5_canny, cmap='gray'), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211121023204p:plain

findContours()関数で輪郭検出します。
前は第2引数をcv2.RETR_TREEにしていましたが、cv2.RETR_EXTERNALで一番外側の輪郭だけ取得してくれるとのこと。
これでやってみたいと思います。

https://code-graffiti.com/opencv-contour-detection-in-python/

img5_contours, img5_hierarchy = cv2.findContours(img5_canny, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img5_with_contours = cv2.drawContours(img5.copy(), img5_contours, -1, (0,255,0), 2)
plt.imshow(cv2.cvtColor(img5_with_contours, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211121024142p:plain

確かに外側だけ取れているようです。
ただ、台紙の外枠だけでなく、フローリングの線も一部輪郭として取得されています。 得られたデータを確認してみます。

len(img5_contours)
18

ちなみに、RETR_TREEでやってみると、

img5_contours2, img5_hierarchy2 = cv2.findContours(img5_canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img5_with_contours2 = cv2.drawContours(img5.copy(), img5_contours2, -1, (0,255,0), 2)
plt.imshow(cv2.cvtColor(img5_with_contours2, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211121024312p:plain

len(img5_contours2)
713

という感じで、たくさん輪郭が出ています。
内部のシールの輪郭は、台紙外形を元に射影変換をしてから改めて取得しようと思うので、まずは外側だけの取得にしておけば処理が軽くなるかと。

RETR_EXTERNALで取得した輪郭のうち、面積が一番大きいものが台紙外枠と思われるので、それを取り出してみます。

img5_contour_areas = []
for ctr in img5_contours:
    img5_contour_areas += [cv2.contourArea(ctr)]

面積の分布を見てみると、

max(img5_contour_areas)
47000.5
plt.hist(img5_contour_areas, 50, [0,50000])
(array([17.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.]),
 array([    0.,  1000.,  2000.,  3000.,  4000.,  5000.,  6000.,  7000.,
         8000.,  9000., 10000., 11000., 12000., 13000., 14000., 15000.,
        16000., 17000., 18000., 19000., 20000., 21000., 22000., 23000.,
        24000., 25000., 26000., 27000., 28000., 29000., 30000., 31000.,
        32000., 33000., 34000., 35000., 36000., 37000., 38000., 39000.,
        40000., 41000., 42000., 43000., 44000., 45000., 46000., 47000.,
        48000., 49000., 50000.]),
 <BarContainer object of 50 artists>)



f:id:nokixa:20211121024336p:plain

という感じで、面積が明らかに大きいものが1つあるので、これが台紙外枠かと。
これを表示してみます。

import numpy as np
img5_largest_contour = img5_contours[np.argmax(img5_contour_areas)]
img5_with_largest_contour = cv2.drawContours(img5.copy(), [img5_largest_contour], -1, (0,255,0), 3)
plt.imshow(cv2.cvtColor(img5_with_largest_contour, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211122231050p:plain

輪郭だけ表示してみると、

img5_black = np.zeros_like(img5)
img5_only_largest_contour = cv2.drawContours(img5_black, [img5_largest_contour], -1, (0,255,0), 3)
plt.imshow(img5_only_largest_contour), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211122231143p:plain

と、うまく台紙外枠が取れています。

輪郭形状の近似は、cv2.approxPolyDP関数でできます。
特に希望の近似形状を指定できるわけではなさそう。 近似の精度を指定する引数がありますが、他のサイトを調べたりする限り、輪郭の周長の何倍かに設定しています。

eps = 0.01 * cv2.arcLength(img5_largest_contour, True)
approximated_contour = cv2.approxPolyDP(img5_largest_contour, eps, True)

得られた輪郭に含まれる点数が4であれば成功ですが。

approximated_contour.shape
(4, 1, 2)

いけたようです。
中身も見てみよう。

print(approximated_contour)
[[[ 77  72]]

 [[ 45 322]]

 [[263 325]]

 [[237  76]]]

画像に乗せてみると、

img5_with_corners = cv2.drawContours(img5.copy(), [approximated_contour], -1, (0,255,0), 3)
for pt in approximated_contour:
    cv2.circle(img5_with_corners, pt[0], 5, (0,0,255), -1)
    
plt.imshow(cv2.cvtColor(img5_with_corners, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])

f:id:nokixa:20211122231255p:plain

よい感じです。

他の画像も同様の処理をしていきます。

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
imgs = [img1, img2, img3, img4, img5, img6, img7]
approx_contours = []
for img in imgs:
    plt.figure()
    approx_contours += [get_and_disp_card_corners(img)]
Number of corners :  4
[[[ 28   1]]

 [[ 31 402]]

 [[270 377]]

 [[287  34]]]
Number of corners :  4
[[[ 23   0]]

 [[ 34 361]]

 [[281 375]]

 [[289  17]]]
Number of corners :  5
[[[184   0]]

 [[ 38  47]]

 [[ 42 387]]

 [[278 390]]

 [[274  49]]]
Not approximated as quadrangle
Number of corners :  2
[[[172 347]]

 [[ 86 399]]]
Not approximated as quadrangle
Number of corners :  4
[[[ 77  72]]

 [[ 45 322]]

 [[263 325]]

 [[237  76]]]
Number of corners :  4
[[[231  90]]

 [[ 98  99]]

 [[ 59 295]]

 [[217 315]]]
Number of corners :  4
[[[218  86]]

 [[164  91]]

 [[193  90]]

 [[174 140]]]

f:id:nokixa:20211122231351p:plain

f:id:nokixa:20211122231353p:plain

f:id:nokixa:20211122231356p:plain

f:id:nokixa:20211122231359p:plain

f:id:nokixa:20211122231401p:plain

f:id:nokixa:20211122231404p:plain

f:id:nokixa:20211122231407p:plain

だいたい外枠検出できていますが、

  • img1, img2ではカーペットの上で撮影していて、その模様をエッジとして取得、外枠の形が乱れてしまっています。
    エッジ検出の前の下処理が必要かと。
  • img3では、コーナー点が1つ多めに検出されている、すなわち5角形で近似されてしまっています。
  • img4では、画像に余計なものが写っているため、そちらを検出してしまっています。
    これは写真のほうが悪いということでいいかな。実際にこの検出をするときはシール台紙だけ写すようにする、ということで。
  • img7は検出しやすそうなのにうまくコーナーが取れていません。
    検証してみます。

ちなみに、外形近似をしたときのeps = 0.05*...の0.05は、いくつか値を試した結果一番よかったものなので、ここも要調整かと。

一旦ここまで

ここで区切っておきたいと思います。
次回は今回の台紙外枠検出でうまくいかなかった部分の調整をしていきます。