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 で圧縮すればかなり小さなファイルとして保存できます。需要はないでしょうけど。。。

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



まとめ

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

2017年12月23日土曜日

AA-Camera を作る (カメラ作成編)

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

使うもの


まずはラズパイ本体。RaspbetyPi 3 ModelB です。トランプとほぼ同じ大きさのシングルボード・コンピュータです。



小型でもパソコンとして必要なものは一通り揃っています。CPU は ARM プロセッサのクアッドコア 1.2 GHz。GPU も積んでいます。USB ポートを4つに HDMI 端子1つ。100Mbps のイーサネットポートも搭載。Bluetooth4.1 にも対応してます。45g という超軽量ながら、抜群の能力を秘めています。
しかも電子工作には欠かせない、GPIO を搭載しています。GPIO とは General Purpose Input/Output(汎用入出力)の略で、文字通りの入出力ピンがずらっと並んでいます。これにスイッチや LED やセンサー等を繋いで電子工作ができます。



次にラズパイカメラ。SONY 製の CMOS センサー「IMX219」を搭載しており、800 万画素の能力を持っています。3280x2464ピクセルまでの写真や、1080p・30fps までの動画を撮影することができます。500円玉よりひと回り小さいくらいのサイズなのに、かなりの高性能です。ラズパイと組み合わせれば、ちょっとしたデジカメは簡単に自作できてしまいます。(制御するためにちょっとだけ python の知識も必要です)



あとは、電子工作に必要なものがいくつか。ブレッドボードやジャンパー線、LEDやスイッチに抵抗など、おきまりのパーツ達です。

組み立て

カメラのケーブルを挿すためのコネクタがありますので、ラズパイカメラはそこに差し込むだけです。他のパーツは以下のように接続します。これで、GPIO27(入力ピン#13) からスイッチの状態を読み込むことができるようになります。それだけだと寂しいので、ついでに GPIO12(出力ピン#32) に信号を送ることで自由に LED を光らせることができるようにもう1つの回路を組んであります。



実際にカメラとスイッチとLEDを繋いだ状態がこれ。




プログラミング

HDMIの出力ポートがありますのでテレビやモニタに繋いで、USBキーボードやマウスを接続すれば、普通のLinuxPCとして使えます。プログラミングはそんな環境で行います。と言っても、無線LANを搭載していますので、一度ネットワークの設定を済ませて、SSH で接続できるようにしておけば、普段使っている PC から操作できますので、モニタやキーボードなどを接続する必要も無くなります。(そのあたりの説明は省略)

どんな環境であれ、とにかくラズベリーパイ上に GPIO を制御するための python ライブラリをインストールしましょう。

$ pip3 install wringpi

以上!
簡単。これで python から GPIO を制御できるようになります。

   1 !/usr/bin/python
   2 # -*- coding: utf-8 -*-
   3 
   4 import wiringpi as pi, time
   5 
   6 switch_pin = 27
   7 led_pin = 12
   8 
   9 pi.wiringPiSetupGpio()  
  10 pi.pinMode(switch_pin, 0)
  11 pi.pullUpDnControl(switch_pin, 2)
  12 
  13 pi.pinMode(led_pin, 1)
  14 
  15 before_switch = 0
  16 
  17 while True:
  18     status = pi.digitalRead(switch_pin)
  19     time.sleep(0.1)
  20     if (status != before_switch):
  21         before_switch = status
  22         if (status == 0):
  23             pi.digitalWrite(led_pin, 1)
  24         else:
  25             pi.digitalWrite(led_pin, 0)

スイッチのピンから0.1秒間隔で状態を読み込んで、状態に変化があった時だけ LED を点灯したり消灯したりします。 説明が雑ですが、実際その程度のことしかしていません。ここまでだと、スイッチを押したら LED が光るだけのプログラムです。LED を光らせるのではなく、カメラで撮影するのが目的ですので、このあたりの処理を置き換えましょう。

というわけで、ラズパイカメラを使うための python ライブラリもインストールします。

$ pip3 install picamera

以上!

やっぱり簡単。さすが python。

で、先ほどのコードを更新します。

  1 #!/usr/bin/python
  2 # -*- coding: utf-8 -*-
  3 
  4 import wiringpi as pi, time
  5 import datetime
  6 import picamera 
  7 
  8 switch_pin = 27
  9 
 10 pi.wiringPiSetupGpio()
 11 pi.pinMode(switch_pin, 0)
 12 pi.pullUpDnControl(switch_pin, 2)
 13 
 14 before_switch = 0
 15 
 16 with picamera.PiCamera() as camera:
 17     camera.resolution = (1280, 720)
 18     camera.rotation = 180
 19     camera.start_preview()
 20     camera.preview_fullscreen = False
 21     camera.preview_window = (100, 100, 640, 360)
 22 
 23     while True:
 24         status = pi.digitalRead(switch_pin)
 25         time.sleep(0.1)
 26         if (status != before_switch):
 27             before_switch = status
 28             if (status == 0):
 29                 now = datetime.datetime.now()
 30                 camera.capture("{0:%Y%m%d_%H%M%S}.jpg".format(now))
 31 
 32     camera.stop_preview()

カメラの初期設定が数行ありますが、それ以外はシンプルです。camera.capture() で写真を撮影し、引数に指定したファイル名で保存するだけです。これで、簡単なデジカメは完成です。

2017年12月22日金曜日

AA-Camera を作る (概要編)

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

AA-Camera とは

AA はアスキーアートの事です。プレーンテキストで絵を描くアレです。 つまり AA-Camera とは、パシャっと撮影すると、アスキーアートで保存されるカメラという事です。 画像ファイルではなく、テキストファイルとして保存するので、ファイルサイズはかなり小さくできます。
たとえば、3280 x 2187、色深度8bit、RGBで撮影した画像を JPG で保存して、だいたい 1MB だとしましょう。



これをテキストファイルで表現したら、90KB 程度で表現できます。さらに tgz で圧縮してあげれば 10KB 程度まで小さくできます。


色情報をはじめ、かなりの情報は失われますが、なんとか読み取れる程度の画像になります。あとは見る人の想像力次第です。本来はテキストファイルとして出力しますが、ここでは説明のため画像ファイルにして貼り付けました。(テキストだとブラウザの環境次第で表示が崩れるので)。ちなみに背中のあたりを拡大すると↓こんな感じ。


ちゃんとテキストだけで描かれています。さて、こんなカメラを作るのが今回のテーマです。使うのはラズパイ(RaspbetyPi 3 ModelB) と、ラズパイカメラ(Raspberry Pi Camera Module V2.1) と、ちょっとした電子工作のためのブレッドボードやジャンパー線、スイッチ等。デジカメを自作するようなものですが、ラズベリーパイがあれば実際に簡単に作れてしまいますので、やってみましょう。

次回はカメラを組み立てます。普通の写真が撮れるところまでサクッと作ってしまいます。
さらにその次の回では、撮影した画像を編集してテキストファイルにして保存する作業です。

と、ここまで書いてみたものの、概要編が寂しい気がするので、もう一枚パシャっと。


かなりの情報は抜け落ちてますので、見る人が補ってください。(笑)