月を眺める孤島

多分デレステとポケモンの話がメインのブログです

Pythonでデレステのガシャのスクショからアイドルの名前を読み取ろうと頑張った

こんばんは、むぅんです。
僕はこれまで引いたガシャの記録を全て取っており、定期的にExcelにデータを入力しては眺めて楽しんでいたのですが最近サボりまくったせいでスクショが溜まりに溜まってえらいことになってしまいました。
特におはガシャはその場ですぐに記録すればいいものをあろうことか毎日スクショで済ませてしまったが故に、今からこれを手動で打ち込むのはExcelの入力補完を使っても流石にしんどいものがあります。

そこで、Pythonで画像を頑張って加工して文字を自動で読み取れるようにしたら楽なんじゃね?という結論に至ったのでプログラムを組んでみました。
今回は、TesseractというGoogleによって開発されているOCR光学文字認識)エンジンを用いて画像から名前を出力するのを目標とします。画像の加工は皆大好きOpenCVです。
なお、Pythonバージョンは3.6です。

f:id:shinemoon227:20181017204950p:plain


これから、このひたすらコンビニと家を往復し天井をオーバーした末に拝むことができたスクショから「姫川友紀」という文字列を出力していくまでの流れを見ていきます。

f:id:shinemoon227:20181017195249p:plain



from PIL import Image
import sys
import cv2
import numpy as np
import math
import pyocr
import pyocr.builders

まずは必要なものをimportしていきます。OpenCV以外にもTesseractを動かすためのpyocrが必要となるのでそちらも合わせてインストールしておきます(日本語を読むことになるので、Tesseractは日本語のライブラリも導入しておく必要があります)。pip installで簡単に導入できるので詳細は各自で調べてください。
また、行列の計算を行うのでnumpyも必要です。

def contrast(image, a):
	lut = [np.uint8(255.0 / (1 + math.exp(-a * (i - 128.) / 255.))) for i in range(256)] 
	result_image = np.array([lut[value] for value in image.flat], dtype=np.uint8)
	result_image = result_image.reshape(image.shape)
	return result_image

OpenCVにはコントラストを調整するための関数がないようなのでここで作っておきます。こちらのコードをお借りしました。

path_input = 'IMG_0181.png'
path_output = 'IMG_0181_P.png'

画像のパスをここで指定します。inputは読み込むスクショ、outputは加工した後の画像の保存先です。間違って既に存在する画像のパスをoutputに指定してしまうと容赦なく上書きされてしまうので気をつけてください。


ss = cv2.imread(path_input, 0)
size = tuple([ss.shape[1], ss.shape[0]])
center = tuple([int(size[0]/2), int(size[1]/2)])

f:id:shinemoon227:20181017195355p:plain

画像を読み込みます。ここで、cv2.imreadの第2引数に0を指定することでグレースケールで画像を読み込むことができます。画像を加工するにあたって、このようにグレースケールで読み込むと後から大変都合が良いので忘れずに指定しておきましょう。

angle = -6
rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
img_rot = cv2.warpAffine(ss, rotation_matrix, size, flags=cv2.INTER_CUBIC)

f:id:shinemoon227:20181017195534p:plain

続いて画像を6°だけ時計回りに回転させます。cv2.getRotationMatrix2Dで回転行列を作成でき、アフィン変換を容易に行うことができます。

pos = [[902, 1178], [1942, 1316]] # 切り取る矩形の左上と右下のx座標とy座標の組
img_cut = img_rot[pos[0][1]:pos[1][1], pos[0][0]:pos[1][0]]

f:id:shinemoon227:20181017195757p:plain

ここではスクショから名前の部分だけを切り取る作業を行います。左上の座標を \((x_1, y_1)\) 、右下の座標を \((x_2, y_2)\) とすると、リストposは \([[x_1, y_1], [x_2, y_2]]\) となります。勿論この通りである必要はないので各自分かりやすいように書き換えてもらっても大丈夫です。
なお、このまま実行するとiPadで撮影した画像に対応した箇所が切り取られるので、各自の機種の座標を自分で調べて入力しておきます。ここでの座標は回転後の座標になるので、アフィン変換を行った直後に一度cv2.imwriteで画像を書き出してみて、その画像を元に座標を調べておくのが良いかもしれません。
ちなみに、横にかなり長く切り取っているのはイヴちゃんのためです。ただし、残念ながらTesseractではロダンNTLGで書かれた「ン」を認識できないらしく、「イヴ・サ〕タクロ一ス」となってしまいます…。

con = 10
img_con = contrast(img_cut, con)

f:id:shinemoon227:20181017195819p:plain

ここから画像の加工に入っていきます。まずは先程定義した関数を用いてコントラストを調整します。パラメータは自分で何度も試行錯誤を行った結果10に落ち着きましたが、よりよい値を見つけた方はコメントで教えてください。

gamma = 0.1
imax = img_con.max()
img_gam = imax * (img_con / imax)**(1/gamma)

f:id:shinemoon227:20181017195834p:plain

続いてガンマ補正を加えます。この段階で既に文字だけ非常にくっきり表示できていることが分かります。CuとCoはここまで行ったコントラストの調整とガンマ補正だけで既に文字を読み取ることが可能です(con=50、gamma=0.01で動作確認済)が、Paは文字が薄く同じように調整すると上半分が欠けて読み取れなくなるのでここから更に調整を加えていきます。CuとCoの画像だと無駄な処理となってしまいますが、今回は属性による場合分けは考えないことにします。

thresh = 80
max_pixel = 255
ret, img_dst = cv2.threshold(img_gam, thresh, max_pixel, cv2.THRESH_BINARY_INV)

f:id:shinemoon227:20181017195847p:plain

ここで画像を2値化します。その際、後に行う処理のために白黒を反転しておきます。cv2.thresholdでcv2.THRESH_BINARY_INVを指定するとネガポジ反転した状態で2値化してくれます。この指定方法を知る前は普通に2値化したあとcv2.bitwise_notで反転しようと考えていたのですが、この関数で2値化したときRGB値がちょうど255と0でなくわずかにずれた値となり、NOTをかけると負数とnanになる(→画像が真っ黒になる。もう一度NOTをかけると復元は可)という不可解な現象が起きたため断念しました。2値化直後に一度png画像として出力してしまえば恐らくこの問題は解決できますが、それよりは引数で指定するほうがわかりやすく処理も高速なのでこちらの方法をオススメします。

m = 3
move_matrix = np.float32([[1, 0, m],[0, 1, m]])
size2 = tuple([img_dst.shape[1], img_dst.shape[0]])
img_mov = cv2.warpAffine(img_dst, move_matrix, size2, flags=cv2.INTER_CUBIC)

f:id:shinemoon227:20181017195902p:plain

ここまで加工した画像を複製し、3pxだけ右下にずらしたものを用意します。2×3の行列を用意して平行移動を行います。

img_and = cv2.bitwise_and(img_dst, img_mov)
cv2.imwrite(path_output, img_and)

f:id:shinemoon227:20181017195931p:plain

最後にずらす前とずらした後の画像のANDをとります。すると、元々文字の影だった部分が見事に消えてなくなります。先程2値化したときにネガポジ反転を行ったのは白い文字(1)のみを残して、影だった部分を黒い文字の縁(0)と重ねて消去するためです。この影だった部分が残っていると正しく文字を読み取れません(先程con=50、gamma=0.01でCuとCoは2値化せずとも検出可能と書きましたが、これはその補正をかけた段階で影が消えてなくなるためです。ただ、Paの文字の上半分は影と同程度の明度しかないので影と一緒に消えてしまいます。それを防ぐため、コントラストの調整やガンマ補正は弱めにかけて後からずらして重ねる方法で影だけを除去しました)。
これで見事に文字のみを真っ白な状態で抜き出せたので、これを画像として出力します。

tools = pyocr.get_available_tools()
if len(tools) == 0:
	print("No OCR tool found")
	sys.exit(1)

tool = tools[0]

txt = tool.image_to_string(
	Image.open(path_output),
	lang='jpn',
	builder=pyocr.builders.TextBuilder()
)
txt = txt.replace(' ', '')
txt = txt.replace(' ', '')
print(txt)

最後に文字をpyocrで読み取ります。関数1つで読み取れるのはすごいですよね。
返ってきた文字列は空白を含んでいることがあるのでreplaceで除去してから出力するようにします。


それでは早速実行してみましょう。

$ python3 idolName_OCR.py
姫川友紀

いけました!こんな感じでアイドルの名前を出力できます。
他の属性のアイドルも見ていきましょう。



f:id:shinemoon227:20181017201636p:plain
f:id:shinemoon227:20181017201838p:plain

$ python3 idolName_OCR.py
小早丿ーー紗枝

姫川の「川」がいけて小早川の「川」がいけないのはなんで???非常に惜しいんですけどね…



f:id:shinemoon227:20181017201817p:plain
f:id:shinemoon227:20181017201854p:plain

$ python3 idolName_OCR.py
、森久保乃々_}_`

こっちはこっちですごいことになってます。ただ、これは半角文字を全て取り除けばいいだけなのでまだ何とかなりそうです。



というわけで、OCRで完璧に名前を読むことが如何に難しいかということがよく分かる結果となりました。183人全員試すのは難しいですが、1文字も間違えずに出力できるアイドルはそこまで多くないかと思われます。
勿論スクショを撮ったタイミングが悪かったりする(キラキラしてたり白みがかってたり)とこれより更に精度は落ちますが、そうでなくてもこの結果なのでテンプレートマッチングを使って一致率が一定以上だった文字列を採用する方法のほうが確実でしょう。

{
\displaystyle
\begin{equation}
\end{equation}
}