Developer's Blog

iOS11 で文字認識

 

 

こんにちは、アプリケーション共同開発部の河野(こうの)です。

今年の WWDC で Vision フレームワークが発表され、テキスト検出などの機能を簡単にアプリに組み込むことができるようになりました。
しかし、画像のここにテキストがあるよという検出はできるものの、そのテキストが何の文字なのかという文字認識の機能についてはまだサポートされていません。
近い将来 Vision フレームワークに文字認識の機能が追加されるとは思いますが、いま文字認識の機能をアプリに組み込みたい場合はどうすればいいのか。
iOS アプリ開発のプロジェクトで文字認識機能を開発する機会があり、そのときに調査したことなどをまとめたいと思います。

概要

今回、認識したい文字列は

– 数字と大文字英字のみからなる文字列
– 1 行のある決まった文字数
– 特定のフォント
– 文字間で前後関係を持たない文字列

というような条件があり、文字認識の中では簡単な課題といえると思います。

既存のライブラリを使用して解決できれば手っ取り早いですが、この種のライブラリはサイズが大きく、アプリのサイズを大きくしてしまうこともあります。

いま開発しているアプリは、iOS11 以降対応の予定なので、iOS11 で追加される Core ML も使用できます。
上記程度の難易度の文字認識であれば、独自にモデルを作成し Core ML を使用しても十分な精度を実現できるかもしれません。

ここでは、この既存のライブラリと Core ML の2つのアプローチを試してみます。

SwiftOCR

既存のライブラリを調べてみると、オープンソースの OCR ライブラリとして有名な Tesseract の iOS ポート Tesseract-OCR-iOS  がみつかります。
また後発の SwiftOCR という Swift でかかれた OCR ライブラリも見つかりました。

SwiftOCR の README を呼んでみると Tesseract よりも高速で精度も高く

SwiftOCR is optimized for recognizing short, one line long alphanumeric codes

とかかれており、今回の要件に合致します。

サンプルのプロジェクトがあったので試しに起動してみます。

実際に読み取りたい文字列を認識させてみると、認識精度は 50% もなさそうです。
SwiftOCRDelegate で画像の前処理をカスタマイズできるので調整してみますが、精度はあまり上がりません。

いったん SwiftOCR はあきらめて Tesseract-OCR-iOS を試してみます。

Tesseract x Vision.framework

Tesseract-OCR-iOS のライブラリをインストールして、tessdata から英語の学習モデルをダウンロードしてプロジェクトに追加します。

ここで実際に読み取ろうとした際に、エラーが発生しました。

actual_tessdata_num_entries_ <= TESSDATA_NUM_ENTRIES:Error:Assert 
failed:in file tessdatamanager.cpp, line 53 https://github.com/gali8/Tesseract-OCR-iOS/issues/299#issuecomment-267363981

少し古い学習モデルであれば動作するというコメントが GitHub の Issues にあったので、こちらのモデルに置き換えて、再度試したところ、問題なく動作しました。

先程 SwiftOCR で試してたものと同じ文字列の認識を試したところ、精度は SwiftOCR より高い 90% 以上でいい感じです。

Vision フレームワークのテキスト検出で、文字を分割して認識させてみたところ、精度はほぼ 100% にまであがりました。

今回実現したい機能は、Tesseract x Vision.framework で実現できそうですが、もうひとつの Core ML を使ったアプローチも試してみます。

Core ML x Keras

Python も深層学習もほとんど経験がないので、Keras の examples にある image_ocr.py を参考に学習用の画像を作成し、mnist_cnn.py を参考に作成した画像の学習をおこなうことにします。

Vision フレームワークのテキスト検出で、文字を1文字ずつに分解して文字認識することを前提として、英数字1文字が描かれた画像を学習データとして作成します。image_ocr.py に下記の CharImageDataGenerator を追加して paint_text 内のフォントの設定を読み取りたい文字に合わせ、その他の不要なコードはコメントアウトします。

class CharImageDataGenerator:
    def __init__(self, char_list, img_w, img_h):
        self.img_w = img_w
        self.img_h = img_h
        self.char_list = char_list

    def generate_image_data(self, size=100):
        labels = np.ones([size])
        if K.image_data_format() == 'channels_first':
            data = np.ones([size, 1, self.img_w, self.img_h])
        else:
            data = np.ones([size, self.img_w, self.img_h, 1])

        for i, text in enumerate(np.random.choice(self.char_list, size)):
            labels[i] = self.char_to_label(text)
            if K.image_data_format() == 'channels_first':
                data[i, 0, :, :] = paint_text(text, self.img_w, self.img_h, rotate=True)[0, :, :].T
            else:
                data[i, :, :, 0] = paint_text(text, self.img_w, self.img_h, rotate=True)[0, :, :].T

        return {'image': data, 'labels': labels}

    def char_to_label(self, char):
        return self.char_list.index(char)

画像を生成して npz ファイルとして保存します。

char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
             'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
             'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
img_w, img_h = 28, 28

gen = CharImageDataGenerator(char_list=char_list, img_w=img_w, img_h=img_h)
train = gen.generate_image_data(size=100000)
test = gen.generate_image_data(size=20000)
np.savez('char_image.npz', x_train=train['image'], y_train=train['labels'], x_test=test['image'], y_test=test['labels'])

つぎに mnist_cnn.py を参考にして、作成した学習データをつかって、モデルを作成します。

mnist_cnn.py をコピーしてコードを変更していきます。

$ cp mnist_cnn.py char_recognizer.py

分類クラス数と画像サイズを学習データに合わせます。

 char_list = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 
 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] 
num_classes = len(char_list) 
img_rows, img_cols = 28, 28

MNIST の代わりにさきほど作成した画像データを読み込みます。

# (x_train, y_train), (x_test, y_test) = mnist.load_data() 
f = np.load('char_image.npz') 
x_train, y_train = f['x_train'], f['y_train'] 
x_test, y_test = f['x_test'], f['y_test']

正規化は不要なのでコメントアウトします。

# x_train /= 255 
# x_test /= 255

最後に .mlmodel を保存するコードを追加します。

coreml_model = coremltools.converters.keras.convert(model, input_names='image', image_input_names='image', class_labels=char_list)
coreml_model.save('CharImageRecognizer.mlmodel')

学習開始。

$ python char_recognizer.py

損失が収束し、学習できたようです。
作成された CharImageRecognizer.mlmodel を見てみるとファイルサイズは 4.8 MB。

CharImageRecognizer.mlmodel を iOS のプロジェクトに取り込んで、文字を認識させてみます。

認識させたい文字を Vision フレームワークのテキスト検出を使って、画像を1文字ずつに切り出し、学習データで使用した画像のサイズ(今回は 28 x 28)にリサイズします。
今回はとりあえず検証のため画像の前処理は Tessaract にまかせて Tessaract で文字認識する際に前処理されて作成された画像を取り出して使用することにします。
これを Input データとして与えてやり、認識結果を得ます。

結果は、、、

70% 程度の精度でした。。特に D と 0、J と U、B と 5 などの認識精度は、かなり低い。。学習データの作成もパラメータ設定も、サンプルほぼそのまま使ったわりには良い方でしょうか。

まとめ

プロジェクトの都合もあり実際に読み取った画像や詳細な認識結果をお見せすることはできませんでしたが、今回の調査では、現状 iOS で文字認識をしたいのであれば、高機能で多言語に対応している Tessaract を使用するのがベストなアプローチだといってよさそうです。
学習するための十分なデータが得られたり、ある程度限られた条件の文字認識であれば、少し時間をかけて学習データやパラメータを調整すると、Core ML を使用した文字認識でも、十分な認識精度を実現することが可能なのではないかと思います。

 

 

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

 

 

フェンリル採用チームの Twitter アカウントです。応募前のお問い合わせや、ちょっとした相談ごとなどお気軽にどうぞ!

 

フェンリルの Facebook ページでは、最新トピックをお知らせしています。よろしければいいね!してください!

 

Sleipnir の Facebook ページでは、ユーザーの方たちとのコミュニケーションや最新情報の投稿などを行なっています。よろしければいいね!してください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.