読者です 読者をやめる 読者になる 読者になる

備忘録

イケイケエンジニアになるために自己嫌悪と戦う大学院生のメモ兼モチベーション維持。

kNNで文字認識

kNNとは

k近傍法(k-nearest neighbor)。かなり単純なクラスタリングアルゴリズム

  1. 教師データ(ラベリング済)を特徴ベクトル化して学習
  2. クラスタリング対象のデータも特徴ベクトル化
  3. 特徴ベクトルの距離を比較し、対象データと近い順にk個の教師データを選ぶ
  4. 選ばれた教師データに付与されているラベルから対象データのクラスを決定

直感的に理解できる簡素なアルゴリズムである。 OpenCVで実装されているため簡単に試せる。

qiita.com

IkalogではkNNで文字認識を行なっているらしい。

「スプラトゥーン」リアルタイム画像解析ツール 「IkaLog」の裏側

文字位置・角度・照明条件が可変な今回のケースではどの程度の精度が出るのか、試しにやってみた。

学習データの準備

前項までで文字位置の特定ができているので、その領域を切り出して等サイズにリサイズ→保存する。

resized_img = cv2.resize(character_img, (RESIZED_WIDTH, RESIZED_HEIGHT), 
                                            interpolation=cv2.INTER_NEAREST)

PythonでkNN

学習は毎回実行時に行う。

  1. 学習データを1行N列のベクトルに変形
# sample = img.reshape((1, img.shape[0] * img.shape[1]))
sample = img.reshape((1, -1))

reshapeの引数のサイズのうち片方に-1を指定すると,もう一方の値から適切な数値を判断して変形してくれる。 Numpyつよい。

ラベルは別配列に要素番号が同じになるように保存する。

label = ord('0') + int(character_i)
labels.append(label)

samplesは[1行N列のベクトル]の配列、かつtype() == CV_32Sになるように注意して変形する。 labelsも[1行1列のベクトル(?)]の配列、かつtype() == CV_32Sになるように変形。

printするとこんな感じ。

samples
[[   0.    0.    0. ...,    0.    0.    0.]
 [   0.    0.    0. ...,    0.    0.    0.]
 [   0.    0.    0. ...,    0.    0.    0.]
 ..., 
 [   0.    0.    0. ...,  255.  255.    0.]
 [   0.    0.    0. ...,  255.  255.    0.]
 [   0.    0.    0. ...,  255.  255.  255.]]

labels
[[ 48.]
 [ 48.]
 [ 48.]
...,
 [ 57.]
 [ 57.]
 [ 57.]
 [ 57.]]

学習部分全体のコード。

    samples = None
    labels = []
    # for 0 - 9
    for character_i in range(0, 10):
        img_dir = TRAIN_DATA_DIR + str(character_i) + "/"
        files = os.listdir(img_dir)
        for file in files:
            ftitle, fext = os.path.splitext(file)
            if fext != '.jpg' and fext != '.png':
                continue

            # Load image
            abspath = os.path.abspath(img_dir + file)
            img = cv2.imread(abspath, cv2.IMREAD_GRAYSCALE)
            if img is None:
                print('Failed to read image')
                break

            sample = img.reshape((1, -1))
            label = ord('0') + int(character_i)

            if samples is None:
                samples = np.empty((0, img.shape[0] * img.shape[1]))
            samples = np.append(samples, sample, 0).astype(np.float32)
            labels.append(label)

    labels = np.array(labels, np.float32)
    labels = labels.reshape((labels.size, 1)).astype(np.float32)

    knn = cv2.ml.KNearest_create()
    knn.train(samples, cv2.ml.ROW_SAMPLE, labels)

文字認識

学習結果を使って画像をクラスタリングすることで文字認識を行う。

# kNN
ch_string=""
for ch_img in character_imgs:
    sample = ch_img.reshape((1, ch_img.shape[0] * ch_img.shape[1]))
    sample = np.array(sample, np.float32)

    k = 3
    retval, results, neigh_resp, dists = knn.findNearest(sample, k)
    d = chr(int(results.ravel()))
    ch_string += str(d)
print("result:" + ch_string)
cv2.waitKey(0)

認識対象も勿論1行N列の32bitfloat配列に直してknn.findNearest()につっこむ。

結果

成功例

f:id:reverent_f:20170124182341p:plain:w200

result:09828932

f:id:reverent_f:20170124182510p:plain:w200

result:08463768

失敗例

f:id:reverent_f:20170124182543p:plain:w200

result:09955509

f:id:reverent_f:20170124182548p:plain:w200

result:09680560

13画像(104字)に対して6文字の認識ミスだったので認識精度は94.23%。 0-8-9あたりの誤認識が多かった。

考察

kNNのアルゴリズムを考慮すると、学習が足りないとかいう問題ではなくノイズの影響が大きいように思う。

  • ひどい例

9 : f:id:reverent_f:20170124182747p:plain 8 : f:id:reverent_f:20170124182750p:plain

また、各画素値を単純に並べて特徴ベクトルにする関係上、上下端の余白の大きさや回転による影響もありそう。

  • 不必要な部分まで切り取られたパターン

8 : f:id:reverent_f:20170124183215p:plain

2値化処理を見直すかDeep Learning様の力でゴリ押すかしようと思う。