🖨️

プリントシール機のデバイスを制御する(笑って怒ってハイチーズ!に使われている技術_後編)

2024/04/19に公開

はじめに

こんにちは! Whatever Co. でエンジニアをしている登山です。

本記事では、 日本科学未来館 の新常設展示「老いパーク」で展示されている、脳の変化を疑似体験できるコンテンツ「笑って怒ってハイチーズ!」の開発で使用した技術について解説します。(2 本立ての後編です。前編はこちら

どんなコンテンツ?

「笑って怒ってハイチーズ!」(通称わらおこ)は、「年をとると、相手の怒りや悲しみなどの表情が読み取りにくくなる」ことの擬似体験ができるコンテンツです。
詳しくは、前編の記事を参照してください。

プリントシール機をカスタムする

このコンテンツを実現するためには、いわゆるプリントシール機(プリ機)のような筐体を作る必要があります。

数年間は続く常設展示のため、筐体に関しては一般的なプリ機と同じ金属素材で制作することを考えました。
オリジナルの筐体を一から制作する選択肢もありましたが、実現性とコスト面を考慮した結果、既存のプリントシール機をカスタムしていく方針を取りました。

数社のプリ機メーカーさんにカスタム可能かを問い合わせし、相談可能なメーカーさんを見つけました。相談の結果、筐体内部の PC をまるっと入れ替えて、各デバイスの制御プログラムもこちらで一から実装することに決めました。

筐体の入口を広げる

実は、デバイスの制御以外にも、筐体自体のカスタムが必要でした。
具体的には、車椅子の方でも問題なく入って体験ができるように、筐体の入口を広げる必要がありました。また、内部で車椅子を回転できる面積を確保するために、筐体の骨組みを延長しました。

加えて、筐体後ろの照明・フラッシュと接続するためのケーブルも延長する必要があり、ピン配列が特殊だったのですが、専用の延長ケーブルを作成しました。(これらの部分は施工会社さんがやってくれました)

また、この状態にしたときに、元々のカメラ・照明が問題なくワークするかの検証を行いました。
カメラに関しては、元々の取り付け位置のまま、備え付けのレンズを一番ズームにすることで解決できました。撮影時の露光やシャッタースピード等のパラメータもそのままで OK となりました。
照明に関しては、空間ができた分少し暗くなったので、内部の正面に蛍光灯を 2 基追加しました。(元々設置されていた照明と似た色温度のものを選定いただきました)

機材構成

機材構成は下記の通りです。

PC

前編で紹介した表情変化処理を行う PC と、UI 表示・デバイス制御用 PC を分けて、2 台構成にしています。
UI 表示・デバイス制御用の PC は筐体内に組み込み、表情変化用の PC は LAN 経由でバックヤードに置いています。
(可能であれば 2 台とも筐体内に組み込みたかったのですが、サイズが収まりきらなかったことと熱の懸念から、バックヤードに置くことになりました)

デバイス

デバイス関連では、主にカメラ・フラッシュ・プリンターの制御を行いました。タッチパネルに関しては、制御ではないですが、きちんと接続できているかの監視を行いました。

その他の特徴としては、PDU と呼ばれる、LAN 経由でコンセントの各口の電源 ON/OFF ができる機器を導入しました。

今回選定した機種では、HTTP で電源の ON・OFF・切断後再起動のコマンドを送信することができます。別途開発した、展示全体の電源管理アプリから、手動 or 自動で各コンテンツの電源の ON・OFF・再起動ができるようになっています。

(余談ですが、老いパークには PC のみで完結するコンテンツもあり、そこでは PDU を使用していなく、終了 or 再起動時には shutdown コマンド、起動時には Wake on LAN を用いています。)

フレームワーク

UI 表示のフレームワークには Unity を用いました。理由は開発者が手慣れていること・他コンテンツも Unity で開発されていることが挙げられました。
そのため、デバイス制御も Unity 内で行うことにし、各デバイスを C#で制御できるように実装を進めました。

シーケンス図

各デバイスのドキュメントを読んで、ちゃんとしたフローを作る必要があることがわかったので、最初にシーケンス図を作成しました。コードで記述できる mermaid を使ってみました。

ざっくりこんな感じで(長い)、これをもとに実装を進めました。

各種デバイス制御

カメラ

主にライブビュー映像表示、スチル撮影、撮影画像取得などを行います。

SDK 内に C#のサンプルプログラムがあり、そのソースコードを参考にしながら、Unity で扱えるように wrapper を実装していきました。

具体的には、シャッター押下・パラメータ設定などの各コマンドをカメラに非同期で送信し、返信がきたら、登録しておいたコールバック関数を呼び出すような形になっています。

撮影時のオートフォーカスを無効にする

いざサンプルの通りに実装してみると、撮影コマンドを送信してから、少しディレイがあって撮影がされることがわかりました。
調査すると、デフォルトでは撮影時(シャッター押下時)にオートフォーカス処理が実行されており、その時間だけ撮影が遅れてしまっていることがわかりました。

対策としては、撮影時のオートフォーカスを無効にし、代わりに、撮影前のカウントダウンが開始する段階(カメラのライブビュー映像が表示され始める段階)でオートフォーカス処理(シャッターの半押し)を入れることにしました。
撮影自体は筐体内で行われることが確定しているので、カウントダウンの前後で被写体の前後移動があまり発生しないだろうという判断をして、このように対応しました。
これにより、撮影コマンドを送信してすぐに撮影が行われるようになりました。

余談: 顔の高さに合わせてカメラ画像の表示位置を自動調整する

デバイス制御の話題ではないですが、ちょっと工夫したことを書きます。

このコンテンツでは、全身ではなく、顔周辺がアップされた状態で画面に表示され、その画面を見ながら撮影を行います。(カメラ画像自体は全身を捉えていますが、そこから顔周辺の部分だけ切り出して表示しています)

通常だと、アップで表示された画面を見ながら、体験者側がその画角に合わせて撮影する流れになるのですが、画角が固定であるため、子供や車椅子の方など、身長が低い方は合わせるのが難しくなってしまいます。

そのため、最初は目線の高さが 120cm 程度の場合、ちょうど画面の中央に収まるような画角に設定していたのですが、これだと大人はかなり屈んで撮影しなければならず、テスト撮影を行なった際に、これは大変だという声を多くいただきました。

そこで、カメラ画像から顔の位置(高さ)を検出して、どの部分を切り出してアップで表示するかを自動で調整する機能を実装しました。

高速に検出したかったので、Unity 内で顔検出できるライブラリ を使用しました。

実装はシンプルで、ライブビュー画像を Texture2D に変換し、顔の座標を取得したら、その中の最も低い位置にある顔に合わせて、ライブビュー画像を貼り付けている RawImageRectTransform を調整し、画面に表示します。

顔がどの位置の時にどれくらい画像位置を調整するかは、それぞれの最大値・最小値を計測しながら、最終的に目視で微調整しました。
あまりに顔の位置が高いと、天井のフレームが写り込んでしまうことがわかった(グリーンバックで抜けない)ので、ギリギリフレームが写り込まない範囲で調整をしました。

フラッシュ

フラッシュの機材は、インターネット上にはほとんど情報がない特殊なものでした。(ローカルにドキュメントは存在します)
ジェネレータと呼ばれる制御機器に対して、充電・放電などの命令をシリアル通信(RS-232C)で行いました。
(余談ですが、今回はジェネレータに合計 5 基の発光部が接続されています。最大 10 基接続できるようです。)

具体的には、それぞれの発光部に対して

  • 電圧設定、設定確認
  • 充電開始、充電確認
  • 発光確認
  • 放電

の命令を出しています。

ジェネレータはカメラと接続されていて、カメラのシャッターが押下(全押し)されると、充電されていた発光部が自動で発光するようになっています。

それぞれの命令に対して、OK/NG の応答があるのですが、それ以外にも確認用のコマンドが別途存在していて助かりました。エラーが発生した際も、どの発光部が異常だったかがわかるようになっていて便利でした。

充電したら確実に放電する

各発光部は、160V~300V の範囲で充電が可能(電圧が高いほど明るいが充電時間がかかる)なのですが、充電された状態が長時間続いてしまうと負荷がかかり危険であるとプリ機メーカーさんに助言いただきました。
そのため、何かしらのエラーにより、充電後に発光せず異常終了した場合、すぐに必ず放電処理を行うようにしました。(ちなみに問題なく発光した場合でも、超念の為ですが、撮影終了時に放電処理を入れています。)

プリンター

プリンターは、もともと筐体に搭載されていた機種だと印刷時間が結構かかるものだったため、プリ機メーカーさんおすすめの高速印刷可能な機種を選定しました。
(6x4 インチの印刷サイズで、90 秒程度かかっていたのが 30 秒程度になりました。)

これもネット上にはあまり情報がない機種でした。
制御のための DLL があり、C#側で作った wrapper 経由で関数を呼び出します。
詳細なドキュメントは存在していましたが、唯一のサンプルコードが C 言語だったのもあり、マーシャリングなどの実装に少し苦労しました。

具体的には、uGUI の Canvas 上に各画像を配置していき、完成後 RenderTexture に焼いてから画像データ化し、DLL 側に渡しています。

印刷コマンドの実行後、印刷が完了したかどうかを判別できるコマンドが存在していたので、印刷が完了したら、 UI 側で「印刷が完了しました」という画面を表示できました。

png で直接印刷できない

デフォルトでは png データを直接印刷にかけることができず、RGB のバイナリデータ .bin のみ受付が可能でした。
png データでも受付が可能なように実装することもできたと思われますが、実装時間の都合上、RenderTexture のピクセルデータから直接 .bin ファイルを作り、印刷することにしました。

作成した bin ファイルは Photoshop で開くことができたので、想定通りのデータが作られているかを確認しながら作業を進めました。

デザイン素材は CMYK で作っておく

デザイナーさんが作成したフレームの画像(png)に、撮影した画像・表情変化した画像を合成し、印刷にかけていましたが、印刷物の色味が画像データと結構異なってしまっていました。

プリンター側で色味調整を試行錯誤したのですが、色味が全然合わず。
元の ai ファイルをよく見たら、RGB で作成されていたのが原因でした。凡ミス。。
CMYK に直してもらいつつ、何パターンか色を出してもらい、なるべく RGB で作成していた時の色味に近づけていく作業を行いました。

DLL 側でトーンカーブを設定できる機能があったので、困った時の微調整はこれでできそうと思っていたのですが、最終的にはこの微調整はせずに色校正を終えることができました。

タッチパネル

体験者はタッチパネルから言語選択・人数選択を行うのですが、接続に異常があるとタッチが反応せず体験ができなくなるため、タッチパネルが問題なく接続されているかを監視する機能を実装しました。

Win32API のGetSystemMetrics 関数を用いて、デジタイザー(タッチパネル)の接続の有無を確認しました。

実装には下記を参考にしました。
http://grabacr.net/archives/1050

その他やったこと

かなり基本的な内容ですが、その他にやったことを書きます。

エラーが発生したらどの画面からでもエラー画面に遷移する

各デバイスでエラーが発生した際、現在 or 次の体験が継続できない場合は一律で UI をエラー画面に遷移することにしました。
例えば、

  • カメラから撮影画像が得られなかった場合
  • フラッシュの充電に失敗した場合
  • プリンターのインクが切れた場合

などです。

各デバイスのエラー発生時にはイベントが発火され、現在がどの画面であろうと「調整中」というエラー画面に遷移されるようにしました。

ちなみに、Unity での画面遷移の状態管理には ImtStateMachine というステートマシンを用いました。シンプルでとても使いやすかったのでおすすめです。

下記を参考にしました。
https://qiita.com/BelColo/items/a94c9ccc2d5174dc29a3
https://qiita.com/BelColo/items/a27b66b18794b33943f9

また、老いパークでは、バックヤードに各コンテンツを管理する PC(管理 PC と呼んでいます。電源管理アプリも管理 PC に入っている)があり、各コンテンツ PC のアプリケーションが吐き出したログをクロールして貯めておくことができるようになっています。

そのため、エラーが発生すると、スタッフが管理 PC を見て、どんなエラーログなのか内容が確認できるようになっています。

ダミークラスで UI 開発する

アプリの UI を開発する時に、デバイス本体がないと動かせない状態では、極めて開発効率が悪くなってしまいます。これを避けるために、各デバイス制御のダミークラスを作成しました。

共通の interface を定義し、使用する側は interface を参照するようにして、開発時はダミークラス・検証時は本番用クラスにすぐに切り替えが可能なようにしました。

非常に簡素ですが、例えば下記のような感じです。

//共通の interface
public interface IDeviceControl
{
    void Initialize();
}

//本番クラス
public class DeviceController : IDeviceControl
{
    public void Initialize()
    {
      //初期化処理
    }
}

//ダミークラス
public class DummyDeviceController : IDeviceControl
{
    public void Initialize()
    {
      //初期化処理(何もしない)
    }
}
//使用する側
public class Main : MonoBehaviour
{
    private IDeviceControl deviceController;

    void Start()
    {
        //configのフラグによって本番用クラスかダミークラスかを切り替える
        if (Config.isDebug)
        {
            deviceController = new DummyDeviceController();
        }
        else
        {
            deviceController = new DeviceController();
        }
        deviceController.Initialize();
    }
}

設定用の config ファイルを用意する

上記コードからも伺えますが、別途 config ファイルを作っておき、各種フラグや定数を後から設定できるようにしていました。config 内のフラグを切り替えるだけで、上記のクラスの切り替えができるようになっています。便利。
(余談ですが config ファイルは YAML 派です。YamlDotNet を使っています。)

さらに、config ファイルを Application.dataPath の一つ上の階層に置き、そこから読み込むようにすることで、Editor だと都度コンパイルが走らず、Runtime だと都度ビルドする必要がなくなります。便利。

謝辞

上記のデバイス制御部分の実装は、BASSDRUM さん と行いました。wrapper 部分は BASSDRUM さんがほとんど担当してくれました。ありがとうございました。
また、実装を進めるにあたり、プリ機メーカーさんには仕様の確認や質問をたくさんさせていただきました。ありがとうございました。
最後に、技術記事の投稿を快諾くださった日本科学未来館さん、本当にありがとうございました!!

終わりに

ここまで読んでいただきありがとうございました。
前編と合わせると非常にボリューミーになってしまいましたが、これだけ頑張ったぞ!というのが伝われば幸いです。(前編は AI を使った表情変化について解説しています)

そして、老いパークには、わらおこ以外にも楽しいコンテンツがたくさん展示されています!

お台場に立ち寄った際には、ぜひ老いパークに来て、色々体験してみてください〜!!!
(未来館の他の展示も、ぜひご覧になってください!)


ちなみに、弊社 Whatever Co. では「さまざまなテクノロジー領域を越境し、なんでもつくる」エンジニアを募集しています。
もし興味があれば、下記ページをご覧ください & お気軽にご連絡ください!
https://whatever.co/ja/careers/tokyo/engineer/

Discussion