備忘録

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

ディープラーニングするぞ

久しぶりに時間が取れたので文字切り出し・ノイズ低減(削除)・モアレ除去など試して見たがなかなかうまくいかない。 そもそも環境光の異なる(予測できない)状況下において、 古典的画像処理で全ての状況に対応しようという試みが間違っているのだろうと思う。

ということでDeepLearningでなんとかすることにした。

文字認識するだけならMNISTやりましたという記事を漁ればゴロゴロ情報が出てくるのだが、 一度体系的に学習したかったので本を読む。

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)

ラボにころがってたこれ。

やるぞ。

Kodak PIXPRO SP360-4K のWifiストリーミング環境構築

Kodak社のPIXPRO SP360-4Kは1台で半天球、2台合わせて全天球映像を撮影可能なカメラである。

PIXPRO - マスプロ電工|MASPRO

UVC規格対応でUSBケーブルによる接続でWebカメラとしても利用できるが、 Wifiルータを内蔵しており映像をストリーミングできる機能が付いている。

下記のサイトを参考にMacでの利用を試みたが微妙にハマったので忘れないようにメモしておく。

tks-yoshinaga.hatenablog.com

yaaam.blog.jp

環境

Mac OS Sierra + Python3(3.5.2) + openCV3(3.2.0)

とりあえず繋いでみる

cap = cv2.VideoCapture("http://172.16.0.254:9176/")
cap is None:
        print("Failed to open the camera.")
while True:
        ret, frame = self.cap.read()
        if not ret:
            print("Failed to capture the image.")
            return None
        cv2.imshow("image", frame)
$ python capture.py 
VIDEOIO(cvCreateFileCapture_AVFoundation (filename)): raised unknown C++ exception!

Failed to capture the image.

原因

どうやらffmpegが無かったのが原因らしい。

$brew reinstall --force opencv3 --with-ffmpeg --with-python3

virtualenvを利用しているので再インストールしたOpenCV3へのシンボリックリンクを作り直す。

blog.ymyzk.com

その他

参考にしたサイトには受信側IPを172.16.0.1に固定するように書いてあったが、 自動設定でも問題なく接続できた。(むしろ固定すると接続に失敗した)

Pythonによるスクレイピング&機械学習 開発テクニックを買った

夕飯の買い物ついでにふらりと寄った書店で技術書を買った。

スクレイピングもTensorflowもやってみたかったのでちょうどいいと思い購入。 Seleniumってスクレイピングにも使えるのね(考えてみればそりゃそうだけど)

コンマイの楽曲データをスクレイピングするプログラムでもサクッと書いてみようと思う。 画像認証(reCAPTCHAとかそういう意味の)の突破はやらない。

adaptiveThresholdさん見直し

2値化の処理を探っているとき、cv2.adaptivethreshold()があまり使えないように思っていたが、ただ単に自分のチューニング不足だった。

cv2.adaptivethreshold(image, method, blocksize, c)

画像のしきい値処理 — OpenCV-Python Tutorials 1 documentation

Block Size - しきい値計算に使用する近傍領域のサイズ.1より大きい奇数を指定する必要があります.

C - 計算されたしきい値から引く定数です

Cの存在意義を理解していなかったが、文字領域の抽出においてはこいつを適切にチューニングする必要があった。

OpenCvSharpをつかう その15(適応的閾値処理) - schima.hatenablog.com

減算定数の意味

最後の減算定数は何のためにあるのでしょうか。

(中略)

文字が有る領域: 周囲の画素値はバラエティ豊か(白地に黒い細い線、で構成されるので)

文字が無い領域: 周囲の画素値はほぼ同じ(周りじゅうが白)

周り中が似たような色のとき、減算定数が有ることで、減算後は対象ピクセル閾値を上回ることになり、白くなります。

これにより、背景領域では多少のノイズ・色の揺らぎに負けずに白で塗りつぶしやすくし、文字領域では黒いエッジを残しやすくなります。賢いですね。

なるほど賢いので早速実験してみた。

コード

def binalizeByAdaptive(img):
    r = img.copy()

    # R, G値のみ取り出しグレースケール化
    green = r[:,:,1]
    red = r[:,:,2]
    redGreen = cv2.addWeighted(red, 0.5, green, 0.5, 0)

    # binalize
    th_red = cv2.adaptiveThreshold(redGreen,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\
            cv2.THRESH_BINARY_INV,21,20)
    
    # cleaning noise by opening
    kernel = np.ones((1,1),np.uint8)
    th_red = cv2.morphologyEx(th_red, cv2.MORPH_OPEN, kernel)

    cv2.imshow("binalize", th_red)
    # cv2.waitKey(0)

    return th_red

blockSize, Cの値は試行錯誤した結果の値である。

結果

成功例

f:id:reverent_f:20170124185128p:plain:w250 f:id:reverent_f:20170124185130p:plain:w250

文字の縁取り部分まで綺麗に検出できている。 また、0,8,9などの文字中の空洞部分も比較的綺麗に見える!

失敗例

  • 減算定数

f:id:reverent_f:20170124185156p:plain:w250

減算定数が大きすぎることで文字の中心部分が潰れた。 これは検出に失敗したら減算定数を変化させてもう一度認識、とすれば対応できそう。

  • モアレ

f:id:reverent_f:20170124185235p:plain:w250 f:id:reverent_f:20170124185238p:plain:w250

モアレが発生している画像だとノイズが多量にのってしまう。 フーリエ変換して高周波成分を取り除く、までやるのは難しいか … ?

まとめ

やはり適応的2値化処理は照明条件の変化に強く、綺麗に文字の検出ができる!

ただし、文字の縁も綺麗に残るので、文字の切り出し処理を修正しなければならない(嬉しい悲鳴)

ただし、モアレに非常に弱いことが判明した。 有効なモアレの除去方法が思いつかないため、モアレが発生している場合は従来の2値化処理を使うしかなさそうだ。

その場合は教師データを2値化の処理ごとに2つ用意しなければならない … 。

雑記

どこまでの画像を認識対象とするか、そろそろ限定しなければならない。 そろそろサイトの作成にも取り掛かりたい。

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様の力でゴリ押すかしようと思う。

macOS10.12 x Objective-C x 32bit(i386)でビルドエラー

ちょっとした理由があり、SwiftではなくObjective-COSXのアプリケーションを書いている。

初遭遇するタイプのハマり方をしたのでメモ書き程度に残しておく。

問題

f:id:reverent_f:20170119190333p:plain

  • Architectures を64bit(x86_64)なら問題なくビルドできる
  • Architectures が32bit(i386)の時のみエラーが発生する

エラー内容

@property (nonatomic, strong) HOGE *hoge;

しているのに

_hoge = fuga;

Use of undeclared identifier '_hoge'.

自動生成されるはずのsetter/getterが作られていない?

stackoverflow.com

5年前の記事だけど、そういうことらしい。 32bit版と64bit版で挙動が異なるなんてことがあるのだなあと勉強になった。 (バグ?)

まとめ

  • Architecturesの違いでSDKの動作が変わるなんてことがあるらしい
  • 思った以上にMacOSSDKの仕様変更の影響は大きい
  • 1から書くのならあまり問題にはならないけれど、古いコードを新SDKに対応させるのはとても大変だ
  • 早くAWSOSXSDKをリリースしてほしい

文字位置切り抜き

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

スコア部分の切り抜き

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

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

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でやる予定。