FlashAir を使ってデジタルカメラを『はいふりカメラ』にする

萬成 亮太
323

この記事は株式会社アニプレックス様よりコンテンツの使用許可を頂いた上で掲載しております。

こんにちは。フロントエンジニアの萬成です。

今回はFlashAirの活用方法として、デジタルカメラを『はいふりカメラ』にする事例を紹介したいと思います。

haifuri-overview
今回実装したガジェット

用語紹介

FlashAir

東芝が開発したプログラムが書けるSDカードです。
FlashAir に Lua 言語で記述したプログラムを置いておくと、SDカードに電源が入った時や、SDカード内にファイルが記録された時に Lua を走らせることが出来ます。また、FlashAir には Wi-Fi に接続して Lua から HTTP 通信やメール送信できる機能があり、手軽に IoT 開発を楽しむことが出来ます。
今回は触れませんが、FlashAir に AP を立てて Web サーバーにすることも出来ます。

haifuri-flashair-example
FlashAir を利用したガジェットの一例(リクルートマーケティングパートナーズのバックドア1)【朗報】オフィスにバックドア(物理)が実装されました

はいふりカメラ

人気アニメ ハイスクール・フリート(通称「はいふり」)の公式アプリに搭載されている機能です。はいふりカメラを使うと、はいふりの登場キャラクターと一緒に写真を撮影することが出来ます。iOS と Android に対応していますが、デジタルカメラには対応していません。

今回はデジタルカメラからでもはいふりカメラを楽しめるように、FlashAir と RaspberryPi、AS-289R2(プリンターシールド)を用いてはいふりカメラ&はいふりプリンタを開発します。

開発するシステム

FlashAir の Lua 機能には、ファイルシステムにアクセスできるものがあります。これを使うと、デジタルカメラで写真を撮影した時に Lua を実行して、撮影した画像ファイルを操作することが可能です。
この機能を使い、FlashAir の中で撮影した画像にはいふり画像を合成すれば、デジタルカメラをはいふりカメラにすることが出来そうです。っと思いきや、実は FlashAir の RAM は非常に小さいため、FlashAir の Lua 機能で画像処理をすることは困難な事がわかりました。そこで、今回ははいふり化を行うためのサーバー(はいふりプリンタ)を用意し、FlashAir から撮影した画像をはいふりプリンタに出力することで、はいふりカメラを構築することにしました。

haifuri-system
① 写真を撮影
② 撮影した写真をFlashAir から RaspberryPi に送信(HTTP)
③ 写真にはいふりのキャラクターを合成し、AS-289R2に送信して印刷(シリアル通信)

はいふりカメラの実装

まずデジタルカメラ側を実装します。デジタルカメラでは、FlashAir の Lua 機能を使って、写真を撮影した時に RaspberryPi に立てた Webサーバーに画像を送信します

FlashAirの設定

FlashAir の Lua 機能を使うには、まず設定ファイルを用意します。設定ファイルは /SD_WLAN/CONFIG に配置します。
今回は設定ファイルは以下のようになりました。

[Vendor]
APPSSID=無線LANのSSID
APPNETWORKKEY=無線LANのパスワード
STA_RETRY_CT=3
LUA_SD_EVENT=/SD_WLAN/haifuri.lua

APPNAME=haifuri
APPMODE=5
PRODUCT=FlashAir
VENDOR=TOSHIBA
DNSMODE=0
NOISE_CANCEL=2
LOCK=1
VERSION=FA9CAW3AW3.00.00
CID=02544d535733324731ea94c97900f701

APPSSID に無線LANのSSID、APPNETWORKKEY に無線LANのパスワードを設定しておくと、デジタルカメラに電源が入った時に FlashAir を無線LANに接続します。Lua 機能の中で HTTP 通信を行う場合には必須の設定です。 LUA_SD_EVENT に任意の Lua スクリプトへのパスを記述すると、FlashAir の中でファイルが保存された時、この Lua スクリプトが実行されるようになります。

CONFIG ファイルのすべての設定項目は公式のドキュメントから参照できます。

ファイルアップロードの実装

先ほどの設定で、FlashAir にファイルが保存されると /SD_WLAN/haifuri.lua が実行されるようになりました。ここから haifuri.lua にファイルアップロードのプログラムを lua 言語で書いていきます。

プログラムは、まずはじめにあるディレクトリ内で最も新しいファイルを特定する必要があります。今回使用するデジタルカメラは /DCIM/100OLYMP に撮影した写真を保存しますので、/DCIM/100OLYMP の中で一番新しいファイル、ということになります。
なぜこのような処理が必要なのかというと、FlashAir がやってくれるのはファイル保存時に任意の lua を実行することだけだからです。どのファイルが保存されたか、といった情報は自分で特定する必要があります。
以下のようにして最新のファイルパスを特定します。

local PHOTO_TARGET_PATH = "/DCIM/100OLYMP"
function find_latest_path()
    local max_mod = 0
    local max_mod_path = nil
    for file in lfs.dir(PHOTO_TARGET_PATH) do
        local path = PHOTO_TARGET_PATH.."/"..file
        local last_mod = lfs.attributes(path, "modification")
        if last_mod > max_mod then
            max_mod = last_mod
            max_mod_path = path
        end
    end
    return max_mod_path
end

次に、保存された画像の横幅と高さを元にはいふりカメラの有効・無効を切り替え出来るようにします。

haifuri-cam-mode
デジタルカメラの記録画素数の設定。C4以外は通常モード、C4ははいふりカメラモード。C4では画像ファイルが 1280x960 で保存される。

今回はいふりカメラ化したデジタルカメラは、撮影の設定を C1〜C4 までカスタム登録することが出来ます。そこで、C4 の設定の時だけ、写真が 1280x960 で保存されるようにしました。lua スクリプトで、保存された画像ファイルが 1280x960 ならファイルアップロードする、というように処理することで、カメラの外からはいふりカメラの有効・無効を設定することが出来るようになりました。

詳細は割愛しますが、以下のように JPEG 画像のヘッダーを探っていくことで、画像の横幅と高さを取得できます。

function read_short(fp)
    local bytes = fp:read(2)
    local b0 = bytes:sub(1, 1):byte()
    local b1 = bytes:sub(2, 2):byte()
    return bit32.lshift(b0, 8) + b1
end

function get_image_size(path)
    local fp = assert(io.open(path, "rb"))
    fp:seek("cur", 4)
    local offset = read_short(fp)
    for i = 1, 10 do
        fp:seek("cur", offset - 2)
        local marker = read_short(fp)
        if marker == 65472 then break end
        offset = read_short(fp)
    end
    fp:seek("cur", 3)
    local height = read_short(fp)
    local width = read_short(fp)
    fp:close()
    return width, height
end

最後にファイルアップロードを実装します。これは公式のサンプルにもありますので詳細は割愛します。
転送方法は multipart/form-data のみサポートしており、リクエストボディ内の <!--WLANSDFILE--> が、アップロードファイルの中身に置換される仕組みです。

local ENDPOINT_URL = "http://192.168.1.7/api.php"

function request_api(path)
    boundary = "--HAIFURI--BOUNDARY"
    payload = "--" ..  boundary .. "\r\n"
        .."Content-Disposition: form-data; name=\"image\"; filename=\"res.jpg\"\r\n"
        .."Content-Type: image/jpeg\r\n\r\n"
        .."<!--WLANSDFILE-->\r\n"
        .."--" .. boundary .. "--\r\n"
    fa.request{
        url = ENDPOINT_URL,
        method = "POST",
        headers = {
            ["Content-Length"] = lfs.attributes(path, "size") + string.len(payload) - 17,
            ["Content-Type"] = "multipart/form-data; boundary="..boundary
        },
        file = path,
        body = payload
    }
end

ここまでで説明したメソッドを使い、以下のようにすることで、1280x960の画像ファイルが撮影された時にファイルをアップロードする lua スクリプトが完成しました。

local PHOTO_TARGET_WIDTH = 1280
local PHOTO_TARGET_HEIGHT = 960
local path = assert(find_latest_path())
local width, height = get_image_size(path)
if width == PHOTO_TARGET_WIDTH and height == PHOTO_TARGET_HEIGHT then
    request_api(path)
end

はいふりプリンタの実装

次にプリンタ側を実装します。プリンタは RaspberryPi 3 Model BAS-289R2 プリンタシールド から成ります。

API エンドポイントの実装

まず RaspberryPi をセットアップします。ディスクに Raspbian を焼き、Apache2、PHP5、imagemagick をインストールしておきます。

次に、合成するはいふり画像のフレームを 683x384 にリサイズします。なぜこのサイズなのかというと、プリントアウトに使用する AS-289R2 の1ラインの最大ドット数が 384 ドットであるためです。

そして以下のような素朴な PHP により、送られてきた画像ファイルにはいふりフレームを合成します。

<?php

define('HASH', md5(time()));
define('PATH_TMP', '../upload/' . HASH . '.jpg');
define('PATH_OUTPUT', '../upload/' . HASH . '.bmp');
define('PATH_FRAME', '../frame/*.png');

if (!move_uploaded_file($_FILES['image']['tmp_name'], PATH_TMP)) {
    echo 0;
    exit;
}

$frames = glob(PATH_FRAME);
shuffle($frames);

// haifurize
$cmd_resize = '\( ' . PATH_TMP . ' -resize "683x384^" -gravity center -crop 683x384+0+0 \)';
$cmd_composite = '\( ' . $cmd_resize . ' ' . $frames[0] . ' -composite \)';
$cmd_normalize = 'convert ' . $cmd_composite . ' -rotate 90 -colorspace gray -dither FloydSteinberg -colors 2 -normalize -colorspace RGB ' . PATH_OUTPUT;
exec($cmd_normalize);
echo 1;

imagemagick を使い、FlashAir から送られてきた写真とはいふりのフレーム画像を合成し、誤差拡散法で二値化します。これは AS-289R2 の印刷時に各ドットが二値(印刷する・しない)で受け付けるためです。また、シリアル転送を容易に行うため、画像は合成を行った後90度回転させて縦長にし、384x683無圧縮ビットマップ形式として保存します(理由は後述)。

haifuri-haifurize
写真とフレームから印刷用の二値画像を出力する

プリントサーバーの実装

最後にプリントサーバーを実装します。プリントサーバーは API が吐き出したはいふり画像を AS-289R2 にシリアル転送する役割を持ちます。
RaspberryPi から AS-289R2 への印刷は、テキストデータであれば以下のように簡単に実装できます。

#include <stdio.h>
#include <string.h>
#include <wiringPi.h>
#include <wiringSerial.h>

int main() {
    int fd;
    if ((fd = serialOpen("/dev/ttyAMA0", 9600)) >= 0) {
        serialPrintf(fd, "Hello world.\r");
    }
    return 0;
}

しかし今回は画像データですので、AS-289R2 の ビットマップ登録・印字という手順が必要になります。この手順には wiringPi の serialPutchar を用い、以下の図のようにデータを送信します。

haifuri-print-data
1行分のビットマップの印刷

上図により、ビットマップ1行を印刷することが出来ます。画像には横幅がありますので、データを繰り返し送信すると写真として印刷することが出来ます。

AS-289R2 は性能上、上図の印刷ライン数480まで指定できるため、最大で 480x384 の画像を一気に転送し、一気に印刷することが出来ます。しかし、印刷したい画像がこのサイズより大きい場合、データを転送するのにラグが発生してしまい、画像の続きの部分を印刷する際に印刷のズレが起きてしまいます。転送と印刷を1ラインずつ刻んだ方が、印刷のズレを防ぐことができ、印刷中にプログラムを停止すると印刷もすぐにストップするため勝手が良いと感じました。

ここではいふり化の際に画像を 683x384 から90度回転させて 384x683 にしたことが生きてきます。ビットマップの画像はデータ部が左下から右下、下から上の順番で各ピクセルのRGBが3バイトずつ保管される形式です。1ラインの印刷で384ビットのビットマップデータを送信する際、Cのプログラムで画像をバイナリオープンしてforループで舐めるとイメージ全体を簡単に転送できるのです。

haifuri-bitmap-bin
生成した無圧縮ビットマップのバイナリ。C言語で実装するプリントサーバーで扱いやすい

デモ

FlashAir を挿入したデジタルカメラの電源を入れると、まもなく指定した無線 LAN に接続され、写真を RaspberryPi に送信出来るようになります。デジタルカメラの写真が 1280x960 で保存されるようにカメラの設定を変更して写真を撮影すると、FlashAir から撮影した写真が RaspberryPi に送信され、imagemagick によってはいふりフレームを合成し、AS-289R2 に印刷されます。

Github: manse/haifuri-camera-flashair

まとめ

FlashAir を Web サーバーと組み合わせてサービスを構築する事例を紹介しました。FlashAir は SD カードが刺さる機械ならなんでも IoT 化出来てしまう魅力があります。この機会に是非触れてみてください。

参照