Keras + OpenCV で画像認識による簡易的なアンケート読み取りシステムを作ってみた②

##この記事について
飲食店のテーブルなどに置いてあるアンケート用紙。そのアンケートの集計はお店のスタッフがExcelなどの表計算ツールに打ち込んで集計していたりします。こういう非効率な作業を自動化できないかと思って、OCRに関する知識が全くない状態でアンケート用紙を読み取って自動集計する簡易的なシステムを作って見ました。この記事はその備忘録です。


システムの概要については前回の記事をご覧ください。

[Keras + OpenCV で画像認識による簡易的なアンケート集計システムを作ってみた①](https://qiita.com/ba--shi/items/d22ebdd10fa55f2ad161)

前回の記事ではOCR処理の対象となるアンケート項目を画像として抽出するまでの処理を行いました。ここからは、切り抜いたアンケート項目の記入数値を判定する処理を実装してみます。


##深層学習で画像認識モデルを作成する

ディープラーニングの手法の１つである畳み込みニューラルネットワーク(CNN：Convolutional Neural Network)を使って画像認識モデルを作成しました。

具体的には以下の手順で進めました。

1. 学習用に画像を用意する
2. 画像の水増しを行う
3. 学習モデルの作成&評価を行う
4. KerasモデルをTensorFlowモデルに変換する

###開発環境
[ハードウェア]
　OS： Windows10 Home 64bit
　CPU： Intel Core i7
　GPU： NVIDIA Geforce GTX 1070 8GB
　HDD： 1TB
　
[ソフトウェア]
　Python： 3系
　Tensorflow： 1.14.0
　Keras： 2.2.4
　Cuda： 8.0
　cuDNN： 5.1
　OpenCV： 2系


###1. 学習用に画像を用意する
まず5点尺度のみ、11点尺度のみを記載したアンケート用紙をそれぞれ用意し、各数値にひたすらマルを記入していきました。記入する際には、学習モデルの精度を上げるためにあらかじめ想定される記入パターンを洗い出しておきました。

![cnn0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/470b8a48-cc3b-d8a9-418e-2a66fd1c4597.png)


そして、それに基づいてひたすら下記のアンケート用紙に記入していきました。

![cnn1-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/ecdfbfc5-7c45-fda8-c241-7d74e9b723df.png)

各数値ごとに50枚ずつ記入し、各アンケート用紙をスマホで撮影していきました。また、数値が未記入の場合にも対応させるために未記入のアンケートも撮影しました。
前回の記事でアンケート用紙の撮影画像からアンケート項目を切り出す処理を実装していたので、この処理方法で撮影画像から各アンケート項目を切り抜いていきました。(切り抜く際には画像サイズを400×30に小さくしました。変更理由はCNNによる学習モデルを作成する際にメモリ不足になるのを防ぐためです。)
切り抜いた画像は下記のフォルダ構成で記入数値ごとにフォルダを分け、各フォルダごとに約500枚の画像を保存しました。("None"というフォルダには未記入のアンケートを保存)

![cnn2-min (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/61eeab1d-63c0-7eb2-9cbc-92f93e42699e.png)


###2. 画像の水増しを行う
画像の水増しは最初考慮していませんでした。当初は1の画像数で後工程の学習モデルの作成を行なってみたのですが全く精度が上がりませんでした(正解率50%以下)。そこで、精度を上げるために画像の水増しを行い学習データを増やすことにしました。

画像の水増しにはKerasのImageDataGeneratorクラスを使用しました。

####ImageDataGeneratorクラス
画像に対して変換処理(反転、拡大、縮小など)を加えることで、学習データの「水増し」を行うことができます。このクラスは下記のオプションを設定することでどのような変換処理を加えるか指定することができます。


|オプション|内容|
|---|---|
|rotation_range|画像を回転させる角度の上限|
|width_shift_range|ランダムに水平方向にシフトを行う範囲を指定|
|height_shift_range|ランダムに垂直方向にシフトを行う範囲を指定|
|horizontal_flip|水平方向にランダムで反転させる|
|vertical_flip	|垂直方向にランダムで反転させる|
|shear_range	|ランダムに引っ張る角度の範囲を指定|
|zoom_range	|ランダムにズームする範囲を指定|

今回実装したコードは以下です。

```python

import os
import glob
import numpy as np
from keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array, array_to_img


def generate_images(generator, x, dir_name, index):
    save_name = 'extened-' + str(index)
    g = generator.flow(x, batch_size=1, save_to_dir=output_dir, save_prefix=save_name, save_format='jpg')
    # 1枚の画像から3枚拡張する
    for i in range(3):
        bach = g.next()


for count in range(0, 10):
    # 出力先ディレクトリの設定
    output_dir = "extend_scale1/" + str(count)
    if not(os.path.exists(output_dir)):
        os.mkdir(output_dir)
    # フォルダ内の全画像のパスを取得
    images = glob.glob(os.path.join('scale1/' + str(count), "*.jpg"))
    # ImageDataGeneratorの設定
    generator = ImageDataGenerator(
                    width_shift_range=0.001, # 水平方向にランダムでシフト
                    shear_range=0.001, # 斜め方向(pi/10まで)に引っ張る
                    )
    # 読み込んだ画像を拡張していく
    for i in range(len(images)):
        img = load_img(images[i])
        x = img_to_array(img)
        x = np.expand_dims(x, axis=0)
        generate_images(generator, x, output_dir, i)
```
このコードを実行することで学習用画像を3倍に増やしました。



###3. 学習モデルの作成&評価を行う
学習用のデータは用意できたので、いよいよ学習モデルを作成します。
ここでは深層学習の畳み込みニューラルネットワーク(CNN)という画像認識に使われる手法を採用しました。

####深層学習(Deep Learning)
深層学習(Deep Learning)とは、ニューラルネットワークの中間層を多層に組み合わせたものをいいます。
ニューラルネットワークとは下図のように入力層、中間層、出力層といった構成要素から成るネットワークのことで、x1,x2,x3といったデータを渡すことでy1,y2,y3といった出力結果を取得する仕組みです。ネットワークの途中過程ではwという重みを掛け合わせ、活性化関数を経由することで出力結果を調整します。このwは最初は明らかではありませんが、入力xと出力yの既知のデータから求めることができます(誤差逆伝播法)。
wが明らかになれば、別の入力データxから出力すべきyを予測することができます。

![cnn3-min (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/82740c6c-12ef-f240-9139-336271b7be4e.png)


####畳み込みニューラルネットワーク(CNN)
上記のようなニューラルネットワークに画像データを渡す場合は、その画素数やチャネル数に関係なく１次元のデータにする必要があります。例えば、白と黒で構成される3×3画素のモノクロ画像の場合は以下のようになります。

![cnn4-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/f65f355e-bf97-7549-c43d-4f0e5929d197.png)

カラー画素の場合も、RGB３チャネルにはなりますがモノクロ画像と同じように１次元の入力データになります。

![cnn5-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/b3c3bebf-1308-0afa-8694-fbd5dbcf7019.png)

この方法では画像データが一列で表現されてしまうため下記のような空間的な情報は考慮されなくなってしまいます。

　・空間的に近い画素は似たような値をとる
　・RGBの各チャネル間には密接な関連性がある
　・距離の離れた画素同士には、あまり関連性がない

こういった情報も含めておくことができるのが畳み込みニューラルネットワーク(CNN)です。CNNは「畳み込み層」と「プーリング層」を含むニューラルネットワークになります。

![cnn10-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/e89d62e7-930d-cc70-3064-1278ce39c140.png)


####畳み込み層

畳み込み層では、認識対象となるオブジェクトの特徴をまとめたいくつものフィルタを使って畳み込み演算を行います。

畳み込み演算とは、入力データ(画像)に対してフィルタを一定の間隔でスライドさせながら、フィルタの数値と入力データの数値を乗算し、その総和を求めるというものです。

このフィルタを使用することで、点ではなく領域を考慮した特徴抽出が可能になり、画像によってオブジェクトの位置や形が多少異なっていても特徴を抽出することが可能となります。


例えば、下記のような「×」マークを模した白黒の二値画像に対して右のフィルタを使った畳み込み演算を行ってみます。

![cnn6-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/6332a00e-8a51-f901-05e0-b272e3ff76fb.png)

フィルタを１ピクセルずつスライドさせながら対応するマス同士の乗算の総和を出力していきます。

![cnn7-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/320d3ee6-fc04-cc5f-5012-d55deb8a0f90.png)

フィルタを最後までスライドさせて出力されたものを「特徴マップ」といいます。

![cnn8-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/ce70d388-d32d-a81e-4d52-3d230a3aeb35.png)

畳み込み層では同じ演算処理をフィルタの数だけ行います。フィルタの値は重み(w)であり、既知の入力データ(画像)と出力データ(画像に写っているものが何か)を使った学習を行うことで値が設定されます。


####プーリング層

プーリング層では、入力データの特徴を絞り込むことでより扱いやすい形に変形します。
畳み込み層に比べると演算方法は単純であらかじめ決めておいた範囲内における最大値や平均値を出力するだけです。

例えば、先ほどの特徴マップにプーリング処理(範囲内の最大値を出力するMaxPooling)を行うと以下のような出力結果になります。

![cnn9-min.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/faba27d8-e91c-2038-04c9-cdaa00ba70a7.png)

このようにすると元データの特徴を維持しながら、圧縮することができます。
そのため、計算コストを下げることができる上に微妙な位置変化に対しても頑健になります。



これら「畳み込み層」「プーリング層」を何層にも重ねることで画像の特徴を抽出していきます。そして最終的に各フィルタの特徴を１次元のデータに変換することで認識結果を出力していきます。


####実装コード
まず、５段階尺度のアンケートの判別を行うための学習モデルを構築していきました。構築していく過程で

・フィルタサイズやフィルタ数を調整
・過学習を防ぐためにドロップアウト層を追加
・Batch Normalizationの追加

といった対応を行い、最終的にはテストデータに対する予測精度が最も高かった下図の学習モデルを構築しました。

![cnn11-min (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/2e41d3a4-78db-1e61-d57a-9b3b30611000.png)


以下、実装コード

```python
import tensorflow as tf
import os
import keras
from keras.utils.np_utils import to_categorical
from keras.utils import np_utils
import numpy as np
import cv2
from PIL import Image
from keras.layers.normalization import BatchNormalization
from keras.models import Sequential
from keras.layers.convolutional import Convolution2D, MaxPooling2D
from keras.layers.core import Dense, Dropout, Activation, Flatten
from keras.preprocessing.image import array_to_img, img_to_array
from keras.callbacks import EarlyStopping, CSVLogger
from keras import backend as K
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split

image_list = []
label_list = []

for dir in os.listdir("C:/Users/~/extend_scale2"):
    if dir == ".DS_Store":
        continue
    img_dir = "C:/Users/~/extend_scale2/" + dir
    # フォルダごとにラベル分けする
    if dir == "none":
        label = 0
    if dir == "1":
        label = 1
    elif dir == "2":
        label = 2
    elif dir == "3":
        label = 3
    elif dir == "4":
        label = 4
    elif dir == "5":
        label = 5
    for file in os.listdir(img_dir):
        if file != ".DS_Store":
            # 配列label_listに正解ラベルを追加
            label_list.append(label)
            # 配列image_listに画像の配列データを追加
            filepath = img_dir + "/" + file
            img = cv2.imread(filepath, 0)
            image = img_to_array(img)
            image_list.append(image)


# パラメータ
nb_classes = 6  # 分類するクラス数
nb_epoch = 50   # 最適化計算のループ回数
batch_size = 10 # バッチサイズ

# Numpy配列に変換
image_list = np.asarray(image_list)
label_list = np.asarray(label_list)

X = image_list.astype('float32')

# 画像データ（X）には0〜255の画素値が入っているため0.0〜1.0に正規化
X = X / 255.0

# 正解クラスをone-hot-encoding
Y = np_utils.to_categorical(label_list, nb_classes)

# 学習用データ(67%)とテストデータ(33%)に分ける
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.33, random_state=111)

# 入力データの形状を設定(二値画像のため1チャネル)
img_rows, img_cols = 30, 400
input_shape = (img_rows, img_cols,1)


# 学習モデルを定義
model = Sequential()
model.add(Convolution2D(input_shape=input_shape, filters=60, kernel_size=(20, 20), strides=(1, 1), padding="same"))
model.add(Activation("relu"))
model.add(BatchNormalization())
model.add(Convolution2D(filters=96, kernel_size=(10, 10), strides=(1, 1), padding="same"))
model.add(Activation("relu"))
model.add(BatchNormalization())
model.add(Convolution2D(filters=96, kernel_size=(8, 8), strides=(1, 1), padding="same"))
model.add(Activation("relu"))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Convolution2D(filters=128, kernel_size=(4, 4), strides=(1, 1), padding="same"))
model.add(Activation("relu"))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten()) # 全結合層入力のためのデータの一次元化する
model.add(Dense(512))
model.add(Activation("relu"))
model.add(BatchNormalization())
model.add(Dropout(0.3))
model.add(Dense(128))
model.add(Activation('relu'))
model.add(BatchNormalization())
model.add(Dropout(0.3))
model.add(Dense(6))
model.add(Activation('softmax'))
model.summary()

# モデルのコンパイル
model.compile(loss="categorical_crossentropy", optimizer="adadelta", metrics=["accuracy"])

es = EarlyStopping(monitor='val_loss', patience=2)
csv_logger = CSVLogger('training.log')

hist = model.fit(x_train, y_train,
                 batch_size=batch_size,
                 epochs=nb_epoch,
                 verbose=1,
                 validation_split=0.1,
                 callbacks=[es, csv_logger])

# モデルの評価を行う (返り値:モデルの予測精度)
score = model.evaluate(x_test, y_test, verbose=0)
print('test loss:', score[0])
print('test acc:', score[1])

# plot results
loss = hist.history['loss']
val_loss = hist.history['val_loss']

epochs = len(loss)
plt.plot(range(epochs), loss, marker='.', label='loss')
plt.plot(range(epochs), val_loss, marker='.', label='val_loss')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()


# モデルを保存
model.save("C:/Users/~/scale2_predict_model.h5")
```

以下、出力結果

```
name: GeForce GTX 1070
major: 6 minor: 1 memoryClockRate (GHz) 1.683
pciBusID 0000:01:00.0
Total memory: 8.00GiB
Free memory: 6.64GiB
2020-12-26 15:46:30.235852: I c:\tf_jenkins\home\workspace\release-win\m\windows-gpu\py\35\tensorflow\core\common_runtime\gpu\gpu_device.cc:961] DMA: 0
2020-12-26 15:46:30.238814: I c:\tf_jenkins\home\workspace\release-win\m\windows-gpu\py\35\tensorflow\core\common_runtime\gpu\gpu_device.cc:971] 0:   Y
2020-12-26 15:46:30.242432: I c:\tf_jenkins\home\workspace\release-win\m\windows-gpu\py\35\tensorflow\core\common_runtime\gpu\gpu_device.cc:1030] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1070, pci bus id: 0000:01:00.0)
2020-12-26 15:46:30.879476: I c:\tf_jenkins\home\workspace\release-win\m\windows-gpu\py\35\tensorflow\core\common_runtime\gpu\gpu_device.cc:1030] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1070, pci bus id: 0000:01:00.0)
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 30, 400, 60)       24060
_________________________________________________________________
activation_1 (Activation)    (None, 30, 400, 60)       0
_________________________________________________________________
batch_normalization_1 (Batch (None, 30, 400, 60)       240
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 30, 400, 96)       576096
_________________________________________________________________
activation_2 (Activation)    (None, 30, 400, 96)       0
_________________________________________________________________
batch_normalization_2 (Batch (None, 30, 400, 96)       384
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 30, 400, 96)       589920
_________________________________________________________________
activation_3 (Activation)    (None, 30, 400, 96)       0
_________________________________________________________________
batch_normalization_3 (Batch (None, 30, 400, 96)       384
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 15, 200, 96)       0
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 15, 200, 128)      196736
_________________________________________________________________
activation_4 (Activation)    (None, 15, 200, 128)      0
_________________________________________________________________
batch_normalization_4 (Batch (None, 15, 200, 128)      512
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 7, 100, 128)       0
_________________________________________________________________
flatten_1 (Flatten)          (None, 89600)             0
_________________________________________________________________
dense_1 (Dense)              (None, 512)               45875712
_________________________________________________________________
activation_5 (Activation)    (None, 512)               0
_________________________________________________________________
batch_normalization_5 (Batch (None, 512)               2048
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_2 (Dense)              (None, 128)               65664
_________________________________________________________________
activation_6 (Activation)    (None, 128)               0
_________________________________________________________________
batch_normalization_6 (Batch (None, 128)               512
_________________________________________________________________
dropout_2 (Dropout)          (None, 128)               0
_________________________________________________________________
dense_3 (Dense)              (None, 6)                 774
_________________________________________________________________
activation_7 (Activation)    (None, 6)                 0
=================================================================
Total params: 47,333,042
Trainable params: 47,331,002
Non-trainable params: 2,040
_________________________________________________________________
Train on 13212 samples, validate on 1469 samples
Epoch 1/50
13212/13212 [==============================] - 389s 29ms/step - loss: 0.1170 - acc: 0.9623 - val_loss: 8.3242 - val_acc: 0.2587
Epoch 2/50
13212/13212 [==============================] - 386s 29ms/step - loss: 0.0173 - acc: 0.9957 - val_loss: 0.3054 - val_acc: 0.8816
Epoch 3/50
13212/13212 [==============================] - 387s 29ms/step - loss: 0.0108 - acc: 0.9973 - val_loss: 2.6781e-05 - val_acc: 1.0000
Epoch 4/50
13212/13212 [==============================] - 386s 29ms/step - loss: 0.0087 - acc: 0.9975 - val_loss: 4.8810 - val_acc: 0.5303
Epoch 5/50
13212/13212 [==============================] - 386s 29ms/step - loss: 0.0083 - acc: 0.9975 - val_loss: 3.6730e-05 - val_acc: 1.0000

test loss: 0.0001827530876174434
test acc: 0.9913174490688059
```

テストデータによる検証では99%の精度を示しました。

同じ要領で、11段階尺度のアンケートの学習モデルも作成しました。こちらのモデルはテストデータに対する検証で最も高い98.5%の予測精度を示したモデルを採用しました。


###4. KerasモデルをTensorFlowモデルに変換する
学習モデルを構築したので、実際に未知の画像に対する予測を行っていきたいのですが、この学習モデルはKerasで構築されている( .h5ファイル)ため、TensorFlowモデル( .pbファイル)に変換します。

なぜ、変換するかというと処理速度の向上を行うためです。[こちらの記事](https://qiita.com/yukiB/items/1ea109eceda59b26cd64)にあるようにKerasとTensorFlowでは、モデルの実行速度に差があります。

実際にそれぞれの学習モデルで１つのアンケート項目の記入数値判定の処理を行った結果、以下のように処理時間に差が出ました。※環境はEC2(t2.medium, vCPUs:2)

|ライブラリ|処理時間|
|---|---|
|Keras|5.1秒|
|Tensorflow|3.6秒|


変換作業は、[こちらのリポジトリ](https://github.com/amir-abdi/keras_to_tensorflow)を活用させていただきました。


##アンケートの記入数値の判定(画像認識)を行う

学習済みモデルを用意できたので、実際にアンケートの記入数値の判定処理を行ってみます。

```python
import numpy as np
import tensorflow as tf
import cv2


# 学習済みモデルのファイルパス
SCALE1_PREDICT_MODEL = 'model/scale1_predict_model.pb' # 11点尺度の記入数値判定モデル
SCALE2_PREDICT_MODEL = 'model/scale2_predict_model.pb' # 5点尺度の記入数値判定モデル

# 11点尺度の記入数値判定
def scale1_cnn_predict(scale_img_list):
    scale1_predict_result = []
    for img in scale_img_list:
        sess = tf.Session()
        # 学習済みモデルの読み込み
        with tf.gfile.FastGFile(SCALE1_PREDICT_MODEL,'rb') as f:
          graph_def = tf.GraphDef()
          graph_def.ParseFromString(f.read())
          _ = tf.import_graph_def(graph_def,name = '')
        # 認識処理を実行
        prediction = predict_model_run(sess, img)
        tf.reset_default_graph()  #グラフをリセット
        scale1_predict_result.append(int(np.argmax(prediction)))
    return scale1_predict_result

# 5点尺度の記入数値判定
def scale2_cnn_predict(scale_img_list):
    scale2_predict_result = []
    for img in scale_img_list:
        sess = tf.Session()
        # 学習済みモデルの読み込み
        with tf.gfile.FastGFile(SCALE2_PREDICT_MODEL,'rb') as f:
          graph_def = tf.GraphDef()
          graph_def.ParseFromString(f.read())
          _ = tf.import_graph_def(graph_def,name = '')
        # 認識処理を実行
        prediction = predict_model_run(sess, img)
        tf.reset_default_graph()  #グラフをリセット
        scale2_predict_result.append(int(np.argmax(prediction)))
    return scale2_predict_result

# 対象画像を学習モデルに読み込ませる
def predict_model_run(sess, img):
    image = cv2.imread(img, 0)
    image = cv2.resize(image, (400, 30))
    image = np.reshape(image, (30, 400, 1))
    image_list = [image]
    image = np.asarray(image_list)
    X = image.astype('float32')
    X = X / 255.0
    prediction = sess.run('strided_slice:0',{'conv2d_1_input:0':X})
    return prediction


# アンケート用紙から切り抜いた各アンケート項目の画像(前回の記事参照)
scale1_img_list = ['images/q_img_0.jpg', 'images/q_img_1.jpg']
scale2_img_list = ['images/q_img_2.jpg', 'images/q_img_3.jpg', 'images/q_img_4.jpg', 'images/q_img_5.jpg', 'images/q_img_6.jpg']

# 予測処理
scale1_predict_result = scale1_cnn_predict(scale1_img_list)
scale2_predict_result = scale2_cnn_predict(scale2_img_list)



print('-------------')

for i in range(len(scale1_predict_result)):
    print( '11点尺度_'+str(i+1)+'の判定値：'+ str(scale1_predict_result[i]))


for i in range(len(scale2_predict_result)):
    print( '5点尺度_'+str(i+1)+'の判定値：'+ str(scale2_predict_result[i]))

print('-------------')

```
実行結果

```
-------------
11点尺度_1の判定値：10
11点尺度_2の判定値：8
5点尺度_1の判定値：5
5点尺度_2の判定値：5
5点尺度_3の判定値：4
5点尺度_4の判定値：3
5点尺度_5の判定値：4
-------------
```

第三者に記入してもらったアンケート用紙の撮影画像を使って
上記のコードで数値判定を行ったところ下記のような判定結果になりました。

|モデル|対象画像数|正解数|正解率|
|---|---|---|---|
|11点尺度|200|184|92%|
|5点尺度|200|191|96.5%|

誤判定した画像を確認すると、下記のようにアンケート項目の切り抜きの部分がうまくできていなかったり、アンケート用紙が曲がっていたりというパターンが見受けられました。

![cnn12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151361/d585b2ed-0941-25f9-6df5-b42454dc0d11.png)

そういう意味では、アンケート項目の切り抜き処理が安定すれば判定精度は向上できるのではないかという印象です。

前回の記事に記載したアンケート用紙の撮影画像の傾き補正や物体検出の処理を含めると以下のようなコードになります。


```python
import cv2
import numpy as np
import os
import tensorflow as tf

# 読み込み対象のアンケート用紙の撮影画像のパス
image_path = "images/~.JPG"

# アンケートの項目数
Q_NUMBERS = 7

#物体候補となる矩形が、最低でも含んでいなければならない近傍矩形の数(値が小さいほど誤検出は増え、大きいほど検出漏れが増える)　(物体候補領域が何個以上重なっていたらそれを一つの物体として検出する)
MIN_NEIGHBORS = [1, 2, 3, 4, 5]

#カスケード分類器
Cascade = cv2.CascadeClassifier('cascade/cascade.xml')

# 学習済みモデルのファイルパス
SCALE1_PREDICT_MODEL = 'model/scale1_predict_model.pb' # 11点尺度の記入数値判定モデル
SCALE2_PREDICT_MODEL = 'model/scale2_predict_model.pb' # 5点尺度の記入数値判定モデル

#二値化しノイズ除去を行う
def image_thresholding(img_path):
	img = cv2.imread(img_path, cv2.IMREAD_COLOR)
	#画像を二値化する
	gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
	gray_image = cv2.GaussianBlur(gray_image, (9,9), 0)
	binary_image = cv2.adaptiveThreshold(gray_image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 19, 2)
	#白黒反転させる
	invgray = cv2.bitwise_not(binary_image)
	#ノイズ除去を行うフィルターサイズの設定
	kernel = np.ones((4,4),np.uint8)
	#ノイズ除去処理
	none_noiz_img = cv2.morphologyEx(invgray, cv2.MORPH_OPEN, kernel)
	#再度反転
	image = cv2.bitwise_not(none_noiz_img)
	return image

#傾き補正した画像からアンケート項目を検出する
def detect_questionnaire_topic(dst):
	gray_image = cv2.cvtColor(dst, cv2.COLOR_BGR2RGB)
	results = False
	for min_neightbor in MIN_NEIGHBORS:
		point = Cascade.detectMultiScale(gray_image, 1.1, min_neightbor)
		if len(point) == Q_NUMBERS:
			results = True
			break
	return results, point

#検出した"■"マークの縦幅を取得
def get_square_height(point):
	h_list = [int(p[3]) for p in point]
	return min(h_list) * 5

#切り出す画像をy座標を元に昇順に並び替え
def sort_img(img_y_list):
	q_img_list = []
	for k, v in sorted(img_y_list.items()):
		q_img_list.append(str(v))
	return q_img_list

#検出した"■"マークを始点にアンケート項目を別画像として切り出す関数
def export_questionnaire_topic_img(dst, point):
    q_img_dict = {}
    square_height = get_square_height(point)
    for i, p in enumerate(point):
        save_path = 'images/q_img_'+str(i)+'.jpg'
        imgs = dst[p[1] + p[3] : p[1] + square_height, p[0] + p[2] : 3000]
        cv2.imwrite(save_path, imgs)
        q_img_dict[p[1]] = save_path
    return q_img_dict

#cv2.HoughCircles()によって検出された円のx,y座標を取得する
def get_circle_coordinate(circles):
	circle_list = []
	for circle in circles[0]:
		circle_list.append([circle[0], circle[1]])
	return circle_list

#画像の中心を軸として右上、右下、左上、左下それぞれの円の座標を確認する
def sort_coordinate(circle_list, center_point):
	for i in range(0, 4):
		if circle_list[i][0] > center_point["x"] and circle_list[i][1] > center_point["y"]:  #右上: x >1000 and y>1000
			upper_right = [circle_list[i][0],circle_list[i][1]]
		elif circle_list[i][0] > center_point["x"] and circle_list[i][1] < center_point["y"]:  #右下: x >1000 and y<1000
			under_right = [circle_list[i][0],circle_list[i][1]]
		elif circle_list[i][0] < center_point["x"] and circle_list[i][1] > center_point["y"]:  #左上: x <1000 and y>1000
			upper_left = [circle_list[i][0],circle_list[i][1]]
		elif circle_list[i][0] < center_point["x"] and circle_list[i][1] < center_point["y"]:  #左下: x <1000 and y<1000
			under_left = [circle_list[i][0],circle_list[i][1]]
	return { "upper_right" : upper_right, "under_right" : under_right, "upper_left" : upper_left, "under_left" : under_left }

#取得した円座標を元に射影変換を行う
def perspective_transform(circle_list, height_rate):
	height_value = round(3000 * height_rate)
	before_pts = np.float32([circle_list["upper_right"], circle_list["under_right"], circle_list["under_left"], circle_list["upper_left"]])
	after_pts = np.float32([[3000, height_value], [3000, 0], [0, 0], [0, height_value]])
	M = cv2.getPerspectiveTransform(before_pts, after_pts)
	dst = cv2.warpPerspective(img, M, (3000, height_value))
	return dst

# 11点尺度の記入数値判定
def scale1_cnn_predict(scale_img_list):
    scale1_predict_result = []
    for img in scale_img_list:
        sess = tf.Session()
        # 学習済みモデルの読み込み
        with tf.gfile.FastGFile(SCALE1_PREDICT_MODEL,'rb') as f:
          graph_def = tf.GraphDef()
          graph_def.ParseFromString(f.read())
          _ = tf.import_graph_def(graph_def,name = '')
        # 認識処理を実行
        prediction = predict_model_run(sess, img)
        tf.reset_default_graph()  #グラフをリセット
        scale1_predict_result.append(int(np.argmax(prediction)))
    return scale1_predict_result

# 5点尺度の記入数値判定
def scale2_cnn_predict(scale_img_list):
    scale2_predict_result = []
    for img in scale_img_list:
        sess = tf.Session()
        # 学習済みモデルの読み込み
        with tf.gfile.FastGFile(SCALE2_PREDICT_MODEL,'rb') as f:
          graph_def = tf.GraphDef()
          graph_def.ParseFromString(f.read())
          _ = tf.import_graph_def(graph_def,name = '')
        # 認識処理を実行
        prediction = predict_model_run(sess, img)
        tf.reset_default_graph()  #グラフをリセット
        scale2_predict_result.append(int(np.argmax(prediction)))
    return scale2_predict_result

# 対象画像を学習モデルに読み込ませる
def predict_model_run(sess, img):
    image = cv2.resize(img, (400, 30))
    image = np.reshape(image, (30, 400, 1))
    image_list = [image]
    image = np.asarray(image_list)
    X = image.astype('float32')
    X = X / 255.0
    prediction = sess.run('strided_slice:0',{'conv2d_1_input:0':X})
    return prediction


#画像を読み込む
img = cv2.imread(image_path, cv2.IMREAD_COLOR)

#画像ピクセルサイズ取得
h, w, c = img.shape

#画像の横幅に対する高さ比率を取得
height_rate = round((h / w), 2)

#画像の中心座標を求める
center_point = {"x" : round((w / 2), 2), "y" : round((h / 2), 2)}

#画像にぼかしを入れる
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur_image = cv2.GaussianBlur(gray_image, (29, 29), 0)

#アンケート用紙の四隅にある円を検出させる
minDistList = [2600, 2400, 2200]
param1List = [30, 20, 10]
param2List = [40, 30, 20]

for minDist in minDistList:
	for param1 in param1List:
		for param2 in param2List:
			circles = cv2.HoughCircles(blur_image, cv2.HOUGH_GRADIENT, 1, minDist, param1=param1, param2=param2, minRadius=15, maxRadius=100)
			if len(circles) == 4:
				break
		else:
			continue
		break
	else:
		continue
	break


if len(circles[0]) != 4 or isinstance(circles,type(None)) == True:
    print('検出エラー')
else:
	#検出した４つの円の座標を取得
	circle_list = get_circle_coordinate(circles)

	#右上,右下,左上,左下それぞれの座標を取得
	circle_list = sort_coordinate(circle_list, center_point)

	#射影変換による傾き補正
	dst = perspective_transform(circle_list, height_rate)

	results, point = detect_questionnaire_topic(dst)

	q_img_dict = export_questionnaire_topic_img(dst, point)
	q_img_list = sort_img(q_img_dict)

	# 画像を二値化
	scale_img_list = []
	for img_path in q_img_list:
		image = image_thresholding(img_path)
		scale_img_list.append(image)

	# アンケート用紙から切り抜いた各アンケート項目の画像(前回の記事参照)
	scale1_img_list = scale_img_list[:2] # 上から２つが11点尺度の画像
	scale2_img_list = scale_img_list[2:] # 上から3つ目以降が11点尺度の画像

	# 予測処理
	scale1_predict_result = scale1_cnn_predict(scale1_img_list)
	scale2_predict_result = scale2_cnn_predict(scale2_img_list)

	print('-------------')

	for i in range(len(scale1_predict_result)):
	    print( '11点尺度_'+str(i+1)+'の判定値：'+ str(scale1_predict_result[i]))

	for i in range(len(scale2_predict_result)):
	    print( '5点尺度_'+str(i+1)+'の判定値：'+ str(scale2_predict_result[i]))

	print('-------------')
```

上記のコードを走らせて見たところ、アンケートの撮影画像を読み込んでから記入数値の認識処理までの合計処理時間が約30秒程度かかりました。KerasモデルをTensorflowモデルに変換したことで多少は改善されていますが、リアルタイム性の観点で考えると少し厳しい印象です。

今後は、処理速度の問題も考慮しながら、画像認識によって出力された数値をDBに保存し、表やグラフで集計できるようなシステムを検討したいと思います。