20210116のTensorFlowに関する記事は2件です。

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

この記事について

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

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

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

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

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

ディープラーニングの手法の1つである畳み込みニューラルネットワーク(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

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

cnn1-min.png

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

cnn2-min (1).png

2. 画像の水増しを行う

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

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

ImageDataGeneratorクラス

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

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

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

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

畳み込みニューラルネットワーク(CNN)

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

cnn4-min.png

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

cnn5-min.png

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

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

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

cnn10-min.png

畳み込み層

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

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

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

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

cnn6-min.png

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

cnn7-min.png

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

cnn8-min.png

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

プーリング層

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

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

cnn9-min.png

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

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

実装コード

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

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

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

cnn11-min (1).png

以下、実装コード

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ファイル)に変換します。

なぜ、変換するかというと処理速度の向上を行うためです。こちらの記事にあるようにKerasとTensorFlowでは、モデルの実行速度に差があります。

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

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

変換作業は、こちらのリポジトリを活用させていただきました。

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

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

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

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

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

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:
    #検出した4つの円の座標を取得
    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] # 上から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に保存し、表やグラフで集計できるようなシステムを検討したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

この記事について

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

システム概要

作成するシステムはざっくり以下のような流れを想定しています。
 1.記入済みのアンケートを撮影する
 2.撮影されたアンケート画像に対してOCR処理を行う
 3.OCRによる読み取り結果を出力する

アンケートの記入形式

リッカート尺度に基づく11段階評価と5段階評価による記入形式で
該当する数字にマルをつけてもらう形式です。

リッカート尺度.png

OCRの仕組み

アンケート用紙のOCR処理は下記のような仕組みにしました。

① 傾き補正を行う

傾き補正-min (2).png

② 11点尺度&5点尺度の各アンケート項目を画像として切り抜く

物体検出-min.png

③ノイズ処理を行う(平滑化)

二値化-min.png

④ 切り抜いた画像に対してあらかじめディープラーニング(CNN:Convolutional Neural Network)で学習した画像認識モデルを利用してアンケート項目の記入数値の判定処理を行う
⑤ 判定結果(集計データ)をDBに保存する

傾き補正

撮影されたアンケート画像が傾いていると、後工程で記入数値の判定を行うモデルを構築する際にその傾きも考慮しておく必要があります。しかし開発環境のリソースは限られていたため、前処理として傾き補正を行なっておく必要がありました。傾き補正は射影変換を用いました。

射影変換とは

射影変換とはある平面を別の平面に射影することができる変換手法です。例えば、斜めから見たものを、あたかも正面から見たように表現することができます。

どのような仕組みかというと
まず下記のように4点の座標を、それぞれの別の座標に移動させるとします。

傾き補正2-min.png

それぞれの移動先の座標は移動前の座標を用いて行列で以下のように表すことができます。

傾き補正3-min.png

上記の変換行列(3×3)は連立方程式によって求めることができます。この変換行列を用いてすべての座標を移動させると移動前の元画像のオブジェクトの形状を維持したまま、まるで違う向きから見たような表示が可能になります。

傾き補正4-min.png

今回は傾き補正を具体的に以下の手順で行いました。

  1. アンケート用紙の四隅に"○"マークを記載しておきます。
  2. OpenCVのcv2.HoughCircles()関数を使って円検出を行い、それぞれの座標を取得します。
  3. 2の座標を元に射影変換を行います。

1. アンケート用紙の四隅に"○"マークを記載

傾き補正では対象のオブジェクトの輪郭検出を行なった後に頂点の座標を取得する処理がよく行われますが、今回はあらかじめアンケート用紙の四隅に記載した"○"マークを頂点の代わりに利用します。

傾き補正1-min.png

2. 円検出

OpenCVのcv2.HoughCircles()関数を使って四隅の円検出を行います。

アンケートは該当数字にペンでマルをつける記入形式を想定しているので、そのマルを誤検出されないように意図的に画像にぼかしを入れます。ぼかしを入れることで細い線は検出されずらくなります。

傾き補正5-min.png

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

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

cv2.GaussianBlur()は指定の画像に対してぼかしを入れる関数になります。第2引数はぼかし処理に使うフィルタのサイズ指定になり、この値が大きければ大きいほどぼかしの度合いは強くなります。

この状態で円検出を行うことで四隅のマルを検出されやすくします。

#アンケート用紙の四隅にある円を検出させる
circles = cv2.HoughCircles(blur_image, cv2.HOUGH_GRADIENT, 1, 2400, param1=30, param2=40, minRadius=15, maxRadius=100)

以下、cv2.HoughCircles()の引数の詳細です。

内容
第一引数 blur_image 対象画像。
第二引数 cv2.HOUGH_GRADIENT 変換手法。現在、選択できる手法は cv2.HOUGH_GRADIENT のみ。
第三引数 dp=1 処理するときに元画像の解像度を落として検出する場合は増やす。元画像のままなら1を指定。
第四引数 minDist=2400 検出される円同士が最低限離れていなければならない距離。
第五引数 param1=30 値が低いほどより多くのエッジを検出できるらしいです。
第六引数 param2=30 円の中心を検出する際の閾値。低い値だと誤検出が増え、高い値だと未検出が増える。
第七引数 minRadius=15 検出する円の半径の下限値。
第八引数 maxRadius=100 検出する円の半径の上限値。

今回は円検出の精度を上げるためパラメータの数値を変化させながら4つの円が検出されるまで繰り返し処理を行いました。

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

返値circlesには検出された円の座標と半径が配列として入ります。

[[[ 238.5 3613.5   56. ]
  [ 484.5  241.5   54.5]
  [2651.5 3717.5   52. ]
  [2843.5  465.5   51. ]]]

この状態では各座標が四隅のどの"○"マークの座標かわからないため整理します。

#画像を読み込む
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)}

#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"]:
            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"]:
            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"]:
            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"]:
            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 }

circle_list = get_circle_coordinate(circles)
circle_list = sort_coordinate(circle_list)

3. 射影変換

円検出によって四隅の"○"マークの座標が取得できたはずなので、先ほど説明した射影変換を行います。
変換後の出力画像は横幅3000に固定し、縦幅は元画像のアスペクト比により算出しました。
変換行列はcv2.getPerspectiveTransform()によって生成し、その変換行列を使ってcv2.warpPerspective()によって射影変換を行なっています。

#取得した円座標を元に射影変換を行う
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

dst = perspective_transform(circle_list, height_rate)

ここまでの実装コードをまとめたものが下記になります。

import cv2
import numpy as np

#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


#画像サイズ取得(imageは最初にノイズ処理&二値化した画像)
h, w, c = image.shape

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

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

#画像にぼかしを入れる
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur_image = cv2.GaussianBlur(gray_image, (47, 47), 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:
    error = '検出エラー'
else:
    #検出した4つの円の座標を取得
    circle_list = get_circle_coordinate(circles)
    #右上,右下,左上,左下それぞれの座標を取得
    circle_list = sort_coordinate(circle_list, center_point)
    #射影変換による傾き補正
    dst = perspective_transform(circle_list, height_rate)

これで傾き補正ができました。

傾き補正-min (2).png

物体検出

用紙に記載されている各アンケート項目をそれぞれ別画像として切り抜きます。

OpenCVでは特定のオブジェクトを検出する学習器を作成することができます。そこで、各アンケート項目を物体検出することを検討しましたが、大量のサンプルデータが必要であり、検出する際の矩形のサイズのバラ付きがありそうだったため他の方法を検討しました。

採用した方法は以下のような内容です。
1. "■"マークをアンケート項目の左側に記載
2. "■"マークを検出するための学習器を作成
3. "■"マークを物体検出
4. 検出した"■"マークを始点として、アンケート項目を切り抜き画像として保存

1. "■"マークをアンケート項目の左側に記載

"■"マークをアンケート項目の左側に記載し、このマークを物体検出の対象オブジェクトとします。

物体検出2-min.png

2. "■"マークを検出するための学習器を作成

OpenCVでは特定のオブジェクトを検出するカスケード分類器を作成することができます。今回は"■"マークを検出するカスケード分類器を作成しました。

カスケード分類器とは

物体検出を行う際には、検出したい物体がどのような特徴を有しているのか把握しておく必要があります。そこで、あらかじめ該当する物体を含む画像と含まない画像を用意し、検出したい物体の特徴を抽出しておきます。この抽出された特徴をまとめたものをカスケード分類器と言います。

カスケード分類器の作り方については下記の記事を参照

OpenCVでカスケード分類器を作って物体検出をやってみた

3. "■"マークを物体検出

2で作成したカスケード分類器を使って、"■"マークを物体検出します。

import cv2
import numpy as np
import os

#アンケートの項目数
Q_NUMBERS = 7
#物体候補となる矩形が、最低でも含んでいなければならない近傍矩形の数(値が小さいほど誤検出は増え、大きいほど検出漏れが増える)
MIN_NEIGHBORS = [1, 2, 3, 4, 5]
#カスケード分類器を読み込む
Cascade = cv2.CascadeClassifier('cascade.xml')

#傾き補正した画像からアンケート項目を検出する
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

results, point = detect_questionnaire_topic(dst) #引数の"dst"は傾き補正済みの画像

上記のコードを実行させると下図のように"■"マークが検出されます(赤枠の矩形)。

物体検出3-min (1).png

また、変数pointには検出したオブジェクト("■"マーク)の座標が配列で保存されます。ここでいうx座標とy座標は矩形の左上の頂点の座標を指します。

#[x座標, y座標, 横幅, 縦幅]
[[  91  531   76   76]
 [  96  930   76   76]
 [  92 1479   76   76]
 [  93 1875   74   74]
 [  96 2272   71   71]
 [  96 2667   74   74]
 [  93 3061   73   73]]

4. 検出した"■"マークを始点としてアンケート項目を切り抜き、画像として保存

3で検出した"■"マークを始点としてアンケート項目を切り抜きます。
始点および縦幅、横幅以下のように設定しました。

項目 内容
始点 x座標 : "■"マークのx座標 + "■"マークの横幅
y座標 : "■"マークのy座標 + "■"マークの縦幅
縦幅 "■"マークの縦幅 × 4
横幅 始点から画像の右端まで

物体検出1-min.png

切り抜き方としてはあまり綺麗ではありませんが、リソースが限られていたため、今回はこちらの方法で進めます。

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

#切り出す画像を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


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

q_img_dictは切り出した画像の始点のy座標と画像ファイルのパスが入っています。

{531: 'images/q_img_0.jpg', 930: 'images/q_img_1.jpg', 1479: 'images/q_img_2.jpg', 1875: 'images/q_img_3.jpg', 2272: 'images/q_img_4.jpg', 2667: 'images/q_img_5.jpg', 3061: 'images/q_img_6.jpg'}

この連想配列をsort_img()によってy座標を軸に並び替えます。そして、画像パスの配列q_img_listを作っておきます。

['images/q_img_0.jpg', 'images/q_img_1.jpg', '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']

これで各アンケート項目を別画像として切り抜くことができました。

物体検出-min.png

ノイズ除去&二値化

切り出した画像に対してノイズ除去と二値化を行います。
元々はこれらの処理は考慮していませんでした。しかし、CNNによる数値判定モデルを作成する際に、開発環境でメモリ不足に陥ってしまったため、白黒の二値画像かつノイズを除去したものに学習データを絞ることでメモリ負荷を軽減させる必要が出てきました。それが契機でノイズ除去&二値化を加えました。

ノイズ除去にはモルフォロジー変換という手法を使います。

モルフォロジー変換

モルフォロジー変換とは、画像中のオブジェクトに対して、「収束」や「膨張」を行うことでオブジェクトの周りあるいはオブジェクトの中に含まれているノイズを除去する手法です。この手法には「オープン処理」「クロージング処理」「モルフォロジー勾配処理」など複数のモードがありますが、今回は収束させたあとに膨張させる処理を行う「オープン処理」を採用しました。(※モルフォロジー変換は、二値画像を対象とするため、最初に対象画像の二値化を行なっておく必要があります。)

収束

画像に対してカーネル(フィルタ)をスライドさせていきます。画像中の(1か0のどちらかの値を持つ)画素は、カーネルの領域に含まれる画素の画素値が全て1(白)であれば1(白)となり,そうでなければ0(黒)として出力されます。

モルフォロジー変換1-min.png

例えば下記のような二値化された画像があるとします。中央にある白のブロックの周辺に散らばる小さな白色のブロックをノイズだと仮定します。この画像にフィルタ(サイズは3×3とします)をスライドさせて「収束」を行います。

モルフォロジー変換2-min.png

スライドさせた結果、一部のブロックが白から黒に変わり、ノイズが除去されます。

モルフォロジー変換3-min (1).png

ノイズは除去されましたが、中央の白のブロックは1回り小さくなってしまいました。これを膨張によって元の大きさに近づけます。

膨張

収束と同じように画像に対してカーネル(フィルタ)をスライドさせていきます。ただし、収束とは逆でカーネルの領域に含まれる画素の画素値が全て0(黒)であれば0(黒)となり,そうでなければ1(白)として出力されます。

モルフォロジー変換4-min.png

スライドさせた結果、一部のブロックが黒から白に変わり、収縮された中央のブロックが元の大きさに近づきます。

モルフォロジー変換5-min.png

このようにモノフォロジー変換(収束→膨張)を行うことで画像内のノイズを除去することができます。

以下が実装コードになります。物体検出で切り出した各アンケートの画像(q_img_list)に対して二値化とノイズ除去を行なってきます。

#二値化しノイズ除去を行う
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


for img_path in q_img_list:
    image = image_thresholding(img_path)
    cv2.imwrite(img_path, image)

これで画像を白と黒に二値化しノイズ除去もできました。

二値化-min.png

ここからは、切り出した画像に対してディープラーニングで構築した記入数値判定モデルを使った画像認識を行なっていきます。

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む