手書き数字認識モデルを作成したらがっかりな結果だったので改良してみた

<h2>はじめに</h2>

４年ほど前からエクセルのＶＢＡで自分用の
業務アプリを制作し始めたエクセル業務効率
家？です。

__python__ でＡＩを組みこんだ１ランク上の便
利アプリを作りたいと思い講座の受講を決め
ました。

<h2>本記事の概要</h2>
<h4>手書き数字認識の精度に不満があるので改良します</h4>

初めて __MNIST__ の手書き数字データセット
を利用して学習済みモデルを作成しました。

さっそく手書き数字のファイル作ってモデ
ルに認識させてみたら、思ったよりも認識
精度が悪いです。

手書き数字を何でもサクサク認識してくれる
と思い込んでいた私としては満足いくレベル
ではありませんでした。

それで、認識精度アップを目指し工夫を行い
最終的にはペイントソフトで書いた数字の複
数認識を目指します。

この記事の対象の方としては、
__MNIST__ の データセットを用いて 数字認識モ
デルを構築してみたけど、思ったような認識
精度が得られなかったという方です。

すこし長くなるとは思いますが、興味のある
方は是非ご覧ください。



<br>

# 改善方法とモデル作成の流れ
#### １．白黒反転データセット作成と学習
#### ２．文字を太くする関数を作る
#### ３．ノイズ有りデータセット学習
#### ４．複数の数字認識に対応する
#### ５．自作データセット作成と学習







<br>
<br>

## １．白黒反転データセット作成と学習


数字を書く場合、白い紙に黒のペンで書くの
が一般的だと思います。

図のようにペイントアプリを使い２８＊２８
pix の白い背景に 黒で数字を書いた画像を
作成します。

学習済みモデルに認識させると
__”８です”__ と答えが返ってきて正しく認識し
ません。
　<img width=150 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/94d0a783-d578-c29a-90de-8ab25d403d4a.png">
図：　２８＊２８pix 　黒文字で書いた５

<br>

これが認識できなかったら何が認識できるの
？ということで早速対策します。

<br>
<br>
<br>
<br>
<br>
<br>
<br>


原因は  __ＭＮＩＳＴ__ データセット の背景が黒
で 数字が白 ということにあるようです。

下の図は __ＭＮＩＳＴ__ オリジナルデータの５
の画像です。確かに黒の背景に白文字で描か
れています。
　<img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/9d68fe14-c3e7-b3c3-6a93-2a88c0e55a0c.png">
図：ＭＮＩＳＴデータセットの５





データセットを白黒反転して学習させれば認
識するようになるかも？ということで、まず
は白黒反転データセット作成と学習を行いま
す。

モデルはそのままに数字画像データを反転さ
せても認識できるような気がしますが今回は
より汎化性能の高いモデルを目指します。

<br>
<br>
<br>
<br>
<br>




白黒反転データ作りに取り掛かります。

<br>
<br>
<br>
<br>


使うのは __`cv2.bitwise_not`__ 関数です
この関数は画像を白黒反転してくれます
使い方は以下のとおりです。


```python:sample bitwise_not
#cv2をインポート
import cv2

#imgを白黒反転
img=cv2.bitwise_not(img)

```

これで白黒反転した画像が作成できます。オ
リジナル画像を反転すると図のようになりま
す。
　
<img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/d5b4d597-c82e-6fdc-5d10-2c8a6cbc2471.png">
図：__ＭＮＩＳＴ__ データを白黒反転した５



これで白黒反転画像ができました。
これを __MNIST__ データ全てに行い白黒反転デ
ータセットを作ります。

<br>
<br>
<br>



 __MNIST__ データセットをダウンロードします

 __MNIST__ データセットは教師データとして６
万枚の手書き数字画像と正解ラベルデータ

テストデータとして１万枚の手書き数字画
像と正解ラベルとなっています。

```python:sample load_data 
#mnistの教師データとテストデータをダウンロードします。
(X_train,y_train),(X_test,y_test) = mnist.load_data() 
```

ダウンロードした手書き数字画像データを白
黒反転します。

```python:sample bitwize_not
#cv2をインポート
import cv2

#教師データ６００００枚を白黒反転
X_train_bit_n = cv2.bitwise_not(X_train)

#テストデータ１００００枚を白黒反転
X_test_bit_n = cv2.bitwise_not(X_test)
```
白黒反転データセットができましたので、オ
リジナルのデータセットに追加します。
１２万枚の教師データと２万枚のテストデー
タを作ります。

```python:sample append
#オリジナルの教師データ６００００枚に白黒反転教師データ６００００枚を追加します。
X_train = np.append(X_train, X_train_bit_n, axis=0)

#オリジナルのテストデータ１００００枚に白黒反転テストデータ１００００枚を追加します。
X_test = np.append(X_test, X_test_bit_n, axis=0)

#教師データ正解ラベルはそのまま追加します
y_train = np.append(y_train, y_train, axis=0)

#テストデータ正解ラベルはそのまま追加します
y_test = np.append(y_test, y_test, axis=0)

#教師データ正解ラベルをワンホット値に変換します
y_train = to_categorical(y_train)

#テストデータ正解ラベルをワンホット値に変換します
y_test = to_categorical(y_test)

```
:::note
 __ワンホット値とは__
例えば正解ラベルが２だった場合のワンホット値は
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0]
正解ラベルが６だった場合のワンホット値は
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0]
となります。
:::

これで __MNIST__ データセットと白黒反転デー
タセットを組み合わせたデータセットができ
ました。モデルに学習させます。


<br>
<br>
<br>
<br>
<br>
<br>

１００エポック学習済みモデルを使って先程
の背景が白の５を認識させると、

<img width=150 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/94d0a783-d578-c29a-90de-8ab25d403d4a.png">









<br>

先程までは __”８です”__ と答えていたモデルが
無事に __”５です”__ という答えが返ってきまし
た。成功です。


<br>
<br>
<br>
<br>
<br>
<br>
<br>
<br>


次は少し大きめの５００×５００pixのキャ
ンバスに数字を書きます。

<img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/9337c418-b604-0497-0067-92a31997304a.png">

図：ペイントソフトで書いた５

<br>
<br>
<br>
<br>


画像をモデルに認識させると __”２です”__　と
返ってきて正しく認識しません。
対策を考えます。


<br>
<br>
<br>
<br>

まずモデルにどのような画像が送られている
のか確認します。

下図の左がグレースケール化した後に２値化
したもので、右がその画像を（２８、２８）
にリサイズ、一番下がリサイズした画像を更
に２値化しています。


<table>
<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/71d5aaf2-f1d5-5448-2b61-bc4b5f48b0ea.png"></td>

<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/bcc71263-6fe5-1d13-6a6a-0f7c9338227f.png"></td>
</tr>
<tr>
<td>図：グレースケール化＆２値化</td>

<td>図：処理後にリサイズ</td>
</tr>
<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/14dfc933-8887-d011-f591-98c98049a842.png">
</td>
</tr>

<tr>

<td>図：リサイズ後に２値化</td>
</tr>
</table>

画像を確認します。
描いた線が細いとリサイズした時に線がぼや
けます。更に２値化すると完全に数字が消え
てしまいます。
何も映ってなければ数字を判断することはで
きません。

<br>
<br>
<br>
<br>

## ２．文字を太くする関数を作る
次は線を太くする処理を行います。
線を太くするには膨張処理を行います。膨張
は __cv2.dilate__ を使います。

```python:sample cv2.dilate
#ナンパイをインポート
import numpy as np

#cv2をインポート
import cv2

# ８近傍フィルタ
filt = np.array([[1,1,1],
                 [1,0,1],
                 [1,1,1]],  np.uint8)

#膨張
d_img = cv2.dilate(img, filt)
   
```

<br>

以下が実際のコードで、自作関数の中で膨張
を２回行っています。

```python:sample                     
# 膨張  
def dilate_img(img, screen): # img, screen（背景が白か黒かそれ以外かw, b, c)
    # screenはノイズのない背景が白のデータの場合"w",黒の場合"b"を返す
    # 画面が暗い場合は"c"を返す                       
    # ８近傍フィルタ
    filt = np.array([[1,1,1],
                    [1,0,1],
                    [1,1,1]],  np.uint8)
    if screen == "w": # 背景が白ならば反転して２回膨張処理を行う
        img = cv2.bitwise_not(img)   
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt)
        screen = 'b'
    elif screen == 'b':
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt)
    else: # 反転無しで２回膨張処理を行う
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt) 
        screen = 'b'
    return img, screen
```
膨張は背景が白い画像で行うと線が細くなっ
てしまうので、背景が白い場合は白黒反転を
行ってから膨張を行います。

膨張を行う関数を３回繰り返し行ったのが図
２です。図３が膨張処理後にリサイズで、図
４がリサイズ後更に２値化しています。





<table>
<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/9337c418-b604-0497-0067-92a31997304a.png"></td>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/08e18476-5f42-9fa8-d3d5-509672ef49e3.png"></td>


</tr>


<tr>
<td>図１：元画像</td>
<td>図２：膨張の自作関数を３回</td>

</tr>
<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/17b37466-cee1-32dc-7666-c67066eaa6e0.png
"></td>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/e7113b45-7e5f-d89a-754a-fd290b009ad8.png"></td>

</tr>
<tr>
<td>図３：関数処理後にリサイズ</td>
<td>図４：リサイズ後に２値化</td>

</tr>




</table>

綺麗な画像が得られましたのでモデルに認識
させます。答えは __”４です”__ 
不正解です。
今度は認識精度を高めるためにモデルをさら
に学習させます。

## ３．ノイズ有りデータセット学習
学習させるのは、 __MNIST__ データセットに
ノイズを加えた __mnist-corrupted__ のデータ
セットです。

__mnist-corrupted__ は __MNIST__ のテストデータ
を破損させたデータセットとなっています。
以下がその説明です。
>MNISTCorrupted は、MNIST データセットのテスト画像に 15 個の破損を追加して生成されたデータセットです。このデータセットは、元の作成者によってアップロードされた静的で破損した MNIST テスト イメージをラップします。


という事のようなので、__mnist-corrupted__ を
ダウンロードして、更にこのデータセットを反転さ
せたものも加えてモデルに再度学習させます。



<br>
<br>
<br>
<br>
<br>
<br>
<br>
以下の図のように学習が終了しました。

<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/8ea6413c-2956-e099-ed1b-13fcb1f98724.png">
 
図１５：　__mnist-corrupted__ データセットで 学習



先程のペイントで書いた５を認識させます。
答えは __”５です”__ 
正解です。
ペイントソフトで書いた数字は何とか認識で
きました。次は複数の手書き数字も認識でき
るようにします。


<br>
<br>
<br>
<br>
<br>
<br>
<br>






## ４．複数の数字認識に対応する






考えた複数認識の手順は、まず数字の輪郭抽
出を行い、抽出した輪郭の座標をもとに画像
を切り出しファイルに保存というものです。

ファイルに保存された各数字を１つずつモデ
ルに認識させて最後にまとめて出力します。



それでは作成します。
まず輪郭を抽出して数字ごとに切り出してフ
ァイルに保存する関数を作成します。

輪郭を抽出するには __cv2.findcontours__ を使
用します。この関数は使用する前に画像をグ
レースケール化と２値化を行っておく必要が
あります。

輪郭の座標を取得するには、
 __cv2.boundingRect__ を使用します。この関数
は輪郭の外接矩形を計算します。
以下がコードです。
```python:cv2.findcontours cv2.boundingRect
#cv2をインポート
import cv2

def f_contours(img):
   
    #グレースケール
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    #２値化
    retval, th_img = cv2.threshold(gray, 130, 255, THRESH_BINARY)
    
    #輪郭を抽出する
    contours, hierarchy 
    = cv2.findContours(th_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    #輪郭を描画する
    c_img
    = cv2.drawContours(img, contours, -1, color=(0,255,0), thickness=2)
     
    #輪郭のx, y, w, hを格納するリストを作成
    n = []
    
    #輪郭の位置を格納する
    for i in contours:        
       x, y, w, h = cv2.boundingRect(i)
       
       #外接矩形を描画した画像
       img = cv2.rectangle(c_img, (x, y), (x+w, y+h), (0,255,0), 4)
       
       #ｘ、ｙ、ｗ、ｈを格納
       n.append([x,y,w,h])
    
    #x座標が一番左の外接矩形から並び替える
    num_location=sorted(n)    
       
    #外接矩形を描画した画像と座標を返す   
    return img,num_location

```
>cv.findContours() 関数には 3 つの引数があり、1 つ目はソース画像、2 つ目は輪郭検索モード、3 つ目は輪郭近似メソッドです。そして、変更された画像、輪郭、階層を出力します。等高線は、画像内のすべての輪郭の Python リストです。個々の等高線は、オブジェクトの境界点の(x、y)座標のNumpy配列です。

 __findContours__ の戻り値と引数は以下
の通りです

::: note info


 __image（変更された画像）, contours（輪郭）, hierarchy（輪郭の階層）__ 	
     __=	cv.findContours(ソース画像, 輪郭検索モード, 輪郭近似メソッド	)__ 

:::


輪郭検索モードは５種類あります。

<table>
<tr>
<th>RETR_EXTERNAL </th>
<td>極端な外側の輪郭のみを取得します。すべての輪郭に設定されます。</td>
</tr>
<tr>
<th>RETR_LIST</th>
<td>階層関係を確立せずにすべての輪郭を取得します。</td>
</tr>
<tr>
<th>RETR_CCOMP</th>
<td>すべての輪郭を取得し、2 レベルの階層に整理します。最上位レベルには、コンポーネントの外部境界があります。2番目のレベルでは、穴の境界があります。接続された構成部品の穴の内側に別の輪郭がある場合、その輪郭はトップレベルに配置されます。</td>
</tr>
<tr>
<th>RETR_TREE</th>
<td>すべての輪郭を取得し、ネストされた輪郭の完全な階層を再構築します。</td>
</tr>
<tr>
<th>RETR_FLOODFILL</th>
<th></th>
</tr>
</table>



試した結果 __RETR_EXTERNAL__ が良い結果
を得られたのでこれを使います。


<br>
<br>
<br>

輪郭近似メソッドは４種類あります。
<table>
<tr>
<th>CHAIN_APPROX_NONE</th>
<td>絶対にすべての等高線点を格納します。つまり、等高線の任意の 2 つの後続の点 (x1,y1) と (x2,y2) は、水平、垂直、または対角線の隣接、つまり max(abs(x1-x2),abs(y2-y1))==1 になります。</td>
</tr>
<tr>
<th>CHAIN_APPROX_SIMPLE </th>
<td>水平、垂直、および対角線のセグメントを圧縮し、その端点のみを残します。たとえば、直立した四角形の等高線は 4 つの点でエンコードされます。</td>
</tr>
<tr>
<th>CHAIN_APPROX_TC89_L1</th>
<td>Teh-Chin連鎖近似アルゴリズムのフレーバーの181つを適用します</td>
</tr>
<tr>
<th>CHAIN_APPROX_TC89_KCOS </th>
<td>Teh-Chin連鎖近似アルゴリズムのフレーバーの1つを適用します</td>
</tr>


</table>

こちらも試した結果 __CHAIN_APPROX_SIMPLE__ が良
い結果を得られたのでこれを使います。







この関数を使ってペイントソフトで書いた数
字の輪郭を抽出して外接矩形を描画したもの
が下の画像です。
<table>
<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/c8e4f53a-1df0-3a55-ad06-e98d323c75c7.png"></td>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/eb908ed1-9525-25a2-5078-c38d8ec875a0.png">
</td>

</tr>

<tr>

<td>図：ペイントで書いた１２３</td>
<td>図：線を太くする処理後<br>輪郭を描画</td>

</tr>


<tr>
<td><img width=190 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/031d9691-3b70-3380-2ca2-5c3d98b712ce.png">
</td>

</tr>

<tr>
<td>図：数字をボックスで囲む</td>
</tr>

</table>

各数字を囲むことができました。


今度はそれぞれの数字を切り出してファイル
に保存する関数を作成します。



以下がコードです。
```python:sample
#cv2をインポート
import cv2

#num_locationには輪郭位置がX座標が小さいものから順番に格納されている

#数字を切り出してファイルに保存する関数
def create_num_file(img, num_location, screen):
   
    # 切り出し他画像の一時保存ディレクトリ名
    dir_path = './' + NUM_FOLDER + '/'   
    
    # 切り出す余白の設定
    if img.shape[0] <= 28:
        m1, m2, m3, m4 = 0, 0, 0, 0
    elif img.shape[0] <= 50:
        m1, m2, m3, m4 = 1, 1, 1, 1
    else:
        m1, m2, m3, m4 = 10, 10, 10, 10
    
    # (28,28)以下はそのままファイルに保存する    
    if img.shape[0] <= 28 or img.shape[1] <= 28:
        cv2.imwrite(os.path.join(NUM_FOLDER, "num0" + ".png"), img)
    else:
        # 数字の数だけ画像を切り出してファイルを作る
        for i in range(0, len(num_location)):
            x, y, w, h = num_location[i]  
            
            # 画像を正方形に切り取る
            n1, n2 = abs((h - w) // 2), abs((h - w) // 2)
            
            # エラーにならない為の処理    
            if y - m1 < 0: # 切り取る幅が画像の上をオーバー
                m1 = 0 # 上の余白を0
            if y + h + m2 > img.shape[0]: # 切り取る幅が画像の下をオーバー 
                m2 = 0 # 下の余白を0
            if x - n1 - m3 < 0: # 切り取る幅が画像の左端をオーバー
               m3 = 0 # 左の余白を0
               n1 = 0 # 正方形にする左の変数を0 
            if x + w + n2 + m4 > img.shape[1]: # 切り取る幅が画像の右端をオーバー 
               m4 = 0 # 右の余白を0
               n2 = 0 # 正方形にする右の変数を0
            
            # 画像を余白を付けて切り出す
            trim_img = img[y - m1:y + h + m2, x - n1 - m3:x + w + n2 + m4]
          
            # 切り出された画像の大きさに応じて線を太くする
            if h > img.shape[0] * 0.9 and w > img.shape[1] * 0.9:
                #　大きい画像は２度関数を使用
                trim_img, screen = dilate_img(trim_img, screen) 
                trim_img, screen = dilate_img(trim_img, screen)
                # 切り出した画像をそれぞれファイルに保存する
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
            elif h > img.shape[0] * 0.5 and w > img.shape[1] * 0.5:
                # 切り出した画面サイズが元画像の半分程度の場合は１度関数を使用
                trim_img, screen = dilate_img(trim_img, screen)
                # 切り出した画像をそれぞれファイルに保存する
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
            else: 
                # 切り出した画像をそれぞれファイルに保存する      
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
    return dir_path   
```





この関数を使い数字を切り出した画像が下の
図です。

<table>
<tr>
<td><img width=150 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/95b11e5e-439c-87c8-c1a3-8e2dcaf78103.png">
</td>
<td><img width=150 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/6f090377-4518-4a5f-c7ea-c0c262e1144f.png">
</td>
<td><img width=150 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/e347250b-669a-4b03-8751-eae97157c86d.png">
</td>
</tr>





<tr>
<td>図：切り出した１</td>
<td>図：切り出した２</td>
<td>図：切り出した３</td>
</tr>


</table>

この画像の輪郭を除いたものをそれぞれモデ
ルに認識させます。
結果はそれぞれ __”１です””２です””３です”__ 
全て正解です。







<br>
<br>
<br>
<br>
<br>

数字を変えたり認識文字数を増やすなど条件
を変えて試すと６、７、８、９、の認識精度
が悪いのがわかりました。



そこで、自作の手書き数字データセットを作
り学習させて認識精度を一気に高めたいと思
います。


## ５．自作データセット作成と学習

まずペイントソフトで適当に０から９までの
数字を書いてそれぞれのフォルダにファイル
を保存します。

<img width=350 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/843d6bad-b113-ba7d-5ca4-b1c33149123c.png">


図：作成したフォルダ



各数字ごとに１００個ぐらいファイルが作成
出来たらそのファイルを基に新たなデータセ
ットを作ります。

線を太くしたり、ランダムに上下左右移動や
右左回転させた画像を作成して
（２８、２８）にリサイズして保存します。

この方法で０～９までの画像ファイルを２０
１８０個作って、それに対応した正解ラベル
のリストも作成しました。

以下がそのコードです。
まずペイントソフトの手書き画像ファイルか
らランダムな画像を作成するコードです。

```python:sample make_numfile
#ライブラリをインポート
import cv2
import numpy as np
import math
import random

# 手書き数字ファイルをもとにランダムな(28,28)データを作成

# ペイントソフトで書いた手書き数字ファイルのディレクトリ名
NUM_FOLDER, NUM_FOLDER1 = 'num_data_0', 'num_data_1'
NUM_FOLDER2, NUM_FOLDER3 = 'num_data_2', 'num_data_3'
NUM_FOLDER4, NUM_FOLDER5 = 'num_data_4', 'num_data_5'
NUM_FOLDER6, NUM_FOLDER7 = 'num_data_6', 'num_data_7'
NUM_FOLDER8, NUM_FOLDER9 = 'num_data_8', 'num_data_9' 

# 元画像と同じ階層にnum_datasetというフォルダを作成してください
NEW_FOLDER = num_dataset
# (28,28)データファイル名
NEW_FILENAME = num_file_

# 手書き数字フォルダごとにデータを作成していきます
# 作成したデータの中から使えないデータを省くのは手動です
for i in range(1): 
    a = NUM_FOLDER # 作成したい数字フォルダを指定する   
    data_list = os.listdir('../' + a)
    n = len(data_list) + 1 #　同ファイル名上書き防止の通しナンバー
    for j in data_list:
        
        # 元ファイルを読み込む
        im = cv2.imread('../' + NUM_FOLDER[:-2] +'/' + str(a[-1]) + '/' + str(j)) 
        
        # 画像をnp配列に変換
        im = np.array(im)
        
        # 画像を太くする前に白黒反転して背景を黒にする
        im = np.bitwise_not(im)
        
        # 8近傍フィルタ
        filt = np.array([[1,1,1],
                        [1,0,1],
                        [1,1,1]],np.uint8)
        
        # 1~10までのランダムな値を作成
        rn = random.randint(0,10)
        
        # rnの値に応じて線を太くする回数を変える
        # 線が細すぎると(28,28)にしたときに何も残らない
        # ペイントソフトで描く時に太目で書いておくと良いかもしれません
        if (rn >= 1 and rn <=3):
            for i in range(0,12): # １２回繰り返す
                im = cv2.dilate(im, filt)
        elif(rn >= 4 and rn <=6):
            for i in range(0,16): # １６回繰り返す
                im = cv2.dilate(im, filt)
        elif(rn >= 6 and rn <=8):
            for i in range(0,18): # １８回繰り返す
                im = cv2.dilate(im, filt)
        elif rn >=9:
            for i in range(0,22): # ２２回繰り返す
                im = cv2.dilate(im, filt)
            im = cv2.dilate(im, filt)
        
        # ランダムな数値の振れ幅を小さくすれば画像がフレームアウトする確率を減らせます
        
        # 画像を回転させる
        h, w = im.shape[:2]
        affine = cv2.getRotationMatrix2D((0,0), random.randint(140,180), 1.0)
        affine = cv2.getRotationMatrix2D((0,0), random.randint(0,40), 1.0)
        im = cv2.warpAffine(im, affine, (w, h))
        
        # 画像を縦横に移動させる
        im = np.roll(im, random.randint(-10,10))
        im = np.roll(im, random.randint(-10,10), axis=1)
        
        # (28,28)にリサイズする
        im = cv2.resize(im, (28, 28))
       
         # 新しいファイルに保存する
        cv2.imwrite('../' + NEW_FOLDER + '/' + NEW_FILENAME + str(a[-1]) + '/' + str(a[-1]) + '_' + str(n) + '.png', gray)
        
        # ファイルに通しナンバーをふる
        n = n + 1
     
```




次は作成した（２８、２８）画像と、それに
対応した正解を格納した配列を作り。それぞ
れをNumPy配列としてテキストファイルで
保存します。保存は __np.save__ を使用します。
使い方は以下の通りです。
```python:sample np.save
np.save(ファイル名, 保存したいNumPy配列)

```

<br>
<br>
<br>
<br>

これで __npy__ ファイルが作成できます。
以下が実際のコードです。

```python:sample
# ラベルファイルとデータファイルを作成

# (28,28)サイズの画像ファイルとフォルダを入れるフォルダ名
NEW_FOLDER = num_dataset 

# (28,28)の画像ファイルが入っている各フォルダ名
datasetfolder_list = ['num_file_0', 'num_file_1' ,'num_file_2',
                      'num_file_3', 'num_file_4', 'num_file_5',
                      'num_file_6', 'num_file_7', 'num_file_8',
                      'num_file_9']

n = [] # 正解ラベルを格納
X_train_data =[] # 画像ファイルを格納

for i in datasetfolder_list:
    file_list = os.listdir('../' + NEW_FOLDER + '/' + str(i))
    for j in file_list:
        
        #各ファイルを読み込む
        img = cv2.imread('../' + NEW_FOLDER + '/' + i + '/' + j)
        print('j: ' + str(j))
        
        # 画像をNumPy配列に変換する
        img = np.array(img)
        
        # 画像ファイルを格納する
        X_train_data.append(img)
        
        # 各ファイル名から正解ラベルを作り格納
        n.append(str(i[-1]))

# 画像のNumpy配列をnp形式で保存する
np.save('../' + NEW_FOLDER + '/dataset_X_train', X_train_data)

# 正解ラベルをnp形式で保存する
np.save('../' + NEW_FOLDER + '/dataset_label',n)


```

 __npy__ ファイルが作成できました。作成し
た２つのファイルとこれまでに学習させて
きたモデルをグーグルコラボラトリにイン
ポートして、更に反転データも作って学習
させます。

グーグルコラボラトリへのインポートはド
ラッグ＆ドロップでOKです。

作成した __npy__ ファイルを読み込むには
 __np.load__ を使用します。使い方は以下の通
りです。


```python:sample np.load

np.load(ファイル名)


```



<br>
<br>
<br>
<br>

以下が実際のコードです。
```python:sample
#np形式画像データを読み込む
my_data = np.load("/content/dataset_X_train.npy")
#np形式正解ラベルデータを読み込む
my_label = np.load("/content/dataset_label.npy")

a = [] #　画像を格納する
for i in my_data:
  j = cv2.cvtColor(i, cv2.COLOR_BGR2GRAY)
  # 画像をグレースケールで読み込み格納
  a.append(j)

# 画像データをNumPy配列に変換
X = np.array(a)
# 正解ラベルデータをNumPy配列に変換
y =  np.array(my_label)

# 画像データのランダムなインデックスを作成
rand_index = np.random.permutation(np.arange(len(X)))

# 画像データをランダムなインデックスで並び変える
X = X[rand_index]
# 正解ラベルをランダムなインデックスで並び替える
y = y[rand_index]

# データを教師データとテストデータに分割する
X_train_mc = X[:int(len(X) * 0.8)]
y_train_mc = y[:int(len(y) * 0.8)]
X_test_mc = X[int(len(X) * 0.8):]
y_test_mc = y[int(len(y) * 0.8):]

# 教師データ画像の白黒反転データを作成
X_train_b_n = cv2.bitwise_not(X_train_mc)
#テストデータ画像の白黒反転データを作成
X_test_b_n = cv2.bitwise_not(X_test_mc)

# 白黒反転した教師画像データを元データと合わせる
X_train = np.append(X_train_mc, X_train_b_n, axis=0)
# 白黒反転したテスト画像データを元データと合わせる
X_test = np.append(X_test_mc, X_test_b_n, axis=0)

# 教師正解ラベルデータを２つ合わせる
y_train = np.append(y_train_mc, y_train_mc, axis=0)
#テスト正解ラベルデータを２つ合わせる
y_test = np.append(y_test_mc, y_test_mc, axis=0)

#教師正解ラベルデータをワンホット値に変換する
y_train = to_categorical(y_train)
#テスト正解ラベルデータをワンホット値に変換する
y_test = to_categorical(y_test)


```

作成したデータセット数は２０１８０で白黒
反転データを加えるので４０３６０セットに
なります。このデータセットを使用してモデ
ルに４００エポック学習させました。学習結
果は以下の通りです。


<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/660ffc4e-b2b2-5e46-59f5-66b0ecedf97d.png">

![400エポック認識結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/ae89869e-3c16-c7ca-a4df-b766179023ab.png)

図：４００エポック学習結果

<br>
<br>
<br>
<br>


左下の __”２”__ を __”３”__ と認識していますが精
度はかなり上がった印象です。更にバッチ数
を変えながら数回に分けて１４１０エポック
学習を行った結果がこちらです


<br>
<br>
<br>
<br>



<img width=500 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/136dabba-aa2c-8dd4-6bec-a24aa689f110.png">


![1810エポック認識結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3368037/8ce638d5-244c-9ec1-4446-2055f18b3b1a.png)

図：１４１０エポック学習結果


<br>
<br>
<br>
<br>

認識が難しそうな数字が並んでいますが全て
正解です。
ペイントソフトで書いたファイルを読み込ま
せて認識させるとかなりの精度で認識できて
います。以上で改良はいったん終了となりま
す。

ペイントソフトで書いた数字を認識させるプ
ログラムですが、スキャナーからの画像とカ
メラからの画像を認識させる部分も一応作っ
て組み込んでいます。

カメラからの画像を処理するのが難しく、影
を除去するためにマスクをかけたり、画面の
暗さに合わせて自動で閾値を変化させたりす
る機能が必要だと感じました。

メインコードの中の、カメラからの画像処理
とスキャナーからの画像処理は改良の余地が
ありますので今後の課題とします。

以下が画像を切り出して数字を認識する全コ
ードです。


```python:sample
import os
from tensorflow.keras.preprocessing import image
import numpy as np
from tensorflow import keras
import cv2
from cv2 import THRESH_BINARY
import math

# 一次的に切り出した画像を保存するフォルダ
NUM_FOLDER = 'trim-num-file' # プログラムの最後でフォルダ内のファイルをすべて消去します

# ここから画像を切り出して予測する関数

#　画像サイズが大きすぎる場合は小さくする
def size_smaller(img):
    if (img.shape[0] > 4000 or img.shape[1] > 3000):
        img = cv2.resize(img, (img.shape[1] // 3, img.shape[0] // 3))
    elif (img.shape[0] > 2000 or img.shape[1] > 1000):
        img = cv2.resize(img, (img.shape[1] // 2, img.shape[0] // 2))               
    img_size = img.shape[0]
    
    return img, img_size


#　背景が白ければ黒に反転する
def  white_to_black(img):                        
    yl = np.mean(img[0:img.shape[0], 0:img.shape[1] // 20])
    yr = np.mean(img[0:img.shape[0], img.shape[1] - img.shape[1] // 20:img.shape[1]])
    xt = np.mean(img[0:img.shape[0] // 20, 0:img.shape[1]])
    xb = np.mean(img[img.shape[0] - img.shape[0] // 20:img.shape[0], 0:img.shape[1]])
    if (yl > 140 and yl <= 255 or yr > 140 and yr <= 255 
        or xt > 140 and xt <= 255 or xb > 140 and xb <= 255):
        img = cv2.bitwise_not(img)
       
    return img


#　背景がノイズの入っていない白か黒か判断する
def noise_detect(img):
    m = []
    yl = np.mean(img[0:img.shape[0], 0:img.shape[1] // 20])
    yr = np.mean(img[0:img.shape[0], img.shape[1] - img.shape[1] // 20:img.shape[1]])
    xt = np.mean(img[0:img.shape[0] // 20, 0:img.shape[1]])
    xb = np.mean(img[img.shape[0] - img.shape[0] // 20:img.shape[0], 0:img.shape[1]])
    yl_min = np.min(img[0:img.shape[0], 0:img.shape[1] // 20])
    yr_min = np.min(img[0:img.shape[0], img.shape[1] - img.shape[1] // 20:img.shape[1]])
    xt_min = np.min(img[0:img.shape[0] // 20, 0:img.shape[1]])
    xb_min = np.min(img[img.shape[0] - img.shape[0] // 20:img.shape[0], 0:img.shape[1]])
    yl_max = np.max(img[0:img.shape[0], 0:img.shape[1] // 20])
    yr_max = np.max(img[0:img.shape[0], img.shape[1] - img.shape[1] // 20:img.shape[1]])
    xt_max = np.max(img[0:img.shape[0] // 20, 0:img.shape[1]])
    xb_max = np.max(img[img.shape[0] - img.shape[0] // 20:img.shape[0], 0:img.shape[1]])
    m_min = [yl_min, yr_min, xt_min, xb_min]
    m_max = [yl_max, yr_max, xt_max, xb_max]
    if (yl == 255 and yr == 255 and xt == 255 and xb == 255):
        screen = "w"
        d_lebel = 255
        d_min = np.min(m_min)
        d_max = np.max(img)
    elif (yl == 0 and yr == 0 and xt == 0 and xb == 0):
        screen = "b"
        d_lebel = 0
        d_min = np.min(m_min)
        d_max = np.max(img)
    else:
        screen = "c"
        d_lebel = (yl + yr + xt + xb) // 4
        d_min = np.min(m_min)
        d_max = np.max(img)
        
    return img, screen, d_lebel, d_min, d_max, xb_min


#　画像にフレーム状のマスクをかけて主に四隅の影によるノイズを除去する
def mask(img, screen):              
    if screen == 'w': 
        print('w')
        return img
    elif screen =='b':
        #　画像の横サイズを変数に格納
        x_shape = img.shape[1]  
        #　画像の縦サイズを変数に格納
        y_shape = img.shape[0] 
        #　画像横サイズの１５％を変数に格納しマスクの位置調節で使用
        bold_x = img.shape[1] // 15
        #　画像縦サイズの１５％を変数に格納しマスクの位置調節で使用
        bold_y = img.shape[0] // 15
        #　画像横サイズの1/3を変数に格納しマスクの幅調整で使用
        divided_3 = img.shape[1] // 3
        #　画像縦サイズの1/2を変数に格納
        h = img.shape[0] // 2
        #　画像横サイズの1/2を変数に格納
        w = img.shape[1] // 2
        #　画像の横幅の半分を円の半径とする
        r = img.shape[1] // 2        
        #　時間短縮の為のステップ数を設定
        step = 15 
        #　右下のマスク                  
        for i in range(0, 90, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[y + h + bold_y:, x + w - bold_x:]  = 0  
        #　左下のマスク
        for i in range(90, 180, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[y + h + bold_y:, :x + w + bold_x]  = 0
        #　左上のマスク
        for i in range(180, 270, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[:y + w + bold_x, :x + w + bold_x]  = 0    
        #　右上のマスク
        for i in range(270, 360, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[:y + w + bold_x, x + w - bold_x:]  = 0    
        #　フレームの継ぎ目のマスク
        for i in range(0, bold_x, step):
             img[divided_3:img.shape[0] - divided_3, 0:i] = 0
             img[divided_3:img.shape[0], img.shape[1] - bold_x:]  = 0
           
        return img


    elif screen == 'c':
        #　画像の横サイズを変数に格納
        x_shape = img.shape[1]   
        #　画像の縦サイズを変数に格納
        y_shape = img.shape[0] 
        #　画像横サイズの１５％を変数に格納しマスクの位置調節で使用
        bold_x = img.shape[1] // 15
        #　画像縦サイズの１５％を変数に格納しマスクの位置調節で使用
        bold_y = img.shape[0] // 15
        #　画像横サイズの1/3を変数に格納しマスクの幅調整で使用
        divided_3 = img.shape[1] // 3
        #　画像縦サイズの1/2を変数に格納
        h = img.shape[0] // 2
        #　画像横サイズの1/2を変数に格納
        w = img.shape[1] // 2
        #　画像の横幅の半分を円の半径とする
        r = img.shape[1] // 2        
        #　背景が白かったら黒にする
        img = white_to_black(img)
        #　時間短縮の為のステップ数を設定
        step = 15 
        #　右下のマスク                  
        for i in range(0, 90, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[y + h + bold_y:, x + w - bold_x:]  = 0  
        #　左下のマスク
        for i in range(90, 180, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[y + h + bold_y:, :x + w + bold_x]  = 0
        #　左上のマスク
        for i in range(180, 270, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[:y + w + bold_x, :x + w + bold_x]  = 0    
        #　右上のマスク
        for i in range(270, 360, step):
            x = int(math.cos(math.radians(i)) * r)
            y = int(math.sin(math.radians(i)) * r)
            img[:y + w + bold_x, x + w - bold_x:]  = 0    
        #　フレームの継ぎ目のマスク
        for i in range(0, bold_x, step):
             img[divided_3:img.shape[0] - divided_3, 0:i] = 0
             img[divided_3:img.shape[0], img.shape[1] - bold_x:]  = 0
           
        return img


#　キャンバスの暗さに合わせて値を引き陰によるノイズを目立たなくする 
def smoothing_darkness(img, d_lebel):
    d = int(abs(d_lebel * 0.76))#　影カット閾値
   
    if img.shape[0] != 28:
        #　白の背景がもし暗かったら明るく修正する、キャンバスの大きさに合わせて判断する範囲を調整する 
        if(np.mean(img[0:img.shape[0], 0:img.shape[0] // 12]) > 200 and 
        np.mean(img[0:img.shape[0], 0:img.shape[0] // 12]) < 240):
           img = img - d 
        elif(np.mean(img[0:img.shape[0], img.shape[1] - img.shape[0] // 12:img.shape[1]]) > 200 and 
        np.mean(img[0:img.shape[0], img.shape[1] - img.shape[0] // 12:img.shape[1]]) < 240):
            img = img - d
        elif(np.mean(img[0:img.shape[0]//12,0:img.shape[1]])>200 and 
        np.mean(img[0:img.shape[0] // 12, 0:img.shape[1]]) < 240):
            img = img - d
        elif(np.mean(img[img.shape[0] - img.shape[0] // 12:img.shape[0], 0:img.shape[1]]) > 200 and 
        np.mean(img[img.shape[0] - img.shape[0] // 12:img.shape[0], 0:img.shape[1]]) < 240):
            img = img - d
        elif(np.mean(img[0:img.shape[0], 0:img.shape[0] // 12]) > 180 and 
        np.mean(img[0:img.shape[0], 0:img.shape[0] // 12]) <= 200):
           img = img - d
        elif(np.mean(img[0:img.shape[0], img.shape[1] - img.shape[0] // 12:img.shape[1]]) > 180 and 
        np.mean(img[0:img.shape[0], img.shape[1] - img.shape[0] // 12:img.shape[1]]) <= 200):
            img = img - d
        elif(np.mean(img[0:img.shape[0] // 12, 0:img.shape[1]]) > 180 and 
        np.mean(img[0:img.shape[0] // 12, 0:img.shape[1]]) <= 200):
            img = img - d
        elif(np.mean(img[img.shape[0] - img.shape[0] // 12:img.shape[0], 0:img.shape[1]]) > 180 and 
        np.mean(img[img.shape[0] - img.shape[0] // 12:img.shape[0], 0:img.shape[1]]) <= 200):
            img = img - d

    return img 


# 膨張  
def dilate_img(img, screen):
    # 背景が白で膨張をすると文字が見えなくなる
    # screenはノイズのない背景が白のデータの場合"w",黒の場合"b"を返す
    # 画面が暗い場合は"c"を返す                       
    # ８近傍フィルタ
    filt = np.array([[1,1,1],
                    [1,0,1],
                    [1,1,1]],  np.uint8)
    if screen == "w": # 背景が白ならば反転して２回膨張処理を行う
        img = cv2.bitwise_not(img)   
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt)
        screen = 'b'
    elif screen == 'b':
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt)
    else: # 反転無しで２回膨張処理を行う
        #img = cv2.bitwise_not(img)
        img = cv2.dilate(img, filt)
        img = cv2.dilate(img, filt) 
        screen = 'b'
    return img, screen


def erode_img(img, screen):
    # ノイズのない背景が白のデータの場合"w",黒の場合"b"を返す関数で判定        
    # ８近傍フィルタ
    filt = np.array([[1,1,1],
                    [1,0,1],
                    [1,1,1]], np.uint8)
    if screen == "w": # 背景が白ならば反転して２回収縮処理を行う
        img = cv2.bitwise_not(img) 
        img = cv2.erode(img, filt) 
        screen = 'b'
    elif screen == 'b':
        img = cv2.erode(img, filt)
    else: # 反転無しで２回収縮処理を行う
        img = cv2.erode(img, filt)
        screen = 'b'

    return img, screen


# 輪郭を抽出する
def f_contours(img):      
    
    #　背景が白い場合画像の枠が輪郭としてカウントされるので処理する
    img = white_to_black(img)
    
    # グレースケール
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # ２値化
    _, th_img = cv2.threshold(gray, 130, 255, THRESH_BINARY)
    
    # 輪郭を抽出する
    contours, _ = cv2.findContours(th_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    # 輪郭のx,y,w,hを格納するリストを作成
    
    n = [] # 輪郭の位置
    p = [] #
    t = 0 #
    thresh = 0.021 #0.028～0.021ノイズカット閾値

    # 輪郭を格納する
    for i in contours: 
       x, y, w, h = cv2.boundingRect(i)
       
       # 縦横比が１０倍以上は削除する（ノイズカット）
       if (w // h > 0.1 and h // w < 10 or w // h < 10 and h // w > 0.1):   
           # 画像サイズの2.1％以下の輪郭は削除する（ノイズカット）
           if (w > img.shape[1] * thresh and h > img.shape[0] * thresh): 
                n.append([x, y, w, h])
    
                # 抽出した輪郭を左上から順番に並べる
    num_location = sorted(n, key=lambda x:(x[1] // (img.shape[1] // 3), x[0]))
   
    # 外接矩形を描画した画像と座標を返す   
    return img, num_location


# 切り出した画像を一旦ファイルに保存して認識した結果をリストに保存する
def create_num_file(img, num_location, screen):
    
    # 切り出し他画像の一時保存ディレクトリ名
    dir_path = './' + NUM_FOLDER + '/'   
    
    # 切り出す数字の余白設定
    if img.shape[0] <= 28: # 画像サイズが(28,28)以下は余白無し
        m1, m2, m3, m4 = 0, 0, 0, 0
    elif img.shape[0] <= 50: # 画像サイズが(50,50)以下は余白1
        m1, m2, m3, m4 = 1, 1, 1, 1
    else: # 画像サイズが大きい場合の余白は10
        m1, m2, m3, m4 = 10, 10, 10, 10
    
    # 画像をファイルに保存する
    if img.shape[0] <= 28 or img.shape[1] <= 28: # (28,28)の場合はそのままファイルに保存する  
        cv2.imwrite(os.path.join(NUM_FOLDER, "num0" + ".png"), img)
    else: 
        # 画像サイズが大きければ数字の数だけ画像を切り出してファイルを保存する 
        for i in range(0, len(num_location)):
            x, y, w, h = num_location[i]  
            
            # 画像を正方形に切り取る
            n1, n2 = abs((h - w) // 2), abs((h - w) // 2)

            # エラーにならない為の処理    
            if y - m1 < 0: # 切り取る幅が画像の上をオーバー
                m1 = 0
            if y + h + m2 > img.shape[0]: # 切り取る幅が画像の下をオーバー 
                m2 = 0
            if x - n1 - m3 < 0: # 切り取る幅が画像の左端をオーバー
               m3 = 0 
               n1 = 0
            if x + w + n2 + m4 > img.shape[1]: # 切り取る幅が画像の右端をオーバー 
               m4 = 0
               n2 = 0
            
            # 画像を余白を付けて切り出す
            trim_img = img[y - m1:y + h + m2, x - n1 - m3:x + w + n2 + m4]

            # 切り出された画像の大きさに応じて線を太くする
            if h > img.shape[0] * 0.9 and w > img.shape[1] * 0.9:
                #　大きい画像は２度関数を使用
                trim_img, screen = dilate_img(trim_img, screen) 
                trim_img, screen = dilate_img(trim_img, screen)
                # 切り出した画像をそれぞれファイルに保存する
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
            elif h > img.shape[0] * 0.5 and w > img.shape[1] * 0.5:
                # 元画像の半分程度の画像は１度関数を使用
                trim_img, screen = dilate_img(trim_img, screen)
                # 切り出した画像をそれぞれファイルに保存する
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
            else: 
                # 切り出した画像をそれぞれファイルに保存する
                cv2.imwrite(os.path.join(NUM_FOLDER, 'num' + str(i) + '.png'), trim_img)
    return dir_path    


def from_paint(img, screen):
    # グレースケール化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # matplotlibで表示できるように加工するペイントで作ったPNGはこれが必要
    gray1 = cv2.cvtColor(gray, cv2.COLOR_BGR2RGB)
    # ２値化
    print(img.shape)
    _, th_img = cv2.threshold(gray1, 130, 255, THRESH_BINARY)
    # ２度膨張処理を行う
    if (img.shape[0] <= 50 or img.shape[1] <= 50):
        d_img = th_img
    else:
        d_img, screen = dilate_img(th_img, screen) 
    # ２度膨張処理を行う
    if (img.shape[0] <= 500 or img.shape[1] <= 500):
        d1_img = d_img
    else:
        d1_img, screen = dilate_img(d_img, screen)
    # ２度膨張処理を行う
    if (img.shape[0] <= 100 or img.shape[1] <= 100):
        d2_img = d1_img
    else:
        d2_img, screen = dilate_img(d1_img, screen)
    # 輪郭を抽出する
    c_img, num_location = f_contours(d2_img)           
    # 画像を切り出してファイルに保存する
    dir_path = create_num_file(c_img, num_location, screen) 

    return dir_path


def from_camera(img, screen, d_min, d_max, xb_min):                  
    # 四隅のノイズを除去する
    m_img = mask(img, screen)
    # グレースケール化
    gray = cv2.cvtColor(m_img, cv2.COLOR_BGR2GRAY)
    # matplotlibで表示できるように加工するペイントで作った画像はこれが必要
    p_img = cv2.cvtColor(gray, cv2.COLOR_BGR2RGB)
    # ２値化
    _, th_img = cv2.threshold(p_img, 110, 255, THRESH_BINARY)#130  薄い画像は２０
    # ２度膨張処理を行う
    if (img.shape[0] <= 160 or img.shape[1] <= 160):
        d_img = th_img
    else:
        d_img, screen = dilate_img(th_img, screen)
    # 四隅のノイズを除去する
    if (img.shape[0] > 1000 and img.shape[1] >1000):
        m1_img = mask(d_img, screen)
    else:
        m1_img = d_img
    # 輪郭を抽出する
    c_img, num_location = f_contours(m1_img)
    # 画像を切り出してファイルに保存する
    dir_path = create_num_file(c_img, num_location, screen)

    return dir_path


def from_scaner(img, screen):
    # 四隅のノイズを除去する
    m_img = mask(img, screen)
    # グレースケール化
    gray = cv2.cvtColor(m_img, cv2.COLOR_BGR2GRAY)
    # matplotlibで表示できるように加工するペイントで作ったPNGはこれが必要
    p_img = cv2.cvtColor(gray, cv2.COLOR_BGR2RGB)
    # ２値化
    _, th_img = cv2.threshold(p_img, 130, 255, THRESH_BINARY)
    # ２度膨張処理を行う
    if (img.shape[0] <= 50 or img.shape[1] <= 50):
        d_img = th_img
    else:
        d_img, screen = dilate_img(th_img, screen)
    # 収縮処理を行う
    if (img.shape[0] <= 100 or img.shape[1] <= 100):
        e_img = d_img
    else:
        e_img, screen = erode_img(d_img, screen)
    # 四隅のノイズを除去する
    m1_img = mask(e_img, screen)
    # ２度膨張処理を行う
    if (img.shape[0] <= 100 or img.shape[1] <= 100):
        d1_img = m1_img
    else:
        d1_img, screen = dilate_img(m1_img, screen)
    # 輪郭を抽出する
    c_img, num_location = f_contours(d1_img)           
    # 画像を切り出してファイルに保存する
    dir_path = create_num_file(c_img, num_location, screen)    

    return dir_path


#　ここから画像を受け取り切り出して予測する

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

# 画像を読み込む
img = cv2.imread('./' + filepath)

#　画像をNumPy配列に変換
img = np.array(img)

# 処理速度向上のために画像サイズを小さくする
_ ,img_size = size_smaller(img)

# 主にカメラやスキャナー用
img, screen, d_lebel, d_min, d_max, xb_min = noise_detect(img)

# 暗い部分のある画像をスムーズにする
lite_img = smoothing_darkness(img, d_lebel)

#　ペイント画像、カメラ画像、スキャナで処理を分ける 
if img_size != 28 and d_lebel == 0 or d_lebel == 255:
    # ペイントからの画像を処理
    dir_path = from_paint(lite_img, screen)
elif (img_size != 28 and d_lebel > 160 and d_lebel <190 or d_lebel >3 and d_lebel < 30):
    # カメラから画像を処理
    dir_path = from_camera(lite_img, screen, d_min, d_max, xb_min)
elif (img_size != 28 and d_lebel >= 190 and d_lebel <255):
    # スキャナーからの画像を処理 
    dir_path = from_scaner(lite_img, screen)  
elif img_size == 28: #(28,28)サイズの画像は複数文字認識無し
    # 輪郭を抽出する
    _, num_location = f_contours(lite_img)           
    # 画像を切り出してファイルに保存する
    dir_path = create_num_file(lite_img, num_location, screen)


p = [] # 認識結果を格納するリスト
q = 0 # 切り出して書き込んだファイル数をカウントする

# 指定のディレクトリ内のファイルを読み込み数字を認識する
for i in os.listdir(dir_path):
    # 切り取った画像を読み込みむ
    img = cv2.imread(dir_path + i)
    # グレースケール化
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    #(28,28)にリサイズ            
    img = cv2.resize(img,(image_size,image_size))
    img = image.img_to_array(img)
    data = np.array([img]) 
    
    # 変換したデータをモデルに渡して予測する
    result = model.predict(data)[0] 
    
    # リストに認識結果を格納する
    if np.max(result) < 0.5: # 認識結果が0.5より小さい場合は(*)を表示する
        p.append('(*)')
    elif np.max(result) >= 0.5: # 認識結果が0.5以上の場合は認識結果を格納する
        predicted = result.argmax()
        p.append(classes[predicted])          
    q = q + 1           
pred_answer = "これは " + ''.join(p) + " です"
print(str(pred_answer))
p=[] # リストをリセットする  

# dir_pathフォルダ内の切り出した数字ファイルを削除します
for i in range(q): #書き込んだファイル分だけ消去する
    # dir_path内の一番初めのファイルを削除
    # gitkeepなどのファイルを入れた場合はインデックスは１にする
    os.remove(dir_path + os.listdir(dir_path)[0])

```

このプログラムファイルと同じ階層に切り出
した画像を一時的に保存するtrim-num-fileフォル
ダを作成して、数字画像のファイルパスを手
動で指定すると、認識結果が表示されます。モデ
ルはご自身で作成したものをロードしてください。

 ## まとめ
今回改良を行ってみて分かったことは
●暗い画像は認識させるのが難しい
●ペイントソフトで書いた数字は認識させ易い
●学習データの量を増やすと認識精度がアップ
するという事です。
今後はこれをベースとして改良して、今後帳
票などの紙データの入力支援に使うアプリを
作成したいと思います。最後までご覧いただ
きありがとうございました。