CNNを利用した画像判定器の作成

今流行りのディープラーニングを使って画像判定器を作ってみた。GW丸々潰した...

スクレイピングで画像を集める

bingAPIとFlickrAPIを用いた。

bingAPI事前準備 qiita.com 上記のコードは動かなかったので、 この記事のコードを使ったら動いた。 qiita.com

FlickrAPIはこの記事をコピペ。

qiita.com

(本当は、googleから画像を取得できればいいんだけど、googleAPIの仕様なのか1回走らせると10枚程度しか集められないらしい。)

qiita.com

データセット準備

#ライブラリ
from PIL import Image,ImageFilter
import os,glob
import numpy as np
from sklearn import model_selection
#リスト作成、数取得、画像ピクセル定義
classes=["miku","rem","chino"]
num_classes=len(classes)
image_size=50
#検証用データ数定義
num_testdata=15
#画像読み込み
X_train=[]
X_test=[]
Y_train=[]
Y_test=[]
#リスト取り出し、番号を振ってfor文に渡す
for index,classlabel in enumerate(classes):
    #写真のディレクトリ
    photos_dir="./"+classlabel
    #パターン一致でファイル一覧を取得
    files=glob.glob(photos_dir + "/*.jpg")
    #番号を振って順番に画像取り出す
    for i, file in enumerate(files):
        #RGB変換リサイズnumpy変換
        if i>= 147: break
        image = Image.open(file)
        image = image.convert("RGB")
        image = image.resize((image_size, image_size))
        data = np.asarray(image)
        #検証用データ確保
        if i<num_testdata:
            X_test.append(data)
            Y_test.append(index)
        #画像増幅
        else:
            for angle in range(-20,20,5):
                #回転して追加
                img_r =image.rotate(angle)
                data=np.asarray(img_r)
                X_train.append(data)
                Y_train.append(index)
                #反転して追加
                img_trans=img_r.transpose(Image.FLIP_LEFT_RIGHT)
                data=np.asarray(img_trans)
                X_train.append(data)
                Y_train.append(index)
                #フィルター加工して追加
                img_resample=img_trans.rotate(0,expand=True,resample=Image.BICUBIC)
                data=np.asarray(img_resample)
                X_train.append(data)
                Y_train.append(index)
                #エッジ強化して追加
                img_shr=img_trans.filter(ImageFilter.EDGE_ENHANCE)
                data=np.asarray(img_shr)
                X_train.append(data)
                Y_train.append(index)

#numpy変換
X_train=np.array(X_train)
X_test=np.array(X_test)
y_train=np.array(Y_train)
y_test=np.array(Y_test)
#xyに格納
xy = (X_train, X_test, y_train, y_test)
#保存
np.save("./anime_data.npy",xy)

例えば初音ミク、レム、チノちゃん、という青髪キャラ判定器を作りたいなら、
スクレイピングした画像が入っているファイルの名前を、それぞれmiku、rem、chinoと指定。
カレントディレクトリ上にデータセット(anime_data.npy)が出力される。

画像の水増しをしているため、元のデータの 9 (回転) ×2 (反転) ×2 (フィルター加工)×2 (エッジ強化)= 72倍になっているはず。

(水増しをやりすぎると過学習の原因になるため、十分な量の画像を確保できる場合やらなくてもいい。)

CNNによる深層学習

#ライブラリ
from keras.models import Sequential
from keras.layers import Conv2D,MaxPooling2D
from keras.layers import Activation,Dense,Dropout,Flatten
from keras.utils import np_utils
import keras
import numpy as np
#リスト作成、数取得、画像を50pxと定義
classes=["miku","rem","chino"]
num_classes=len(classes)
image_size=50

#メイン関数定義
def main():
    #ファイルからテータを配列に読み込む(Xが画像、Yがラベル)
    X_train,X_test,y_train,y_test=np.load("./anime_data.npy")
    #正規化(0から1の範囲に収める)
    X_train=X_train.astype("float")/256
    X_test=X_test.astype("float")/256
    #正解に1、他0を入れる行列に変換(one-hot-vetcor)
    y_train =np_utils.to_categorical(y_train,num_classes)
    y_test=np_utils.to_categorical(y_test,num_classes)
    #モデル作成関数呼び出し
    model =model_train(X_train,y_train)
    #モデル評価関数呼び出し
    model_eval(model,X_test,y_test)
#CNN定義
def model_train(X,y):
    #連続なモデルであると定義。
    model = Sequential()
    #畳み込み演算。32が出力の次元。3*3のフィルタを用いる。畳み込み結果が同じサイズになるようにする。
    model.add(Conv2D(32,(3,3),padding='same',input_shape=X.shape[1:]))
    #活性化関数(ランプ関数)正の値は通し、負は0
    model.add(Activation('relu'))
    model.add(Conv2D(32,(3,3)))
    model.add(Activation('relu'))
    #一番大きい値を取り出す
    model.add(MaxPooling2D(pool_size=(2,2)))
    #データ35%捨てる(過学習防止)
    model.add(Dropout(0.35))
    model.add(Conv2D(64,(3,3),padding='same'))
    model.add(Activation('relu'))
    model.add(Conv2D(64,(3,3)))
    model.add(Activation('relu'))
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Dropout(0.35))
    #Flatten処理(データを1列に並べる)
    model.add(Flatten())
    #全結合型ニューラルネットワーク
    model.add(Dense(512))
    model.add(Activation('relu'))
    model.add(Dropout(0.5))
    model.add(Dense(3))
    #softmax関数(負の値を正の値に変換して確率に変換)
    model.add(Activation('softmax'))
    #最適化アルゴリズム(1回回すごとに学習率を下げていくらしい)
    opt=keras.optimizers.rmsprop(lr=0.0001,decay=1e-6)
    #損失関数(正解と推定値の誤差)定義、最適化アルゴリズム定義、評価の値を正答率で定義
    model.compile(loss='categorical_crossentropy',optimizer=opt,metrics=['accuracy'])
    #学習分割数、学習回数定義
    model.fit(X,y, batch_size=128,nb_epoch=40)
    #学習データ保存
    model.save('./anime_cnn.h5')
    return  model
#評価関数定義
def model_eval(model,X, y):
    #結果受け取り
    scores=model.evaluate(X,y,verbose=1)
    #損失量表示
    print('Test Loss:',scores[0])
    #精度表示
    print('Test Accuracy:', scores[1])
#ほかのプログラムから参照する際、このプログラムが呼ばれたときのみmainを実行
if __name__ =="__main__":
    main()

Keras公式サイトを参考にして色々パラメーター弄ると精度上がったり下がったりする。
知識ない場合公式サイトに載ってる例の通りでいいかも。
CIFAR-10 CNN - Keras Documentation

画像判定器の作成

#ライブラリ
import os
from flask import Flask,request,redirect,url_for,render_template
from werkzeug.utils import secure_filename
from keras.models import Sequential,load_model
import keras,sys
import numpy as np
from PIL import Image
import tensorflow as tf

#動物の学習データ取り込み
model=load_model('./animal_cnn.h5')
graph = tf.get_default_graph()
classes=["猿","イノシシ","カラス"]
num_classes=len(classes)
image_size=50
#キャラクター学習データ取り込み
model_anime=load_model('./anime_cnn.h5')
graph_anime=tf.get_default_graph()
classes_anime=["ミク","レム","チノ"]
num_classes_anime=len(classes_anime)
image_size_anime=50
#犬猫学習データ取り込み
model_dogcat=load_model('./animal_cnn_dogcat.h5')
graph_dogcat=tf.get_default_graph()
classes_dogcat=["犬","猫"]
num_classes_dogcat=len(classes_dogcat)
image_size_dogcat=50
#アップロードフォルダパス指定
UPLOAD_FOLDER='./static/uploads'
ALLOWED_EXTENSIONS=set(['png','jpg','gif'])

#flaskのおまじない
app=Flask(__name__)
app.config['UPLOAD_FOLDER']=UPLOAD_FOLDER
#何かしらの文字入れないと動きませんでした。多分ログイン機能追加するときに使う?
app.config["SECRET_KEY"]="sample"
#.があるかどうかのチェックと、拡張子の確認。
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.',1)[1].lower() in ALLOWED_EXTENSIONS

#ルーティング
@app.route('/')
def index():
    flag="main"
    #ページ転送
    return render_template('index.html',flag=flag)

#動物画像投稿時のアクション
@app.route('/',methods=['GET','POST'])
def post():
    #学習データ読み込み
    global graph
    with graph.as_default():
     if request.method=='POST':
         #ファイルがなかった時の処理
        if 'file' not in request.files:
            flag="animal_none"
            return render_template('index.html',flag=flag)
        #アップロードされたファイル取り出し
        file =request.files['file']
        #ファイル名がなかった時の処理
        if file.filename == '':
            flag="animal_none"
            return render_template('index.html',flag=flag)
        #アップロードされたファイルのチェック
        if file and allowed_file(file.filename):
            #危険な文字の削除(サニタイズ処理)
            filename = secure_filename(file.filename)
            #ファイル保存
            file.save(os.path.join(app.config['UPLOAD_FOLDER'],filename))
            filepath = os.path.join(app.config['UPLOAD_FOLDER'],filename)

            #RGB変換、リサイズ、numpy配列変換
            image=Image.open(filepath)
            image=image.convert('RGB')
            image=image.resize((image_size,image_size))
            data=np.asarray(image)
            #リスト型変数宣言、data付加、numpy配列変換
            X=[]
            X.append(data)
            X=np.array(X)
            #結果を格納
            result=model.predict([X])[0]
            #最大値を格納
            predicted=result.argmax()
            #百分率化
            percentage=int(result[predicted]*100)
            return render_template('index.html',classes=classes[predicted],percentages=str(percentage),filepath=filepath)

#ルーティング
@app.route('/bunken')
def bunken():
    flag="bunken"
    return render_template("index.html",flag=flag)
#ルーティング
@app.route('/anime')
def ani():
    flag="anime"
    return render_template('index.html',flag=flag)
#キャラクター画像投稿時のアクション(変数が変わるだけで動物画像時と仕組みは同じ)
@app.route('/anime',methods=['GET','POST'])
def anime():
    global graph_anime
    with graph_anime.as_default():
     if request.method=='POST':
        if 'file' not in request.files:
            flag="anime_none"
            return render_template('index.html',flag=flag)
        file =request.files['file']
        if file.filename == '':
            flag="anime_none"
            return render_template('index.html',flag=flag)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'],filename))
            filepath = os.path.join(app.config['UPLOAD_FOLDER'],filename)

            image=Image.open(filepath)
            image=image.convert('RGB')
            image=image.resize((image_size_anime,image_size_anime))
            data=np.asarray(image)
            X=[]
            X.append(data)
            X=np.array(X)

            result=model.predict([X])[0]
            predicted=result.argmax()
            percentage_anime=int(result[predicted]*100)
            return render_template('index.html',classes=classes_anime[predicted],percentages_anime=str(percentage_anime),filepath=filepath)
#ルーティング
@app.route('/dogcat')
def dogcat():
    flag="dogcat"
    return render_template('index.html',flag=flag)
#犬猫画像投稿時のアクション(変数が変わるだけで動物画像時と仕組みは同じ)
@app.route('/dogcat',methods=['GET','POST'])
def dogcats():
    global graph_dogcat
    with graph_dogcat.as_default():
     if request.method=='POST':
        if 'file' not in request.files:
            flag="dogcat_none"
            return render_template('index.html',flag=flag)
        file =request.files['file']
        if file.filename == '':
            flag="dogcat_none"
            return render_template('index.html',flag=flag)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'],filename))
            filepath = os.path.join(app.config['UPLOAD_FOLDER'],filename)

            image=Image.open(filepath)
            image=image.convert('RGB')
            image=image.resize((image_size_dogcat,image_size_dogcat))
            data=np.asarray(image)
            X=[]
            X.append(data)
            X=np.array(X)

            result=model.predict([X])[0]
            predicted=result.argmax()
            percentage_dogcat=int(result[predicted]*100)
            return render_template('index.html',classes=classes_dogcat[predicted],percentages_dogcat=str(percentage_dogcat),filepath=filepath)

    #最初にimportするとなぜか動かないので直前に書きました。
    from flask import send_flom_directory
    #保存したアップロードファイル呼び出し
    @app.route('/static/uploads/<filename>')
    def uploaded_file(filename):
        return send_from_directory(app.config['UPLOAD_FOLDER'],filename)

webフレームワークのFlaskを用いた。
公式サイトとpaizaがおすすめ。

「Flask」入門講座一覧(1講座) | プログラミング学習ならpaizaラーニング

Welcome to Flask — Flask Documentation (1.1.x)

最初にpaizaの入門講座を1周して公式サイトを読んだ。

イノシシ猿カラス判定器、いぬねこ判定器、青髪キャラ判定器
3つの判定器を組み込んだのでちょっとコードが長くなってしまった。

f:id:tosonshirley:20190911231651p:plain
判定器実行画面

画像をアップロードすると、判定してくれる。

f:id:tosonshirley:20190911231727p:plain
f:id:tosonshirley:20190911231745p:plain

精度73%くらいなので間違える時もある。

f:id:tosonshirley:20190911232545p:plain
f:id:tosonshirley:20190911231822p:plain
f:id:tosonshirley:20190911231812p:plain

青髪キャラは画像をあまり集められなかった(各50枚程度)ことで過学習気味になってしまい精度があまり出なかった。
OpenCVなどを用いて顔の部分だけ特徴量を抽出させてみると改善につながるかもしれない。

pythonも深層学習もド素人であるが、ライブラリをフル活用することでなんとか形になった。
GW全日潰したが良い経験になった。

Githubに環境設定等載せました。

GitHub - toson77/Image-Judgement-AI