2017年12月24日日曜日

AA-Camera を作る (画像変換編)

この記事は、ケーシーエスキャロット Advent Calendar 2017 の24日目の記事になります。

画像変換

ソースコード

いよいよ画像変換です。前回までの作業で、撮影した画像をそのままファイルに保存するところまではできているので、今回はその画像ファイルを引数に渡されたらテキストに変換して出力するスクリプトを書きます。ソースコードは全部で 100 行もありませんので、全部載せました。順を追ってざっくり解説していきます。
  1 #!/usr/local/bin/python
  2 
  3 import os
  4 import sys
  5 import time
  6 import math
  7 import numpy
  8 import cv2
  9 from PIL import Image, ImageDraw, ImageFont, ImageFilter
 10 from PIL.ImageFont import FreeTypeFont
 11 
 12 args = sys.argv
 13 if (len(args) != 2):
 14     print('Usage: translate_to_aa.py input_image_file')
 15     quit()
 16 
 17 image_file = args[1]
 18 font_file = "/PathToFontFile/OsakaMono.ttf"
 19 
はじめの方の import 文は必要なものを import しているだけですので、説明は省略。
12〜15行目は、引数のチェックです。引数が足りなければエラーにしていますが、簡単なチェックしかしていません。
17行目は、その引数を入力画像ファイル名と信じて image_file 変数に格納しています。ま、お遊びプログラムなので。
18行目は、システムにインストールされているフォントファイルをひとつ定義しています。ここでは等幅フォントを指定します。ここポイントです。
 20 def generateDictAscii(dictAscii, size):
 21     image_font = ImageFont.truetype(font_file, size)
 22     w, h = image_font.getsize('Q')
 23 
 24     buf_image = "./buf.jpg"
 25     for ch in " .-|+*/\\^_'#!":
 26         text_canvas = Image.new("RGB", (w, h), (255, 255, 255))
 27         draw = ImageDraw.Draw(text_canvas)
 28         draw.text((0, -2), ch, font=image_font, fill='#000')
 29         text_canvas.convert('L').save(buf_image, 'JPEG', quality=100, optimize=True)
 30         tm = Image.open(buf_image, "r")
 31         dictAscii[ch] = numpy.asarray(tm)
 32     os.remove(buf_image)
 33 
 34     return w, h
 35 
やっと関数が出てきました。あとで、入力画像を1文字分の領域ずつに分けて、その領域と文字画像を比較する処理を行いますので、比較対象の文字を一文字ずつ画像データとして保持しておきます。python では辞書で管理するのが妥当でしょう。第一引数には空の辞書が渡ってくることを期待しています。第二引数はフォントのサイズが指定されることを期待しています。
関数の最後に w, h という2つの値を返していますが、これは、一文字分の画像データを作成した時の幅と高さです。第二引数に指定されたフォントサイズによって結果が変わってきます。
25行目の for 文がちょっと読みにくいですが、" "(スペース), "/"(スラッシュ), "+"(プラス) 等の、絵を描画するのに使えそうな12文字を1文字ずつ処理しています。
一番やりたいことは、31行目の代入です。文字コードをキーとして、その画像データを値として、12文字分の辞書データを作成しているわけです。
 36 def matchKeyBySAD(dictAscii, imgarray):
 37     minvar = 255 * 255
 38     minkey = ""
 39     var = minvar
 40     for key, value in dictAscii.iteritems():
 41         var = numpy.var(numpy.absolute(imgarray - value))
 42         if var <= minvar:
 43             minvar = var
 44             minkey = key
 45    
 46     return minkey
 47 
いよいよ一番大事なアルゴリズムの登場です。第一引数は先ほど作成した辞書データを想定しています。第二引数には、入力画像データを1文字分のサイズに分割したものが渡ってきます。第二引数で指定されたデータにもっとも近い文字を判定するのが、この関数の役割です。この関数では、SAD(Sum of Absolute Difference) を採用しました。入力画像も比較対象の文字画像も同じサイズの行列データです。python では行列のまま高速に演算できますので単純に引き算をしたいところですが、プラスになる場所とマイナスになる場所が相殺されてしまうと、似た画像として認識されかねません。そこで、差分の絶対値を使うことにしました。
式で表すと以下のようになります。Iは入力画像、Tは比較対象の文字画像です。
ただ、これはちょっと処理に時間がかかりすぎました。そのため、もう一つ比較アルゴリズムを作成しましたのでそちらも書いておきます。
 48 def matchKeyByMOMENT(dictAscii, imgarray):
 49     matchkey = ""
 50     minmatched = 1
 51     for key, value in dictAscii.iteritems():
 52         matched = cv2.matchShapes(imgarray, value, 3, 0.0)
 53         if matched <= minmatched:
 54             minmatched = matched
 55             matchkey = key
 56 
 57     return matchkey
 58
こちらは、opencv という画像処理・画像解析・機械学習などの機能をもったライブラリを使用しています。こんなお遊びプログラムに使うのが申し訳ないくらい良くできたライブラリです。matchShapse() 関数で2つの画像を比較し、戻り値が小さければ小さいほど良く似ている画像であるということを意味しています。こちらの方が早いので、以下の main 関数ではこちらのアルゴリズムを使っています。
 59 if __name__ == '__main__':
 60 
 61     dictAscii = {}
 62     w, h = generateDictAscii(dictAscii, 12)
 63     filename, suffix = os.path.splitext(image_file)
 64 
 65     img = cv2.imread(image_file)
 66     img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
 67     img_gray = cv2.GaussianBlur(img_gray, (3, 3), 0)
 68     canny = cv2.Canny(img_gray, 30, 80)
 69 
 70     cv2.imwrite(filename + "-canny" + suffix, cv2.bitwise_not(canny))
 71     im = Image.open(filename + "-canny" + suffix, "r")
 72 
 73     imgarray = numpy.asarray(im)
 74     imgarray.flags.writeable = True
 75 
 76     x = 0
 77     y = 0
 78     counter = 0
 79 
 80     while (y+h) < im.size[1]:
 81         while (x+w) < im.size[0]:
 82 #           bestmatchkey = matchKeyBySAD(dictAscii, imgarray[y:y+h, x:x+w])
 83             bestmatchkey = matchKeyByMOMENT(dictAscii, imgarray[y:y+h, x:x+w])
 84             imgarray[y:y+h,x:x+w] = dictAscii[bestmatchkey]
 85 
 86             sys.stdout.write(bestmatchkey)
 87 
 88             x += w
 89         x = 0
 90         y += h
 91         sys.stdout.write("\n")
 92 
 93     pillimg = Image.fromarray(numpy.uint8(imgarray))
 94     pillimg.save(filename + "-text" + suffix, "JPEG", quality=100, optimize=True)
さ。やっと main 関数です。ここでのポイントは入力画像の変換です。カラーの画像データをそのまま文字データと比較しても期待通りにはいかないので、まずは 66行目でグレースケールに変換しています。さらに、67行目でガウスぼかしをして、68行目でエッジ検出しています。canny というのはエッジ検出の一つのアルゴリズムですが、これをやる前にあえてぼかしておくことで余計なノイズを減らすことができて、より重要なエッジだけが拾えるようになります。

この画像が、

エッジ検出するとこうなるわけです。
背中の羽のあたりを拡大してみましょう。

こんな感じの線で描かれるのが canny の効果です。
プログラムに戻りましょう。80行目と81行目のループは、エッジ検出した画像を1文字サイズごとに分割しながら文字画像と比較していくためのループです。83行目で一番似ている文字を調べて、それを86行目で出力します。これがやりたかった。
最終的に出力されたテキストデータを表示すると、

こうなりますが、小さすぎて canny との違いがわかりにくいですね。
同じように背中の羽のあたりを拡大してみましょう。



あとは、このスクリプトを前回の写真撮影スクリプトの中から呼び出してやれば完成です。スイッチを押してパシャっと撮影すれば、テキストが出力されます。標準出力に出力していますので、リダイレクトしてファイルに保存して tar で圧縮すればかなり小さなファイルとして保存できます。需要はないでしょうけど。。。

と、こんな終わり方では残念なので、「概要編」の最後に載せたテキスト画像の元になったカラー画像を載せておきます。



まとめ

やっぱり、カラー写真って良いもんだなぁ。(^^)

0 件のコメント:

コメントを投稿