勉強しないとな~blog

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

OpenCVやってみる- 30. 点数文字輪郭検出3

引き続きの春のパン祭りシール点数集計。

前回の結果再確認

前回の結果をよく見ると、点数文字の輪郭をきちんと取れていない部分がありました。
3つ目の画像で、中央上部の'1'、'2'の輪郭が取れていません。

f:id:nokixa:20211213072314p:plain

対応を検討します。

様子チェック

まずは問題の画像を見てみます。

import cv2
import numpy as np
%matplotlib inline
from matplotlib import pyplot as plt

img3 = cv2.imread('harupan_200317_1.jpg')
img3 = cv2.resize(img3, None, fx=800.0/img3.shape[1], fy=800.0/img3.shape[1], interpolation=cv2.INTER_AREA)
img3_hsv = cv2.cvtColor(img3, cv2.COLOR_BGR2HSV)
ret, th_hue = cv2.threshold(img3_hsv[:,:,0], 135, 255, cv2.THRESH_BINARY)
plt.figure(figsize=(64,48), dpi=100)
plt.subplot(131), plt.imshow(cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)), plt.title('Original',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(img3_hsv[:,:,0], cmap='gray'), plt.title('Hue',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(133), plt.imshow(th_hue, cmap='gray'), plt.title('Thresholded',fontsize=60), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20211223060713p:plain

Hue画像を見てみると、輪郭取得に失敗していたシールではシール領域の一部のはずのエリアで黒くなっているように見えます。おそらく元々のシールの色が赤っぽくて、照明の具合で色相がちょうど一周してしまったのではないかと。
その結果、2値化画像でもシール領域の一部が欠けて、外周輪郭が取れなかったものと考えられます。
Hueのヒストグラムも見てみます。

plt.hist(img3_hsv[:,:,0].ravel(),180, [0,180])
plt.show()

f:id:nokixa:20211223060730p:plain

やはり分布が179(最大値)まで達していて、分布の一部が一周して0付近に来ているようです。

対応検討

HueよりSaturation(彩度)のほうが使えそうな気がしてきたがどうだろう。
台紙は白色でSaturationは低く、シールの色は鮮やかなピンクでSaturationが高くなります。

まずはSaturationのヒストグラムを確認します。

plt.hist(img3_hsv[:,:,1].ravel(),256, [0,256])
plt.show()

f:id:nokixa:20211223060732p:plain

ひとまず閾値は100ぐらいでよさそうかな。
Saturation画像と、2値化した画像を出してみます。

ret, th_sat = cv2.threshold(img3_hsv[:,:,1], 100, 255, cv2.THRESH_BINARY)
plt.figure(figsize=(64,48), dpi=100)
plt.subplot(131), plt.imshow(cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)), plt.title('Original',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(img3_hsv[:,:,1], cmap='gray'), plt.title('Sat',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(133), plt.imshow(th_sat, cmap='gray'), plt.title('Thresholded',fontsize=60), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20211223060740p:plain

これでシール輪郭は取れそうですが、少し心配な点が。

  • フローリングも検出されているので、第一輪郭を取ると台紙の外形が取れてしまうのでは?
  • そうすると第二輪郭はシール外形となってしまう

やっぱりHueの情報も使っていきたいなと。
Hue境界の問題については、値の範囲の変換でどうかと。
すなわち、[0,50)ぐらいの範囲を[180,230)に持ってくる変換をすれば、紫~赤の範囲が連続するようになります。

img3_hsv[:,:,0] = np.where(img3_hsv[:,:,0] < 50, img3_hsv[:,:,0]+180, img3_hsv[:,:,0])
plt.hist(img3_hsv[:,:,0].ravel(),256, [0,256])
plt.show()

f:id:nokixa:20211223060759p:plain

ちょっと思い付き。
Saturationの値が低かったらHueの値を0にしてしまうとどうだろう。
HueとSaturationでの2値化処理をまとめてできるし、Hueの値のふるい分けもしやすくなる。

また、色々調べていて、cv2.inRange()という2値化に便利な関数があることを知ったので、以下ではこれを使っています。

https://docs.opencv.org/4.x/da/d97/tutorial_threshold_inRange.html

img3_hsv[:,:,0] = np.where(img3_hsv[:,:,1] < 100, 0, img3_hsv[:,:,0])
plt.hist(img3_hsv[:,:,0].ravel(),256, [0,256])
plt.show()

f:id:nokixa:20211223060801p:plain

これを元に2値化します。閾値は、今までは135~179にしていましたが、今回は135~190に広げてみます。

th_hue = cv2.inRange(img3_hsv[:,:,0], 135, 190)
plt.figure(figsize=(64,48), dpi=100)
plt.subplot(131), plt.imshow(cv2.cvtColor(img3, cv2.COLOR_BGR2RGB)), plt.title('Original',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(132), plt.imshow(img3_hsv[:,:,0], cmap='gray'), plt.title('Hue',fontsize=60), plt.xticks([]), plt.yticks([])
plt.subplot(133), plt.imshow(th_hue, cmap='gray'), plt.title('Thresholded',fontsize=60), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20211223060808p:plain

これで点数文字輪郭の検出を試してみると、

contours, hierarchy = cv2.findContours(th_hue, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
indices0 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] == -1]
indices1 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] in indices0]
contours1 = [contours[i] for i in indices1]
contours1_filtered = [ctr for ctr in contours1 if cv2.contourArea(ctr) > 800*800/4000]
img_ctrs = cv2.drawContours(img3, contours1_filtered, -1, (0,255,0), 2)
plt.figure(figsize=(10,10), dpi=100)
plt.imshow(cv2.cvtColor(img_ctrs, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
plt.show()

f:id:nokixa:20211223060824p:plain

なんとかいけました!

再確認

他の画像でも再確認してみます。
前回作った関数を変更して適用する感じです。

# image: Input image, BGR format
def calculate_harupan(image, debug):
    h, w, chs = image.shape
    if h > 800 or w > 800:
        k = 800.0/h if w > h else 800.0/w
    else:
        k = 1.0
    img = cv2.resize(image, None, fx=k, fy=k, interpolation=cv2.INTER_AREA)
    if debug:
        print('Resized to ', img.shape)
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # Convert hue value (rotation, mask by saturation)
    hsv[:,:,0] = np.where(hsv[:,:,0] < 50, hsv[:,:,0]+180, hsv[:,:,0])
    hsv[:,:,0] = np.where(hsv[:,:,1] < 100, 0, hsv[:,:,0])
    # Thresholding with cv2.inRange(), using different threshold
    th_hue = cv2.inRange(hsv[:,:,0], 135, 190)
    contours, hierarchy = cv2.findContours(th_hue, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    indices0 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] == -1]
    indices1 = [i for i,hier in enumerate(hierarchy[0,:,:]) if hier[3] in indices0]
    if debug:
        print('Number of contours: ', len(contours))
        print('Number of indices0: ', len(indices0), 'indices1: ', len(indices1))
    contours1 = [contours[i] for i in indices1]
    contours1_filtered = [ctr for ctr in contours1 if cv2.contourArea(ctr) > 800*800/4000]
    if debug:
        return contours1_filtered, img
    else:
        return contours1_filtered
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')

imgs = [img1, img2, img3, img4, img5, img6, img7]

plt.figure(figsize=(20,15), dpi=100)
for i,im in enumerate(imgs):
    ctrs, img_resize = calculate_harupan(im, True)
    img_ctrs = cv2.drawContours(img_resize, ctrs, -1, (0,255,0), 2)
    plt.subplot(241+i), plt.imshow(cv2.cvtColor(img_ctrs, cv2.COLOR_BGR2RGB)), plt.xticks([]), plt.yticks([])
    
plt.show()
Resized to  (1067, 800, 3)
Number of contours:  2514
Number of indices0:  1448 indices1:  875
Resized to  (1067, 800, 3)
Number of contours:  2269
Number of indices0:  1265 indices1:  818
Resized to  (1067, 800, 3)
Number of contours:  2062
Number of indices0:  1154 indices1:  718
Resized to  (1067, 800, 3)
Number of contours:  1204
Number of indices0:  450 indices1:  664
Resized to  (1067, 800, 3)
Number of contours:  1613
Number of indices0:  698 indices1:  795
Resized to  (1067, 800, 3)
Number of contours:  1242
Number of indices0:  373 indices1:  777
Resized to  (1067, 800, 3)
Number of contours:  1258
Number of indices0:  555 indices1:  595

f:id:nokixa:20211223060830p:plain

閾値が少し心配でしたが、照明の反射で取得できなかったものを除いて、きちんと点数文字の輪郭が取得できました!

以上

今回はここまでです。
次回は、前回考えていたようにテンプレートマッチングをやっていきます。