ペイントソフトを作って、ついでに手書き数字認識もする

Python で手書き文字認識をするチュートリアルはよくあるんですが、既存のデータセットを使って「はい予測精度90%でました」ばかりで、「僕の数字も認識してくれよ！」ってなります。せっかくなので数字を自分で手書きして、 AI(えーあい) に認識してもらうアプリケーションを作ります。（ウィンドウの描画に使用したゲームエンジン`pyxel`については記事末尾に参考リンクをまとめておきます。）

![play.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/9570d129-271a-2772-85fe-418779a47aa0.gif)


文字を書いて`s`を押すと、その数字を予測します。コード全体は[こちら(pyxelDigitRecognition)](https://github.com/dannyso16/pyxelDigitRecognition)

**注意：機械学習を使いますが、遊びの記事なので全然厳密ではありません。**



## この記事の対象読者

- ペイントソフトを作りたい人

- 手書き文字認識をしてみたい人

- (適当な実装を許してくれる人)

  

## 環境

- scikit-learn 0.21.3

- pyxel 1.2.4

- Windows 10 

  

## 記事の流れ

1. Pyxel でペイントソフトをつくる
2. digit データセットで学習
3. 予測してみる



## 1. Pyxel でペイントソフトをつくる

まず単純なお絵描きソフトを作ります。
作り方はかなり簡単で以下のようにします。

```
画面の各ピクセルを表す二次元配列を作る(今回は64x64)

WHILE True: # 各フレームで
	if マウスをドラッグ中:
		マウスの座標の色を変える
	二次元配列をもとに画面を描画
```

実装の際はマウスの座標の周りも同時に色を変えて、太い線を描画しています（下図）。

![マウスクリック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/cb2da0cd-e64e-86ce-97a6-b3e2d2a04825.png)


<details><summary>ペイントソフト(クリックで展開)</summary><div>

```python
# constants
WINDOW_SIZE = 64

# 0 white, -1 black ウィンドウの色情報
 
windowData = [[0]*WINDOW_SIZE for _ in range(WINDOW_SIZE)]

pyxel.init(WINDOW_SIZE, WINDOW_SIZE) # window 初期化
pyxel.mouse(visible=True)

def update():
    if pyxel.btnp(pyxel.MOUSE_LEFT_BUTTON, hold=2, period=1):
        # マウスの座標の色を変える
        x, y = pyxel.mouse_x, pyxel.mouse_y
        	if 0 &lt;= x &lt; pyxel.width and 0 &lt;= y &lt; pyxel.height:
        		windowData[y][x] = -1

def draw():
    pyxel.cls(pyxel.COLOR_WHITE)
    for y in range(pyxel.height):
        for x in range(pyxel.width):
            if windowData[y][x]==-1:
                pyxel.pix(x, y, pyxel.COLOR_BLACK)

pyxel.run(update, draw)
```

</div></details>

できたのはこれ。

![paint.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/123584d6-007d-62d5-2b7d-2dbbd0c41078.gif)


## 2. digit データセットで学習

データを集めるのはめんどくさいので、sklearn の [digit データセット](https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_digits.html)を使用します。
これは手書き数字認識でよく使用される MNIST を小さくした？感じのデータセットで、

- 8x8 ピクセル
- 16段階グレースケール
- 10 クラス（0～9）
- データ数：1797

です。[公式ドキュメント](https://scikit-learn.org/stable/auto_examples/classification/plot_digits_classification.html#sphx-glr-auto-examples-classification-plot-digits-classification-py)によればこんな画像が入っています。

![sphx_glr_plot_digits_classification_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/59f043bd-fead-6c7d-21ef-0a37de813c12.png)


分類は k近傍法を使用してみました。k近傍法は先人が説明し尽くしているので、リンクだけ貼ります。

●アルゴリズム
[OpenCV documentation](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_ml/py_knn/py_knn_understanding/py_knn_understanding.html)

●irisデータへの使用例
[K近傍法(多クラス分類)](https://qiita.com/yshi12/items/26771139672d40a0be32)

<details><summary>学習するコード(クリックで展開)</summary><div>

```python
def train():
    digits = load_digits()
    X = digits.data
    y = digits.target
    X_train,X_test,y_train,y_test = train_test_split(X, y)
    knn = KNeighborsClassifier()
    knn.fit(X_train, y_train)
    print(knn.score(X_test, y_test)) # 0.98

    # save model
    with open('knn_digit.pkl', 'wb') as f:
        pickle.dump(knn, f)
```

</div></details>

交差検証も何もしていませんが、遊びなのでこれでよしとします。（せいど98%！）



## 3. 予測してみる

学習できた()ので実際に予測してみます。
お絵描きの画面は 64x64, RGB なので、予測する前に 8x8, 16段階グレースケールに変換することに気を付けます。今回はウィンドウを 8x8 の画像で保存し、それを読みだして数字認識をします。

<details><summary>予測するコード(クリックで展開)</summary><div>

```python
def predict() -> int:

    with open('knn_digit.pkl', 'rb') as f:
        loaded_model = pickle.load(f)
    
    img = Image.open("images/screen_shot.png") # ウィンドウの画像
    img = img.convert('L') # convet (r,g,b) to 0-255
    img = (255 - np.array(img))//16 + 1 # convert to 0-15
    img = img.reshape(1, 64)
    
    pred = loaded_model.predict(img)[0] # np.array to int
    return pred
```

</div></details>

これで数字認識ができるようになりました。大きく丁寧に書くと結構認識できます！

ただ 8x8 に圧縮してるので、小さい数字や細い文字は予測できないのであしからず。

![mistakes.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/077bb137-2367-18d6-aa7d-81b99ac91161.gif)


上のgifの「8」は小さく書きすぎて、 8x8 ピクセルにすると潰れてしまうのが原因です(画像ちっさい…)。

![screen_shot_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305855/1d31fb3a-8265-71de-8d78-a7f0e0ea67e8.png)




## 最後に

簡単にまとめましたが、`pickle`で学習モデルを読み込むと`Pyxel`のウィンドウが落ちてしまうのが治せず結構困ったりしました。（モデルの読み込みを別の`python`ファイルで実行するとなぜかうまく行きました…）

今回のアルゴリズムもデータセットも全く実用に耐えるものではありませんが、アプリケーションにすると中々面白かったです！

お気づきの点がありましたら、ご意見お待ちしております。



## 参考リンク

[kitao/pyxel (Github)](https://github.com/kitao/pyxel/blob/master/README.ja.md "Pyxel")

[レトロゲームエンジンPyxelでプログラミングを始めよう！](http://tkitao.hatenablog.com/entry/2018/11/24/185346)

[PyxelDigitRecognition (Github)](https://github.com/dannyso16/pyxelDigitRecognition)