文字位置切り抜き
学習用の画像を集めていくうちに文字位置の切り抜きに失敗することが多くなり、 このままではロバストな認識は行えないということで改善を試みた。
スコア部分の切り抜き
画像によってヘッダの認識にずれがあり、切り出したスコア部分に欠けが生じることがあった。
この処理で欠けると後の処理では回復できないので、かなり大きめに領域を取得する。
2値化
P-タイル法を試してみたが、安定しないので今まで通り R値とG値の合算結果に大津のアルゴリズムを適用して2値化する。
スコア文字部分の絞り込み
大きく切り抜いた分だけ余計な領域が発生する。 幸い、切り抜きたい文字の周りには黒い余白が現れるためこれを手掛かりに絞り込みを行う。
- 画像の上下左右に固定幅の白枠をつける
このとき、元から画像に存在している枠によって、目的文字部分と(あれば)右の余計なコンボ数部分が隔てられる。
2.白枠で縁取られた領域のうち最大部分を切り抜く
# 白枠で縁取られた面積最大の領域を探す image, contours, hierarchy = cv2.findContours(l_img,cv2.RETR_CCOMP,cv2.CHAIN_APPROX_SIMPLE) contourImg = cv2.cvtColor(l_img, cv2.COLOR_GRAY2BGR) bgr_total = contourImg.copy() inner_contours = [] for index, contour in enumerate(contours): if hierarchy[0][index][2] == -1: inner_contours.append(contour) approxes = [] max_box = None for contour in inner_contours: # 矩形補完 epsilon = 0.01*cv2.arcLength(contour,True) approx = cv2.approxPolyDP(contour,epsilon,True) area = cv2.contourArea(approx) if max_box is None or cv2.contourArea(max_box) < cv2.contourArea(approx): max_box = approx if(area > 20): approxes.append(approx) x,y,w,h = cv2.boundingRect(contour)
各文字特定
以前の記事で書いたヒストグラムを使った処理によって文字候補をリストアップする。 以前までの処理では文字以外の部分もリストアップされていたが、文字候補部分のアスペクト比からさらに候補を絞り込む。
以前より閾値を厳しく設定し、文字候補に漏れがあっても文字同士が連結しないようにする。
候補から外れた文字部分の補完
文字候補として外れてしまった文字(上図なら 7 )を文字の位置関係から推測する。
ほとんどの場合スコア先頭に来る 0 , 9 は上記の処理で文字候補から外れることはほぼ無い。 そこで、スコア先頭文字は確実に候補中にあるとして、右方向に8文字あると仮定し補完する。 先頭5文字と後半3文字でフォントサイズが異なることに気をつける。 細かい推測方法は省略する。
整形
あとは各文字の上下の余白を削除・等サイズにリサイズして完成
まとめ
やっとこれで学習用データの切り出しができるようになった! 文字位置の特定がロバストにできればTesseractを使う必要はないので(各文字の画像を検出器にかければいいので)、文字の認識はkNNかKerasあたりをつかったDeepLearningでやる予定。
画像傾きの補正
文字認識その他色々な処理の前に画像の傾き(回転)を自動補正したいのでやった。
方法
cv2.Canny()
でエッジ検出
→ cv2.HoughLinesP()
で直線検出
→ 水平方向の直線の平均角度を取る
→ 直線が水平になるように画像全体を回転
# 画像の傾き検出 # @return 水平からの傾き角度 def get_degree(img): l_img = img.copy() gray_image = cv2.cvtColor(l_img, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray_image,50,150,apertureSize = 3) minLineLength = 200 maxLineGap = 30 lines = cv2.HoughLinesP(edges,1,np.pi/180,100,minLineLength,maxLineGap) sum_arg = 0; count = 0; for line in lines: for x1,y1,x2,y2 in line: arg = math.degrees(math.atan2((y2-y1), (x2-x1))) HORIZONTAL = 0 DIFF = 20 # 許容誤差 -> -20 - +20 を本来の水平線と考える if arg > HORIZONTAL - DIFF and arg < HORIZONTAL + DIFF : sum_arg += arg; count += 1 if count == 0: return HORIZONTAL else: return (sum_arg / count) - HORIZONTAL;
arg = get_degree(img) rotate_img = ndimage.rotate(img, arg)
結果
→
→
見た目上あまり変化がないように見えるが、後続の処理の精度は向上した
Tesseract-OCRの学習を試してみる
文字位置特定→文字画像を切り出し→kNNで文字認識
という流れを踏む予定だったが、安定した文字位置の特定処理が難しいのでTesseract-OCRを試してみる。
学習前の状態でOCR
- Tesseract v3.04
$ tesseract number.png out Tesseract Open Source OCR Engine v3.04.01 with Leptonica Info in fopenReadFromMemory: work-around: writing to a temp file $ cat out.txt 5915§7WE €22
- digits を指定
$ tesseract number.png out digits Tesseract Open Source OCR Engine v3.04.01 with Leptonica Info in fopenReadFromMemory: work-around: writing to a temp file $ cat out.txt . 957 3 522
- Tesseract v4.00
$ tesseract number.png out Tesseract Open Source OCR Engine v4.00.00alpha with Leptonica Warning. Invalid resolution 0 dpi. Using 70 instead. $ cat out.txt oSsS6S?771e
tessedit_char_whitelist 0123456789
を指定しても上手くいかない、なぜだ
当たり前だが未学習だとほぼ読めない。 OCR Engine modes を指定しても同じ結果だった。
学習データの準備
Tesseract-OCR v4.00はまだ開発版なので3.04の学習を試してみる。
Training Tesseract · tesseract-ocr/tesseract Wiki · GitHub
フォントは入手できないので、とりあえず綺麗め・歪んでいないリザルトからフォントを切り出してみた
ファイルの命名規則は
(3文字の言語名).(フォント名(任意)).exp(インデックス番号)
らしい。 とりあえず言語名vol, フォント名digitとして学習させていく。
.boxファイルの編集
jTessBoxEditorでチマチマと各文字部分のBoxを定義していく。
trファイルの作成
$ tesseract vol.digit.exp0.png vol.digit.exp0 nobatch box.train.stderr
する。
FAIL! APPLY_BOXES: boxfile line 2/9 ((49,8),(83,44)): FAILURE! Couldn't find a matching blob FAIL! APPLY_BOXES: boxfile line 3/6 ((89,8),(123,44)): FAILURE! Couldn't find a matching blob FAIL! APPLY_BOXES: boxfile line 6/7 ((205,9),(231,39)): FAILURE! Couldn't find a matching blob FAIL! APPLY_BOXES: boxfile line 7/1 ((235,9),(261,39)): FAILURE! Couldn't find a matching blob APPLY_BOXES: Boxes read from boxfile: 8 Boxes failed resegmentation: 4 Found 4 good blobs. Leaving 3 unlabelled blobs in 0 words.
どうやら文字間の距離が近すぎると失敗するらしい。 画像を編集し試行錯誤すること数回
APPLY_BOXES: Boxes read from boxfile: 8 Found 8 good blobs. Generated training data for 1 words
成功したっぽい。
トレーニングデータ作成
unicharsetファイル作成
$ unicharset_extractor vol.digit.exp0.box
-bash: unicharset_extractor: command not found
トレーニング用のツールがなかったのでインストールする
$ brew uninstall tesseract Uninstalling /usr/local/Cellar/tesseract/3.04.01_2... (77 files, 70.6M) $ brew install --with-training-tools tesseract
$ unicharset_extractor vol.digit.exp0.box Extracting unicharset from vol.digit.exp0.box Wrote unicharset file ./unicharset.
font_propertiesファイル
$ echo "digit 0 0 0 0 0" > font_properties
学習する
$ mftraining -F font_properties -U unicharset vol.digit.exp0.tr Warning: No shape table file present: shapetable Reading vol.digit.exp0.tr ... Flat shape table summary: Number of shapes = 7 max unichars = 1 number with multiple unichars = 0 Warning: no protos/configs for Joined in CreateIntTemplates() Warning: no protos/configs for |Broken|0|1 in CreateIntTemplates() Done!
$ cntraining vol.digit.exp0.tr Reading vol.digit.exp0.tr ... Clustering ... Writing normproto ...
ここまでで必要なファイルの生成に成功したらしい。 いくつかのファイルをリネームする。
$ mv inttemp vol.inttemp $ mv pffmtable vol.pffmtable $ mv shapetable vol.shapetable $ mv normproto vol.normproto $ mv unicharset vol.unicharset
$ combine_tessdata vol. Combining tessdata files TessdataManager combined tesseract data files. Offset for type 0 (vol.config ) is -1 Offset for type 1 (vol.unicharset ) is 140 Offset for type 2 (vol.unicharambigs ) is -1 Offset for type 3 (vol.inttemp ) is 641 Offset for type 4 (vol.pffmtable ) is 128020 Offset for type 5 (vol.normproto ) is 128115 Offset for type 6 (vol.punc-dawg ) is -1 Offset for type 7 (vol.word-dawg ) is -1 Offset for type 8 (vol.number-dawg ) is -1 Offset for type 9 (vol.freq-dawg ) is -1 Offset for type 10 (vol.fixed-length-dawgs ) is -1 Offset for type 11 (vol.cube-unicharset ) is -1 Offset for type 12 (vol.cube-word-dawg ) is -1 Offset for type 13 (vol.shapetable ) is 129137 Offset for type 14 (vol.bigram-dawg ) is -1 Offset for type 15 (vol.unambig-dawg ) is -1 Offset for type 16 (vol.params-model ) is -1 Output vol.traineddata created successfully.
以上で漸くvol.traineddata
が生成された。
/usr/local/Cellar/tesseract/3.04.01_2/share/tessdata
にvol.traineddata
を移動し,OCRを試す。
結果
ひとまず学習に使った画像でテストしてみる
$ tesseract vol.digit.exp0.png -l vol output Tesseract Open Source OCR Engine v3.04.01 with Leptonica Info in fopenReadFromMemory: work-around: writing to a temp file $ cat output.txt 57712
微妙な結果になった
まとめ
とりあえずTesseractの学習の手順は理解したので、学習を進めていく。
学習を進めても精度が伸びないようであれば、v4.0の利用か自前でOCRを実装することになるだろう。
スコア部分の文字列切り出し
前回まででスコア表示部分のヘッダーが取れたので、その下のスコア表示部分を切り抜き、文字認識に繋げる
スコア表示部分切り抜き
- ヘッダ検出結果から、その下部の領域を適当に切り出す
- 欠けてしまうと文字認識が失敗するため、無駄な部分が入ろうとも大きめに切り出す方針
いい感じの切り抜き
別部分も入った切り抜き
2値化
文字位置特定や文字認識の処理を行うために2値化する。
対象画像の特性
- 背景白に青色・縁取りありの数字が並ぶ
- 切り抜きで余分な部分(青系統の色)が含まれる可能性が高い
- ゲームセンターごとに環境光の影響で色味が変わる
試した2値化処理
- 普通に
cv2.THRESH_OTSU
でcv2.threshold()
- 画像によって文字部分が欠けるなど安定しない
- 適応的2値化処理
cv2.adaptiveThreshold()
- 同上
- HSV変換→青色検出
- 環境光の影響で安定せず
- 白色検出→反転
- 同上
- (Bのみグレースケール) - (Gのみグレースケール) - (Rのみグレースケール)
- うまくいきそうな気がしたけどダメ
- 上記結果の組み合わせ(加算減算etc)
- ダメなものを重ねてもダメ
最終的な2値化処理
画像のRのみ取り出してグレースケール化
→大津の方法で2値化
が一番安定して文字部分を保持したまま2値化できた
red = img[:,:,2] ret,bin_red = cv2.threshold(red,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU) # オープニングで白色ノイズ除去 kernel = np.ones((3,3),np.uint8) bin_red = cv2.morphologyEx(bin_red, cv2.MORPH_OPEN, kernel)
結果
こんな感じ
文字位置特定
2値化結果にノイズが乗っていた時期から色々試していたため、かなり苦労した。
試したこと
- 輪郭抽出
問題点: - 2値化結果で1文字が2つの領域に分断されていると失敗
ヒストグラムから判断
X軸/Y軸ごとのヒストグラムを作成し、そこから文字位置を特定する
Y軸のヒストグラムから文字が存在する範囲のyを特定・切り出し
- 切り出した部分に対してのX軸方向ヒストグラムを作成・文字位置特定
文字以外の枠も検出されているが、仮にその後の文字認識処理においてこの部分が"1"と認識されたとしても、スコアの桁数が固定のため補正できると思う。
次
Tesseract-OCRを試したが認識率が安定しない。 学習データを集めてkNNで文字認識する。
あと横方向エッジ検出結果から画像の水平化もする必要がある
スコアヘッダ認識2
認識失敗時に回転させる処理を追加したら割とうまくいった。
import os import dlib import cv2 from scipy import ndimage IMG_DIR = './experiment_img/' cv2.namedWindow("img", cv2.WINDOW_NORMAL) detector = dlib.simple_object_detector("detector.svm") files = os.listdir(IMG_DIR) for file in files: ftitle, fext = os.path.splitext(file) if fext != '.jpg': continue # Load image abspath = os.path.abspath(IMG_DIR + file) print(abspath) img = cv2.imread(abspath) if img is None: print('Failed to read image') break dets = detector(img) if len(dets) != 0 : for d in dets: cv2.rectangle(img, (d.left(), d.top()), (d.right(), d.bottom()), (0, 0, 255), 2) # Show image cv2.imshow("img",img) cv2.waitKey(0) continue # Failed to Detecting -> rotate rIntr = 15 rStart = -30 rEnd = 30 for r in range(rStart, rEnd+1, rIntr): rotate_img = ndimage.rotate(img, r) dets = detector(rotate_img) if len(dets) != 0 : for d in dets: cv2.rectangle(rotate_img, (d.left(), d.top()), (d.right(), d.bottom()), (0, 0, 255), 2) # Show image cv2.imshow("img",rotate_img) cv2.waitKey(0) break cv2.destroyAllWindows()
結果
前回失敗していた斜めのスライドも検出成功
問題点
認識すべき場所が見当たらないときに端の真っ暗な部分を誤認識してしまうことがあった (特に180度回転時?)
→ 正立で検出
→ -30° 〜 +30°で検出を行うように変更したら割と上手くいった
(逆立ちしてリザルトを撮る人がいなければ大丈夫)
次回以降やること
その他
ネストが深くなるとpythonのブロック構造が分かりづらい。
何か上手い記法があったりしないか調べる
スコアヘッダ認識
やりたいこと
リザルト画像中からスコア表示部分のヘッダを認識する
手段
機械学習ライブラリdlibの物体検出モジュール(structural_object_detection_trainer)を試してみる。
前準備
- dlibのインストールに数回失敗(boostが入ってなかった)
- virtualenvを久しぶりに使ったので再度Google先生に教えを請う
- python3 + opencv3でコケる
python3 + opencv3
/usr/local/Cellar/opencv3/3.1.0_3/lib/
にpython3.5がなかった
→$ brew reinstall opencv3 --with-python2 --with-python3
してみる
(ついでに --with-contrib
)
→ ok
$ pip install numpy
virtualenvから使えるようにチマチマ
$ echo /usr/local/opt/opencv3/lib/python3.5/site-packages >> $(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")/opencv3.pth
データ
学習データ : 27 テストデータ : 14 ラベリングはimglabのGUIで行ったため、回転にうまく対応できているか不安
学習結果
Training accuracy: precision: 1, recall: 1, average precision: 1
Testing accuracy: precision: 1, recall: 1, average precision: 1
実験
また別のリザルトを拾ってきて、うまく認識できるか試す
import os import dlib import cv2 IMG_DIR = './experiment_img/' cv2.namedWindow("img", cv2.WINDOW_NORMAL) detector = dlib.simple_object_detector("detector.svm") files = os.listdir(IMG_DIR) for file in files: ftitle, fext = os.path.splitext(file) if fext != '.jpg': continue # Load image abspath = os.path.abspath(IMG_DIR + file) print(abspath) img = cv2.imread(abspath) if img == None: print('Failed to read image') break # Detecting dets = detector(img) for d in dets: cv2.rectangle(img, (d.left(), d.top()), (d.right(), d.bottom()), (0, 0, 255), 2) # Show image cv2.imshow("img",img) cv2.waitKey(0) cv2.destroyAllWindows()
成功例
失敗例
まとめ
回転に弱いので検出失敗したら画像を少しづつ回転させて検出させる
スコア認識
やりたいこと
リザルト画像からスコアを文字データとして取り出す。
画像の特徴
- どうやら未だボルテはe-amu連携でスコア投稿する機能がないらしい。
- → リザルトは全てユーザの撮影写真となるため、スコア表示部分位置や明るさ・画質等々がまちまち。
- フォントは固定
- SCORE と Hi_SCORE表示がある
方法
画像中からスコア部分の切り出し
- スコア表示部分の手掛かりとしてヘッダ(SCORE or HI-SCORE)を画像中から探索
- 探索結果をもとにその下部の領域を切り出す
OCR
フォントが固定のため、既成のOCR技術ではなく自前のOCRを実装する(Ikalogを参考に) Google Cloud Vision も試してみる
画像の収集
取り敢えずTwitter検索して手動で40枚ほど集めた。 そのうちクローラを実装する。