12/9(月) 応用科学学会シンポジウムで自動運転に関する講演を担当します☆彡(試乗会もあります!来て!)

14-22. 【ハマごはん vs まるみキッチン】卵の黄身が大きいのはどちらか?(vcclick、決定木、Opencvを活用した物体検出)(前編)

やること

Twitterで美味しそうな匂いを漂わせているアカウントが2つ・・・

ハマごはん【お手軽レシピ】

クリックでTwitterにジャンプします

まるみキッチン【簡単レシピ】

クリックでTwitterにジャンプします

どちらもお手軽レシピを紹介する有名アカウントですが、写真を眺めていると気がつくことがあります。

卵の黄身がデカい

一昔前、お手軽レシピ界隈では何でもかんでもベーコンで巻く「ベーコン一強時代」というのがあって[要出典]、もう巻けるものがなくなって一度は下火になりました。その後、みかんをカゴに盛るだけとかカップ麺にお湯を注ぐだけといった混迷期を経て[要出典]、ここ数年は「卵の黄身一強時代」が到来しています[要出典]。もうとにかく黄身をドアップで写します。

今回はPythonのOpencvを主軸に、お手軽にデータ分析や機械学習ができる「Exploratory」やビネクラ開発の領域抽出パッケージ「vcclick」を活用して卵の黄身を物体検出する方法を見ていきましょう。古典的な方法なので深層学習は使いません。

実行環境

WinPython3.6をおすすめしています。

WinPython - Browse /WinPython_3.6/3.6.7.0 at SourceForge.net
Portable Scientific Python 2/3 32/64bit Distribution for Windows

お手軽にデータ分析や機械学習ができる「Exploratory」。最近はこれで前処理をサクッと済ませています。

Exploratory
Data Science is not just for Engineers and Statisticians. Exploratory makes it for Everyone.

手前味噌ですがなかなかニッチな要求に応えてくれていると思う、領域抽出パッケージ「vcclick」。ウィンドウが立ち上がる都合でColabでは使えません(執筆時点)。

準備

vcclickをインストールします。

pip install vcclick

ハマごはんとまるみキッチンから卵の黄身が写っている写真をダウンロードしました。ハマごはんからは127枚、まるみキッチンからは128枚お借りしました。

これ見よがしの黄身。

vcclickで黄身の色サンプルを抽出

今回(前編)では、「黄身の色」と「それ以外の色」を決定木で分けることを目指します。

「extract」フォルダを作り、集めた写真から適当な(代表的な)ものを10枚程度コピーして入れておきます。

次のコードはvcclickのチュートリアルを改造したものです。「extract」フォルダ内の写真が次々と表示されるので、黄身をクリックで囲みます。左クリックで点を繋ぎ、最後に右クリックで領域を閉じます。10枚が終わると「extract.csv」が保存されます。

import cv2, os
from vcclick import vcclick
import matplotlib.pyplot as plt

#フォルダ内の写真を全て読み込む
def get_ims(folder):
    files = os.listdir(folder)
    ims = []
    for file in files[:]:
        img = cv2.imread(folder + file, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, (100, int(img.shape[0]*(100/img.shape[1])))) #横100pxにリサイズ
        ims.append(img)
    return ims

#フォルダ
folder = 'extract/'

#写真読み込み
ims = get_ims(folder)

#csv初期化
with open('extract.csv', mode='w', encoding='utf-8-sig') as f:
    f.write('folder,kimi-flg,R,G,B\n'.format())

#領域抽出
for img in ims[:1]:
    #vcclickでクリック座標を取得(マスク配列も取得)
    myclick = vcclick()
    points = myclick.single(img)
    print(points)
    
    #マスク配列を取得
    mask = myclick.get_mask()
    plt.imshow(mask)
    plt.show()
    
    #黄身の部分
    kimi = img[mask]
    
    #他の部分
    other = img[~mask]
    
    #csv追記
    with open('extract.csv', mode='a', encoding='utf-8-sig') as f:
        #黄身データの書き込み(cv2.imreadしたものはBGRの順なので注意)
        for [b, g, r] in kimi:
            f.write('{},{},{},{},{}\n'.format(folder, True, r, g, b))
        #他データの書き込み
        for [b, g, r] in other:
            f.write('{},{},{},{},{}\n'.format(folder, False, r, g, b))

このように囲っていきます。色が取れれば良いので解像度は横100pxに落としてあります。

作成された黄身のマスク

保存されたcsv。Trueが黄身の領域、Falseはそれ以外の領域です。

Exploratoryの決定木解析で「黄身の色」と「それ以外の色」を分ける

「extract.csv」をExploratoryに読み込みます。(画像中のファイル名は異なっています)

決定木でkimi-flgがうまく分類できるようなR, G, Bの条件分岐を出します。TrueよりもFalseがかなり多いため、データの不均衡を調整するオプションを使用します。

決定木ができました。本番ではこれに従って黄身の領域を抽出することになります。

予測マトリックス、F値、AUCもまずまずの値です。

Opencvで黄身を物体検出

一枚の写真の処理を順に見てみましょう。

写真を読み込み、長辺または短辺が最大1000pxになるようにリサイズします。

import cv2
import numpy as np
from copy import deepcopy
import matplotlib.pyplot as plt

#グレースケール画像の表示
def show_gray(img):
    plt.imshow(img, vmin = 0, vmax = 255)
    plt.show()

#BGR画像の表示
def show(img):
    plt.imshow(img[:,:,[2,1,0]], vmin = 0, vmax = 255) #BGRをRGBにして表示
    plt.show()


#ファイル
file = 'E0NSax5VUAIKAnC.jpg'

#読み込み
img = cv2.imread(file, cv2.COLOR_BGR2RGB)

#解像度を調整(最大1000px)
if img.shape[0] < img.shape[1]:
    img = cv2.resize(img, (1000, int(img.shape[0]*(1000/img.shape[1]))))
else:
    img = cv2.resize(img, (int(img.shape[1]*(1000/img.shape[0])), 1000))
show(img)

写真のR, G, Bチャンネルを使い、決定木に従って黄身領域のマスクを作ります。

#RGBを抽出(cv2.imreadしたものはBGRの順なので注意)
B = img[:, :, 0]
G = img[:, :, 1]
R = img[:, :, 2]

#決定木に従って黄身をマスク黄身マスク
mask = np.zeros(R.shape, np.uint8)

mask[(R >= 222) & (B < 116)] = 255
mask[(R >= 222) & (B >= 116)] = 0
mask[(R < 222) & (B < 11) & (R >= 149)] = 255
mask[(R < 222) & (B < 11) & (R < 149)] = 0
mask[(R < 222) & (B >= 11) & (B >= 40)] = 0
mask[(R < 222) & (B >= 11) & (B < 40) & (R >= 172)] = 255
mask[(R < 222) & (B >= 11) & (B < 40) & (R < 172)] = 0
show_gray(mask)

この時点ではかなりノイズが含まれています。

オープニング処理で白ごまノイズを抑制します。(→参考:モルフォロジー変換

#オープニングで白ノイズを除く
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
show_gray(mask)

ブラーと二値化で少し隙間を埋めます。

#ブラーして二値化
mask = cv2.blur(mask, (5, 5))
_, mask = cv2.threshold(mask, 20, 255, cv2.THRESH_BINARY)
show_gray(mask)

クロージング処理でさらに隙間を埋めます。これを強くすると離れ小島が融合してくれるのですが、他のチーズやサーモンなんかと融合することもあるので要調整です。

#クロージングで白領域を融合
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
show_gray(mask)

最後に輪郭抽出を行い、面積が最大の輪郭の外接楕円を写真に描画します。

#輪郭抽出
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

#最大面積の輪郭に絞る
max_contour = max(contours, key=lambda x: cv2.contourArea(x))
    
#輪郭の外接楕円の描画
ellipse = cv2.fitEllipse(max_contour)
cv2.ellipse(img, ellipse, (0, 0, 255), 10) #BGRなので注意
show(img)

なかなか良いんじゃないでしょうか?楕円で近似するのは、具やトッピングに隠れた境界領域を推定する意味があります。

もう一例置いておきます。

色マスク後
クロージング後

こちらは背景の色がかなり邪魔をしましたが、その後の処理でなんとか黄身を取ることができました。

まとめ

このように複数のソフトを活用して卵の黄身を検出を行いました。後編ではハマごはんとまるみキッチンのどちらが黄身比率が大きいかを調べます。

タイトルとURLをコピーしました