書類仕分けを自動化したい(その2)
概要
前回の続き。前回は丸検出を使って、書類仕分けを試みたが、今回は全く違う方法を使ってみた。問題設定
前回の反省点として、書類に書かれる丸は、手描きで行われるので、真円とはほど遠く、ちゃんとした曲線が見られない場合があった(楕円とか三角形とか)。
なので図形の形ではなく、ペンで黒く書かれたpixelの数を、丸が付いていない書類(’無’ のみの画像)と比べる事によって、’無’ の周りにペンで何かしらが書かれている事を認識させてみた。
やった事
- ‘無’ の周辺の空白部分を推定して切り取り
プログラムは以下の通り。
# coding: UTF-8 import cv2 import numpy as np import sys def patternCut(filepath): #画像をグレースケール(,0)で取得 img = cv2.imread(filepath,0) #切り取り画像の左上のpixel x, y = 720, 4070 #切り取り画像の幅と高さのpixel数 w = 775 h = 150 #画像から切り取り実行 roi = img[y:y+h, x:x+w] #画像から項目欄の左端の縦枠のpixelを推定。 leftPixel = -1 #画像上端から下方向にpixel行を取得。 for sh in range(h): #pixel行の左端から右方向にpixelを取得。 for sw in range(w): #取得したpixelが黒っぽい色をしていた時且つ、 #その下の50pixelの内、40pixelが黒っぽい色をしていた時の処理。 if roi[sh, sw] < 100 and 255*10 > np.sum(roi[sh:sh+50, sw]): leftPixel = sw break #leftPixelに数値が入ったら終了。 if leftPixel != -1: break print(leftPixel) #画像から項目欄の下端の横枠のpixelを推定。 underPixel = -1 #画面右端から左方向にpixel列を取得。 for sw in range(w-1, 0, -1): #pixel列の下端から上方向にpixelを取得。 for sh in range(h-1, 0, -1): #取得したpixelが黒っぽい色をしていた時且つ、 #その左の50pixelの内、40pixelが黒っぽい色をしていた時の処理。 if roi[sh, sw] < 100 and 255*10 > np.sum(roi[sh, sw-50:sw]): underPixel = sh break #underPixelに数値が入ったら終了。 if underPixel != -1: break print(underPixel) #縦枠と横枠が交わるpixelから内側に10pixel動かし、枠を画像から消去。 patternRoi = roi[underPixel-85:underPixel-10, leftPixel+10:leftPixel+400] #切り取った画像をウィンドウで出力。 cv2.imshow('detected circles',roi) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == "__main__": argvs = sys.argv if (len(argvs) <= 1): print('Usage: # python %s TestDataFilePath' % argvs[0]) quit() patternCut(argvs[1])
このプログラムでは、
まず、’無’ を含んだ項目の左黒枠と下黒枠を含むように、画像を切り取る。
こんな感じに。
次に、切り取った画像の上からpixel行を取得し、pixel行の左からpixelを調べる。
そして、黒っぽい所(pixelの数値が100以下)の所が真下に50pixel続いている所を左黒枠の列と推定する。
今度は、画像の右からpixel列を取得し、pixel列の下からpixelを調べて、同じように黒っぽい所が左に50pixel続いている所を下黒枠の行と推定する。
最後に、推定した左黒枠の列と下黒枠の行を基に、左黒枠・下黒枠を含まないように切り取った。
結果は以下の通り。
- 画像のpixelの数値の合計を算出
プログラムは以下の通り。
# coding: UTF-8 import cv2 import numpy as np import sys def margin(filepath): #画像をグレースケール(,0)で取得 img = cv2.imread(filepath,0) print(np.sum(img)) if __name__ == "__main__": argvs = sys.argv if (len(argvs) <= 1): print('Usage: # python %s TestDataFilePath' % argvs[0]) quit() margin(argvs[1])
これは、画像のpixelをグレースケール変換して、その数値の合計を取得しただけ。
何故こんな事をしたかと言うと、グレースケール変換した後のpixelの数値では、
白(空白)は、255 黒(字が書いてある所)は、0
なので、画像に黒が追加される分だけ、pixelの数値の合計が少なくなっていく。
よって、丸が付いていない(’無’ のみが黒い)画像と丸が付いている(’無’ と丸が黒い)画像のpixelの数値の合計を比べる事によって、画像に丸が付いている(’無’ 以外に何か書かれている)かを推定できると思ったからである。
- 丸が付いていない画像と丸が付いている画像との閾値(差)を考察
丸が付いている画像では、’無’ を囲むように丸を付けると仮定した場合、それが出来る最小の円を考えてみた。
まず、’無’ の直径がどの位あるのかを見てみると、60pixel位だと言うことが分かった。
こんな感じ。
よって、’無’ を囲む最小の円は、60 * 3(直径 * 円周率)と言うことにした。
また、円が書かれた所は、空白のpixelが黒のpixelに変わるので、255→0の変化があると仮定すると、最小の円が書かれた時のpixelの数値の変化は、255 * (60 * 3)となる。
さらに、テストデータを確認すると、ペンで書かれた線の太さはおよそ3~5pixelだったので、3を零さないように閾値は、255 * (60 * 3) * 2 = 91800とした。
また、’無’ のみの画像のpixelの数値の合計は7066580~7087520の範囲で、最大で20940のブレが検出されたが、これは上で求めた閾値(255 * (60 * 3) * 2 = 91800)と比べると、小さい物だと考えられるので、誤検出の要因にはなりにくいと考えた。
これらを踏まえると、丸検出のif文は以下の通りになる。
if abs(“’無’ のみの画像のpixelの数値の合計” - np.sum(“画像のピクセルデータ”)) > 255*60*3*2 and “’無’ のみの画像のpixelの数値の合計” > np.sum(“画像のピクセルデータ”):
結果
テストデータは以下の通り。
‘無’ に丸が付いている書類: 289 ‘無’ に丸が付いていない書類: 35
そして、解析結果は以下の通り。
‘無’ に丸が検出された書類: 289 = 0[’無’ に丸が付いていない書類(誤検出)] + 289[‘無’ に丸が付いている書類] ‘無’ に丸が検出されなかった書類: 35 = 35[’無’ に丸が付いていない書類] + 0[‘無’ に丸が付いている書類(誤検出)]
今回貰ったデータ(データ総数: 324)では、誤検出が無かった。
感想
当事者から話を聞いて、貰ったデータ見ながら自分でアルゴリズム考えて、実際にプログラム書いて、データ使って実験して、上手く動いた時までは楽しかった。
だけど、出来た物を人(今回の場合はバイト先の先輩)に説明したり、使い方のREADME書いたり、今みたいにブログ書いたり(途中でグダって、結果欄辺りがなおざりに…)、とにかく自分はアウトプットの作業を凄く面倒ぐさがってしまうなと。
もう出来て動いてんだからそれで良いじゃんって思ってしまう…。
だけど本当は、誰かに自分の作った物・してきた事の良さとか考えた事なんかを共有して、人から意見貰った方が良いんだよな。
じゃないとただの自己満足で終わってしまう…。それはとても勿体無いとは思ってる(なかなか行動に移せないで居るけど)。
だから、少しでもやっていけたらなぁと思い、これからは些細な事でもブログに書いていこうと思います。
最後に、実験に使ったプログラムはgithubに上げたので、興味あったら見てみて下さい。