Keras に入門する 画像分類 Part.2 画像を用意する

32x32のカラー画像とそのラベルがあれば、前回の方法でそれらを判別するシステムを作れそうなので、今回は32x32の画像を用意してみよう

テーマは料理の画像を判別するシステム、とした

個人的にこのテーマは割と良いものが選べたんじゃないかと自負していて、その理由は

  1. 学習データとして得られる画像が均質になりやすい(大体料理のちょっと上から皿全体が映る写真になる)
  2. 画像の数を集めやすい
  3. 同一料理の画像はそこそこ似てるけど、違う料理の画像はそこそこ違う
  4. 分類できるとそこそこ意味がある

最初は、もっと荒唐無稽なテーマの分類(鉛筆と豚と火星とか)をしようと思っていたけど、料理の画像分類ならなんとなくレシピサイトとかで役立ちそう(少なくともなんかそういうサービスの技術検証とかでどこかがやっててもおかしくなさそう)なので随分マシなテーマが設定できたと思う。

ということで、やることは以下

  1. 料理の種類ごとの画像をあつめる
  2. 画像を正方形にトリミング
  3. 32x32に縮小
  4. nparrayとして保存

早速やっていく

  1. 料理の種類ごとの画像をあつめる

"Google画像検索をスクレイピングしたらいいんでしょ"と思っていたけど

Yahoo、Bing、Googleでの画像収集事情まとめ - Qiita とかみてみるとそんなに簡単でもないみたい

そもそものサンプルは、"10種類のカテゴリに対して、訓練データで50000枚、テストデータで10000枚 だったので各カテゴリで大体6000枚使っていることになる

Googleの画像検索でとりあえず"カレーライス"で検索して"結果をもっと表示"を押して最大限表示させると700枚くらいの画像が表示できた

6000枚には遠く及ばないが、まあまあ現実的な数じゃないかと思うのでこれを使うことにする

具体的な手順は、

  1. Google画像検索で料理の名前を入力
  2. 下にスクロール & "結果をもっと表示"をクリック でできるだけ画像を表示する
  3. Chromeの開発ツールでElementを表示
  4. 全体をコピー
  5. テキストファイルとして保存
  6. 保存したファイルから imgタグ かつ srcが "https://encrypted-tbn0" で始まるものを抜き出す
  7. そのimgタグのsrcを画像ファイルとしてダウンロードする

1~5が手作業になるけど10種類くらいなら対して問題にならないのでこれを実行できるコードを書く

import sys
import urllib.request

from bs4 import BeautifulSoup
from time import sleep
import os

def main(file_path, save_dir):
    html_str = None
    with open(file_path, 'r') as f:
        html_str = f.read()
    soup = BeautifulSoup(html_str, features="html.parser")
    imgs = soup.find_all('img', src=lambda s : s and s.startswith('https://encrypted-tbn0'))
    imgs = [ i['src'] for i in imgs ]

    for idx, url in enumerate(imgs):
        print('{}/{}'.format(idx+1, len(imgs)))
        download(url, './{}/{}'.format(save_dir, idx))

def download(url, dest):
    data = urllib.request.urlopen(url).read()
    with open(dest, mode='wb') as f:
        f.write(data)
        sleep(1)

if __name__ == '__main__':
    menus = [
        ('ramen.txt', 'ramen'),
        ('curry.txt', 'curry'),
        ('spaghetti.txt', 'spaghetti'),
        ('gyoza.txt', 'gyoza'),
        ('omlette-rice.txt', 'omlette-rice'),
        ('sandwich.txt', 'sandwitch'),
        ('sushi.txt', 'sushi'),
        ('hamburg.txt', 'hamburg'),
        ('steak.txt', 'steak'),
        ('miso-soup.txt', 'miso-soup')
    ]
    for menu in menus:
        os.mkdir(menu[1])
        main(menu[0], menu[1])

1~5の手順の結果をramen.txt とかの名前で保存しておくと、ramen とかのフォルダの下に連番で画像を保存してくれる

ちなみに今回検索したのは

ラーメン カレーライス スパゲッティ 餃子 オムライス サンドイッチ 寿司 ハンバーグ ステーキ 味噌汁

の10種類

  1. 画像のトリミング

note.nkmk.me

に画像の中心を最大の正方形でトリミングする方法が書いてあるのでこれをそのまま使う

(例えば100x200の画像から中心の100x100を抜き出すことにしたけど、余白部分を白とか黒とかでパディングして200x200にするという方法もあるとは思う。面倒なので検証はしていないけど精度が上がる可能性はあると思う)

全部で8000枚くらい集められた

cifar10では学習用と検査用に分けてあったけど、最後の確認用に各カテゴリ2枚ずつ残しておこうと思う

というので書いたコードが以下

# coding: utf-8
from PIL import Image
import glob
import numpy as np
import pickle

def crop_center(pil_img, crop_width, crop_height):
    img_width, img_height = pil_img.size
    return pil_img.crop(((img_width - crop_width) // 2,
                         (img_height - crop_height) // 2,
                         (img_width + crop_width) // 2,
                         (img_height + crop_height) // 2))

def crop_max_square(pil_img):
    return crop_center(pil_img, min(pil_img.size), min(pil_img.size))

def to_ary(d):
    files = glob.glob('./{}/*'.format(d))
    train = []
    test = []
    valid = []
    for i, f in enumerate(files):
        im = Image.open(f)
        # トリミング
        sq_im = crop_max_square(im)
        # 32x32へ変形
        im32 = sq_im.resize((32, 32))
        # numpy化
        im_ary = np.asarray(im32)
        if im_ary.shape != (32, 32, 3):
            continue
        if i < 20:
            test.append(im_ary)
        elif i < 22:
            valid.append(im_ary)
        else:
            train.append(im_ary)
    return train, test, valid

if __name__ == '__main__':
    X_train, X_test, X_valid = [], [], []
    Y_train, Y_test, Y_valid = [], [], []
    dirs = [
      'curry',
      'gyoza',
      'hamburg',
      'miso-soup',
      'omlette-rice',
      'ramen',
      'sandwitch',
      'spaghetti',
      'steak',
      'sushi'
    ]
    for idx, d in enumerate(dirs):
        print(d)
        x_train, x_test, x_valid = to_ary(d)
        X_train = X_train + x_train
        X_test = X_test + x_test
        X_valid = X_valid + x_valid
        Y_train = Y_train + [[idx]] * len(x_train)
        Y_test = Y_test + [[idx]] * len(x_test)
        Y_valid = Y_valid + [[idx]] * len(x_valid)
    with open('img32.pickle', mode='wb') as f:
        pickle.dump((np.array(X_train), np.array(Y_train), np.array(X_test), np.array(Y_test), np.array(X_valid), np.array(Y_valid)), f)

次回このデータで分類器を作ってみよう