将棋の駒の画像分類アプリ

# はじめに
はじめまして。
私は大学で統計学やデータサイエンスを専攻しており、Rという統計向きのプログラミング言語を使って学習しておりました。社会人になってからはエンジニアとして主にjavaを用いて開発をしております(2024年7月時点)。
AIがここ数年で凄まじい発展を遂げる中で興味が再燃し、転職やスキルアップを目指してオンラインスクール[「Aidemy」](https://aidemy.net/)の「AIアプリ開発講座」に通い始めました。

:::note info
このブログはAidemy Premiumの講座カリキュラムの一環である、受講修了条件を満たすために作成した成果物について記載しています。
:::

# 目次
学習モデルについて
実行環境
学習データの準備
モデルの作成
モデルの評価
webアプリ作成
終わりに

# 学習モデルについて
私が趣味で将棋をやっていることから、将棋の駒の画像分類モデルを作成したいと思いました。
技術としてはすでにHEROZ株式会社の[「棋神アナリティクス」](https://heroz.co.jp/release/2023/11/16_press01-2/)によって将棋の盤面の画像のデジタル化が実現しておりますが、今の私ではそこまでのものを作り上げるのが厳しいため、駒の分類モデルのみとなっています。
# 実行環境
・PC:WindowsPC
・学習モデル作成：Google Coraboratory
・webアプリ作成:Visual Studio Code

# 学習データの準備
以下のサイトから合計で59種類の駒画像を収集しましたが、ライセンスの関係から思ったほど画像を集めることができませんでした。
[しんえれ外部駒](https://shineleckoma.web.fc2.com/)
[将棋ソフト外部用パーツ置き場](https://ux.getuploader.com/fa70/)

各画像をPillowを使って各駒の画像へ切り取りを行いました。

# モデルの作成
GoogleCoraboratoryでモデルを作成していきます。

```Python:Shogi_model.py
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers

path_main = '/content/drive/MyDrive/Colab_Notebooks/Aidemy/Last_Kadai/cropped_Image/'

path_fu = os.listdir(path_main + 'Fu/')
path_kyo = os.listdir(path_main + 'Kyosha/')
path_keima = os.listdir(path_main + 'Keima/')
path_gin = os.listdir(path_main + 'Gin/')
path_kin = os.listdir(path_main + 'Kin/')
path_kaku = os.listdir(path_main + 'Kaku/')
path_hisha = os.listdir(path_main + 'Hisha/')
path_ou = os.listdir(path_main + 'Ou/')
path_to = os.listdir(path_main + 'To/')
path_narikyo = os.listdir(path_main + 'Narikyo/')
path_narikei = os.listdir(path_main + 'Narikei/')
path_narigin = os.listdir(path_main + 'Narigin/')
path_uma = os.listdir(path_main + 'Uma/')
path_ryu = os.listdir(path_main + 'Ryu/')

img_fu = []
img_kyo = []
img_keima = []
img_gin = []
img_kin = []
img_kaku = []
img_hisha = []
img_ou = []
img_to = []
img_narikyo = []
img_narikei = []
img_narigin = []
img_uma = []
img_ryu = []

for i in range(len(path_fu)):
    img = cv2.imread(path_main + 'Fu/' + path_fu[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_fu.append(img)

for i in range(len(path_kyo)):
    img = cv2.imread(path_main + 'Kyosha/' + path_kyo[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_kyo.append(img)

for i in range(len(path_keima)):
    img = cv2.imread(path_main + 'Keima/' +  path_keima[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_keima.append(img)

for i in range(len(path_gin)):
    img = cv2.imread(path_main + 'Gin/' + path_gin[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_gin.append(img)

for i in range(len(path_kin)):
    img = cv2.imread(path_main + 'Kin/' + path_kin[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_kin.append(img)

for i in range(len(path_kaku)):
    img = cv2.imread(path_main + 'Kaku/' + path_kaku[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_kaku.append(img)

for i in range(len(path_hisha)):
    img = cv2.imread(path_main + 'Hisha/' + path_hisha[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_hisha.append(img)

for i in range(len(path_ou)):
    img = cv2.imread(path_main + 'Ou/' + path_ou[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_ou.append(img)

for i in range(len(path_to)):
    img = cv2.imread(path_main + 'To/' + path_to[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_to.append(img)

for i in range(len(path_narikyo)):
    img = cv2.imread(path_main +'Narikyo/' + path_narikyo[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_narikyo.append(img)

for i in range(len(path_narikei)):
    img = cv2.imread(path_main + 'Narikei/' + path_narikei[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_narikei.append(img)

for i in range(len(path_narigin)):
    img = cv2.imread(path_main + 'Narigin/' + path_narigin[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_narigin.append(img)

for i in range(len(path_uma)):
    img = cv2.imread(path_main + 'Uma/' + path_uma[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_uma.append(img)

for i in range(len(path_ryu)):
    img = cv2.imread(path_main + 'Ryu/' + path_ryu[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_ryu.append(img)

X = np.array(img_fu + img_kyo + img_keima + img_gin + img_kin + img_kaku + img_hisha + img_ou + img_to + img_narikyo + img_narikei + img_narigin + img_uma + img_ryu)
y = np.array([0]*len(img_fu) + [1]*len(img_kyo) + [2]*len(img_keima) + [3]*len(img_gin) + [4]*len(img_kin) + [5]*len(img_kaku) + [6]*len(img_hisha) + [7]*len(img_ou) + [8]*len(img_to) + [9]*len(img_narikyo) + [10]*len(img_narikei) + [11]*len(img_narigin) + [12]*len(img_uma) + [13]*len(img_ryu))

rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
label_num = list(set(y))

#配列のラベルをシャッフルする
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

# データの分割
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

#転移学習のモデルとしてVGG16を使用
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

#モデルの定義
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(128, activation="relu"))
top_model.add(Dropout(0.5))
top_model.add(Dense(14, activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:19]:
  layer.trainable = False

model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(learning_rate=1e-4,momentum=0.9),
              metrics=['accuracy'])
# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# 可視化
plt.plot(history.history['accuracy'], label='acc', ls='-', marker='o')
plt.plot(history.history['val_accuracy'], label='val_acc', ls='-', marker='x')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.suptitle('model1', fontsize=12)
plt.legend()
plt.show()
model.summary()
```

とりあえず59枚×14種類でやってみた結果
Test loss: 1.2357170581817627
Test accuracy: 0.6566265225410461
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840505/5358be55-d921-d1df-174c-807671cf15c4.png)

青色が訓練データに対する正解率、オレンジが検証データに対する正解率です。

モデルの精度が0.65とやや不満があったため、画像の水増しをチューターに相談したところ、以下のサイトを教えて下さりました。
https://ann0n.com/36/

画像の性質上、反転はしない方が良いと判断し、閾値処理、ぼかしによる水増し処理を行いました。元の画像を含め295枚×14種類にできました。
さらにそれらの画像の白黒化したものを含めた590枚×14種類で再度実行しました。

その結果
Test loss: 0.6171026825904846
Test accuracy: 0.86077481508255
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3840505/90602ab6-60b1-0503-69ef-b4e5842f7161.png)

何度か実行してみましたが、だいたい0.7から0.8台という感じで、思ったほどの成果が出ませんでした。
また、batch_sizeやepochsを増やしての実行も試しましたが、モデルの精度に特に変化はありませんでした。

# モデルの評価

反省点として、14クラスを分類するには画像の数が少なすぎました。
ただ、これ以上画像を増やすことが難しいため、モデル作成はここで一区切りとさせていただきます。
将棋の駒分類のサービスとしてはとても十分とは言えない結果になりましたが、14クラスある中で0.7から0.8台の精度を達成できたので、概ね満足のいく結果と思っています。

# Webアプリ作成
Flaskとhtml,cssを使ってWebアプリを作成しました。
[shogi-koma-classifier](https://shogi-koma-classifier.onrender.com/)

Flaskのコード
```python:shogi-koma-classifier.py
import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image

import numpy as np


classes = ["歩兵","香車","桂馬","銀将","金将","角行","飛車","王将","と金","成香","成桂","成銀","竜馬","竜王"]
image_size = 50

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

model = load_model('./Shogi_model.h5',compile=False)#学習済みモデルをロード


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)
                
            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"
            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")


if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)
```

htmlのコード
```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Shogi Koma Classifier</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <img class="header_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <a class="header-logo" href="#">Shogi Koma Classifier</a>
    </header>

    <div class="main">    
        <h1> AIが送信された将棋駒画像の種類を識別します</h1>
        <p>画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="送信する" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    <footer>
        <img class="footer_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <small>&copy; 2024 No, inc.</small>   
    </footer>
</body>
</html>
```

cssのコード
```css:stylesheet.css
header {
    background-color: #DEB887;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}

.header-logo {
    color: #fff;
    font-size: 25px;
    margin: 15px 25px;
}

.header_img {
    height: 25px;
    margin: 15px 25px;
}

.main {
    height: 370px;
}

h2 {
    color: #444444;
    margin: 90px 0px;
    text-align: center;
}

p {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

.answer {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #F7F7F7;
    height: 110px;
    margin: -8px;
    position: relative;
}

.footer_img {
    height: 25px;
    margin: 15px 25px;
}

small {
    margin: 15px 25px;
    position: absolute;
    left: 0;
    bottom: 0;
}
```

# 終わりに
思うような結果にはなりませんでしたが、拙い内容とはいえ、自分で一通りAI関連のアプリを作る経験ができてよかったと思います。
中でも、画像を探す作業では、ライセンスなどの関係で画像を集めることが意外と難しかったので、良い勉強になりました。

アプリ作成を通して、AI開発についてたくさん学習し、転職して業務で携わりたいと改めて強く思いました。
また、本アプリも今回の結果で良しとせず、また画像を集めてリベンジできたらと考えております。
最後に、本投稿まで丁寧なサポートやアドバイスをしてくださったAidemyのチューターの皆様に感謝申し上げます。

最後までお読みいただき、ありがとうございました。