備忘録

弱小院生のメモ

文字位置切り抜き

学習用の画像を集めていくうちに文字位置の切り抜きに失敗することが多くなり、 このままではロバストな認識は行えないということで改善を試みた。

スコア部分の切り抜き

画像によってヘッダの認識にずれがあり、切り出したスコア部分に欠けが生じることがあった。

この処理で欠けると後の処理では回復できないので、かなり大きめに領域を取得する。

f:id:reverent_f:20170114200349p:plain

2値化

P-タイル法を試してみたが、安定しないので今まで通り R値とG値の合算結果に大津のアルゴリズムを適用して2値化する。

f:id:reverent_f:20170114200617p:plain

スコア文字部分の絞り込み

大きく切り抜いた分だけ余計な領域が発生する。 幸い、切り抜きたい文字の周りには黒い余白が現れるためこれを手掛かりに絞り込みを行う。

  1. 画像の上下左右に固定幅の白枠をつける

このとき、元から画像に存在している枠によって、目的文字部分と(あれば)右の余計なコンボ数部分が隔てられる。

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)

f:id:reverent_f:20170114201337p:plain

各文字特定

以前の記事で書いたヒストグラムを使った処理によって文字候補をリストアップする。 以前までの処理では文字以外の部分もリストアップされていたが、文字候補部分のアスペクト比からさらに候補を絞り込む。

f:id:reverent_f:20170114201559p:plain

以前より閾値を厳しく設定し、文字候補に漏れがあっても文字同士が連結しないようにする。

候補から外れた文字部分の補完

文字候補として外れてしまった文字(上図なら 7 )を文字の位置関係から推測する。

ほとんどの場合スコア先頭に来る 0 , 9 は上記の処理で文字候補から外れることはほぼ無い。 そこで、スコア先頭文字は確実に候補中にあるとして、右方向に8文字あると仮定し補完する。 先頭5文字と後半3文字でフォントサイズが異なることに気をつける。 細かい推測方法は省略する。

f:id:reverent_f:20170114201818p:plain f:id:reverent_f:20170114202302p:plain f:id:reverent_f:20170114202305p:plain

整形

あとは各文字の上下の余白を削除・等サイズにリサイズして完成

まとめ

やっとこれで学習用データの切り出しができるようになった! 文字位置の特定がロバストにできれば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)

結果

f:id:reverent_f:20170124190820p:plain:w250

f:id:reverent_f:20170124190823p:plain:w250

f:id:reverent_f:20170124190825p:plain:w250

f:id:reverent_f:20170124190853p:plain:w250

見た目上あまり変化がないように見えるが、後続の処理の精度は向上した

Tesseract-OCRの学習を試してみる

文字位置特定→文字画像を切り出し→kNNで文字認識

という流れを踏む予定だったが、安定した文字位置の特定処理が難しいのでTesseract-OCRを試してみる。

学習前の状態でOCR

f:id:reverent_f:20170110144043p:plain

  • 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

Tesseract-OCRの学習 - はだしの元さん

フォントは入手できないので、とりあえず綺麗め・歪んでいないリザルトからフォントを切り出してみた

f:id:reverent_f:20170110183150p:plain

ファイルの命名規則

(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/tessdatavol.traineddataを移動し,OCRを試す。

結果

ひとまず学習に使った画像でテストしてみる f:id:reverent_f:20170110183150p:plain

$ 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を実装することになるだろう。

スコア部分の文字列切り出し

前回まででスコア表示部分のヘッダーが取れたので、その下のスコア表示部分を切り抜き、文字認識に繋げる

スコア表示部分切り抜き

  • ヘッダ検出結果から、その下部の領域を適当に切り出す
  • 欠けてしまうと文字認識が失敗するため、無駄な部分が入ろうとも大きめに切り出す方針

いい感じの切り抜き f:id:reverent_f:20170109154200p:plain

別部分も入った切り抜き f:id:reverent_f:20170109154205p:plain

2値化

文字位置特定や文字認識の処理を行うために2値化する。

対象画像の特性

  • 背景白に青色・縁取りありの数字が並ぶ
  • 切り抜きで余分な部分(青系統の色)が含まれる可能性が高い
  • ゲームセンターごとに環境光の影響で色味が変わる

試した2値化処理

  • 普通にcv2.THRESH_OTSUcv2.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)

結果

こんな感じ

f:id:reverent_f:20170109155622p:plain

f:id:reverent_f:20170109155629p:plain

文字位置特定

2値化結果にノイズが乗っていた時期から色々試していたため、かなり苦労した。

試したこと

  1. 輪郭抽出

輪郭抽出 → 矩形に近似 → アスペクト比から文字候補検出

f:id:reverent_f:20170109160558p:plain f:id:reverent_f:20170109160602p:plain

問題点: - 2値化結果で1文字が2つの領域に分断されていると失敗

  1. ヒストグラムから判断

    X軸/Y軸ごとのヒストグラムを作成し、そこから文字位置を特定する

  2. Y軸のヒストグラムから文字が存在する範囲のyを特定・切り出し

  3. 切り出した部分に対してのX軸方向ヒストグラムを作成・文字位置特定

f:id:reverent_f:20170109161155p:plain f:id:reverent_f:20170109161158p:plain

文字以外の枠も検出されているが、仮にその後の文字認識処理においてこの部分が"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()

結果

f:id:reverent_f:20170102234301p:plain

前回失敗していた斜めのスライドも検出成功

問題点

認識すべき場所が見当たらないときに端の真っ暗な部分を誤認識してしまうことがあった (特に180度回転時?)

→ 正立で検出

→ -30° 〜 +30°で検出を行うように変更したら割と上手くいった

 (逆立ちしてリザルトを撮る人がいなければ大丈夫)

次回以降やること

  • その他のヘッダ部分用の検出器をチマチマ作成
  • スコア部分の切り出し・OCR
  • 鬼門の曲認識 ( タイトルOCRは辛そう ジャケット部分を切り出して特徴量で認識か…? )

その他

ネストが深くなるとpythonのブロック構造が分かりづらい。

何か上手い記法があったりしないか調べる

スコアヘッダ認識

やりたいこと

リザルト画像中からスコア表示部分のヘッダを認識する

手段

機械学習ライブラリdlibの物体検出モジュール(structural_object_detection_trainer)を試してみる。

qiita.com

前準備

  • 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

blog.amedama.jp

データ

学習データ : 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()
成功例

f:id:reverent_f:20170102225959p:plain

失敗例

f:id:reverent_f:20170102230019p:plain

まとめ

回転に弱いので検出失敗したら画像を少しづつ回転させて検出させる

スコア認識

やりたいこと

リザルト画像からスコアを文字データとして取り出す。

画像の特徴

  • どうやら未だボルテはe-amu連携でスコア投稿する機能がないらしい。
  • → リザルトは全てユーザの撮影写真となるため、スコア表示部分位置や明るさ・画質等々がまちまち。
  • フォントは固定
  • SCORE と Hi_SCORE表示がある

方法

画像中からスコア部分の切り出し
  1. スコア表示部分の手掛かりとしてヘッダ(SCORE or HI-SCORE)を画像中から探索
  2. 探索結果をもとにその下部の領域を切り出す
OCR

フォントが固定のため、既成のOCR技術ではなく自前のOCRを実装する(Ikalogを参考に) Google Cloud Vision も試してみる

画像の収集

取り敢えずTwitter検索して手動で40枚ほど集めた。 そのうちクローラを実装する。