UE4
oga at 23:00
UE4 ではスクリプト言語として Blueprint を使った開発ができます。Visual 言語となっており、画面上に命令を並べて線でつなぐだけで動作フローを作成することができます。

BP_Prog01

三角マークでつながった白い線が実行順序を表しています。それ以外の丸い終端でつながっている線は、引数や返り値といったデータの流れです。データの型によって線の色が変わります。Blueprint のプログラムはコードではなくグラフと呼ばれます。

高レベルな命令をバッチのように並べるだけでも良いし、Native 言語のように低レベルな命令を駆使して計算やフローを直接記述することもできます。非常に自由度が高くなっており、修正してすぐ Editor 上で実行できるので便利です。

すべて Blueprint だけで記述する必要はなく、C++ と密な連携ができる仕組みになっています。C++ で定義した class を Blueprint で継承したり interface を実装することも可能。命令も C++ で増やせるので、基礎部分やパフォーマンスが必要な処理を C++ で実装し、それらを Blueprint でまとめあげるような役割分担も可能です。


●関数ノードと Pure 関数ノード

白い線で繋いでいくのが通常の関数ノードです。一般的な命令の実行に相当します。線で繋いだ方向に順次呼び出されていき、順番が勝手に入れ替わることはありません。呼び出し回数も白い実行線の接続によって決まります。手続き型言語と同じです。

null

白い実行線を使わずに、引数と返り値だけを持っているのが Pure 関数ノードです。定義時にプロパティの「純粋」にチェックを入れると Pure 関数になります。C++ で作る場合は UFUNCTION() の中に BlueprintPure を入れます。引数と返り値だけを繋いでいくので、Pure 関数ノードの挙動は関数型言語によく似ています。

null

C++ 風の書き方に展開すると違いがわかります。通常の関数ノードの例を展開すると下記のとおり。返り値は必ず一時変数を経由して次の命令に渡されます。

int tempA= FuncA();
int tempB= FuncB( tempA, 3 );
PrintInt( tempB );


Pure 関数ノードの場合は、変数を経由しないで引数にそのまま渡しているケースに相当します。B の例を展開すると下記のとおりです。

PrintInt( FuncB( FuncA(), 3 );


Blueprint では返り値を返す関数の場合、通常の関数か Pure 関数か選ぶことができます。プロパティの参照のなど実行順序を気にしなくてもよい場合は、いちいち実行線を繋がなくてもすむ Pure 関数は便利です。

その代わり Pure 関数ノードは実行順番が明確ではなく、呼び出し回数も直感的な見た目と一致しません。そのため副作用を持つ Pure 関数ノードを作る場合は注意が必要です。

例えば下記の例は、深さ優先で考えると A->D->C->B の順序で呼ばれるような気がしますが、実際に走らせてみると FuncA->FuncC->FuncD->FuncB で呼ばれていました。

null

LogBlueprintUserMessages: [Prog03_2] Pure Func A
LogBlueprintUserMessages: [Prog03_2] Pure Func C
LogBlueprintUserMessages: [Prog03_2] Pure Func D
LogBlueprintUserMessages: [Prog03_2] Pure Func B


Native 変換しても A,C,D,B の順序で呼ばれていることがわかります。

void AProg03_C__pf3730294777::bpf__PureFunc__pf(int32 bpp__A__pf, int32 bpp__B__pf, /*out*/ int32& bpp__R__pf)
{
    int32 bpfv__CallFunc_PureFuncA_R__pf{};
    int32 bpfv__CallFunc_PureFuncC_R__pf{};
    int32 bpfv__CallFunc_PureFuncD_R__pf{};
    int32 bpfv__CallFunc_PureFuncB_R__pf{};
    bpf__PureFuncA__pf(/*out*/ bpfv__CallFunc_PureFuncA_R__pf);
    bpf__PureFuncC__pf(bpp__B__pf, /*out*/ bpfv__CallFunc_PureFuncC_R__pf);
    bpf__PureFuncD__pf(bpp__A__pf, bpfv__CallFunc_PureFuncA_R__pf, /*out*/ bpfv__CallFunc_PureFuncD_R__pf);
    bpf__PureFuncB__pf(bpfv__CallFunc_PureFuncD_R__pf, bpfv__CallFunc_PureFuncC_R__pf, /*out*/ bpfv__CallFunc_PureFuncB_R__pf);
    bpp__R__pf = bpfv__CallFunc_PureFuncB_R__pf;
}



●Pure 関数ノードと呼び出し回数

乱数の値を表示するプログラムを書いてみます。組み込み関数の Random Integer を使います。これは Pure 関数です。同じ Random Integer から値を取り出して 2回表示してみます。

null

実行結果
LogBlueprintUserMessages: [None] 62
LogBlueprintUserMessages: [None] 23


同じノードから線を引っ張っているにもかかわらず、2回とも異なる値が表示されています。画面上には 1個しか置いていないのに「Random Integer」は2回呼ばれているわけです。

「Random Integer」と同じ機能を持つ、「Pure ではない」通常の関数を作ってみます。

null

実行結果
LogBlueprintUserMessages: [None] 20
LogBlueprintUserMessages: [None] 20


今度は 2回とも同じ値が表示されており、見た目通り一回しか呼ばれていないことがわかります。

C++ 風の書き方で違いを表現すると下記のとおりです。

Pure 関数の場合

PrintInt( RandomInteger( 100 ) );
PrintInt( RandomInteger( 100 ) );


通常の関数ノードの場合

int tempA= RandomInt( 100 );
PrintInt( tempA );
PrintInt( tempA );


通常の関数は白い線でつながった順番で一度しか実行せず、また返り値は必ず一時変数を経由します。そのため同じ値を何度も参照することが可能です。

Pure 関数ノードの場合は、同じノードでも「通常の関数が結果を参照」した回数だけ毎回呼び出しが行われます。

もし Pure 関数で乱数の結果を再利用したい場合は、自分で変数に保存しておく必要があります。

null

また、通常の関数ノードの返り値が暗黙の一時変数に束縛されるという性質を利用することもできます。まず、何もしないすダミー関数 TempInt を作っておきます。

null

引数をそのまま返しているだけでですが、通常関数は返り値が一時変数に束縛されるので何度も参照できるようになります。

null

この場合も同じ乱数値を表示します。

このように Pure 関数は実行回数が見た目と異なるので、意図しないところで何度も呼び出してしまう可能性があります。負荷の高い命令や副作用のある命令は Pure 関数にしない方が良いでしょう。


●返り値を複数もつ Pure 関数

全く同じように、複数の値を返す Pure 関数も問題になりがちです。下記のように、一度に Position と Rotation を返す Pure 関数 GetPosition を作ります。

null

返り値を 1度しか参照していないように見えるのですが、この場合も GetPosition 関数は 2回呼ばれます。一度に複数の値をまとめて返した方が一見効率が良さそうな気がしますが、必ずしもそうではありません。

例外もあります。2つの返り値を、同じ命令の引数で同じタイミングで受け取る場合は 1回しか呼ばれません。下記の場合 GetPosition が呼ばれるのは一回だけになります。

null

構造体の展開も同様です。Blueprint では、構造体を展開して要素ごとに個別に参照することができます。(右クリックメニューから展開できます)

null

展開した構造体の値を、下記のように別の命令から個別に参照した場合も複数回呼ばれます。

null

例えば現在時刻を表示する時計のプログラムを作ってみます。現在時刻の取得は組み込み命令の Now です。これは Pure 関数です。

null

上の例では時、分、秒毎に区切って数字の描画を行っています。Now の結果である 時、分、秒 をそれぞれ一回ずつしか参照していないのに、Now はその都度毎回呼ばれてしまいます。

つまり、時、分、秒の描画時にそれぞれ異なる時刻を見ていることになります。タイミングによっては分を表示した直後に秒が桁上りして、秒だけが先に 00 になる場合があり得えます。表示だけならまだしも、アラームのように時刻を厳密に判定したい場合は問題になるでしょう。

10:05:59
  ↓
10:05:00
  ↓
10:06:00


上のようなケースでは、Now の呼び出しを最初の一回だけにして、予め変数に保存しておく必要があります。


・負荷の高い命令、副作用のある命令は Pure 関数にしない。

・複数の値を返す場合も、Pure 関数を避けた方がよい。

・動的に変化する関数の場合、同じ値を複数参照するには通常の関数にするか事前に保存する。


ちなみにゲームで管理している時間 (tick や GetGameTime) はフレーム単位で固定なので、同一のフレームであれば同じ値です。何度呼び出しても問題ありません。



●同じ Pure 関数ノードが再利用される期間を詳しく調べる

複数値を返す Pure 関数でも、同じ命令の引数から同時に参照される場合は一度しか呼ばれませんでした。この挙動をもう少し詳しく調べてみます。

下記の例は、同一式内の同一引数の複数回参照です。このケースでは Random Integer が一回だけ呼ばれて、必ず同じ値を加算した結果を返します。つまり表示される値は常に偶数です。

null

次のように Random Integer ノードを 2個置くとそれぞれ 1回ずつ呼ばれます。つまり乱数は 2回生成され、異なる乱数値の和を返します。もちろん表示される値が奇数になる場合もあります。

null

同じ命令の引数でなくても構いません。下記のように、同じ式であれば異なる場所から参照しても GetPositon2 は一度しか呼ばれません。

null

↓もちろん 2回書けばそれぞれの GetPosition2 が個別に呼ばれます。

null

↓1つの加算から GetPosition2 が 2回参照されていますが、同じ式なので一度しか呼ばれません。ただし異なる SetActorLocation から 2回参照されているので、結局 GetPosition2 は合計 2回呼ばれることになります。

null

↓複数の引数から同じノードを参照しています。式グラフとしては異なりますが、白い実行線を通過する前なので GetPosition2 は一度しか呼ばれません。

null

まとめると

・同じ式の中では、同じノードの Pure 関数を複数参照しても一度しか呼ばれない。

・白い実行線を通過することでリセットされる。同じノードでも再び Pure 関数が呼ばれる。


また同一の式グラフの中で用いる場合は、Pure 関数のノードは共有してできるだけ同じ場所から線を引いた方が効率が良いこともわかります。メモリも呼び出し回数も節約できます。

もちろんプロパティ参照のような単純な命令では全く気にする必要は全く無いと思います。それでも Now 命令のようにバグの原因となる場合もあるので、挙動はある程度理解しておいた方が良いでしょう。


●通常の関数ノードの実行回数

Pure ではない通常の関数ノードは、白い線の接続で呼び出し回数が明示的に決まると書きましたが例外があります。class object のメソッド呼び出しの場合、Target に複数のオブジェクトを与えると複数回実行を繰り返します。

下記の例では ActorA と ActorB それぞれの PrintActorPosition メソッドを呼び出します。つまりグラフ上のノードは 1つでも 2回実行します。

null

Target は関数を呼び出す対象のオブジェクトで、C++ の this pointer に相当します。戻り値がある場合、一時格納変数が共有されるので最後に呼ばれた値が有効となるようです。呼び出し順番は不定です。

次回に続きます。


関連エントリ
4倍速い Ryzen 9 3950X の UE4 コンパイル速度
UE4 UnrealBuildTool の設定 BuildConfiguration.xml
UE4 UnrealBuildTool VisualStudio の選択を行う
UE4 UnrealBuildTool *.Build.cs のコードを共有する


Windows 10 May 2020 Update (2004) で WSL2 がリリースされました。

Microsoft: WSL 2 の新機能

I/O が速くなっているらしいので、いつも Android + Termux でテストしている「コンパイル時間の計測」をしてみました。

PC WSL1 WSL2
Core i7-6700K 4.0GHz (4C8T) 40s 29s
Ryzen 7 1800X 3.6GHz (8C16T) 26s 21s

・「WSL1」「WSL2」はビルド時間で単位は秒。値が小さい方が高速
・Windows 10 + WSL (Ubuntu 18.04LTS) clang 8.0 での比較

WSL1 にくらべて WSL2 の方がだいぶ速くなっています。Android の UserLAnd と Termux の関係に似ているかもしれません。

さらに直接 install した Linux と比較してみました。

PC clang WSL1 WSL2 Linux
Core i7-6700K (4C8T) clang 8 40s 29s 27s
Core i7-6700K (4C8T) clang 6 40s 28s 26s

・「WSL1」「WSL2」「Linux」はビルド時間で単位は秒。値が小さい方が高速
・WSL = Windows 10 + WSL (SATA SSD: MX500 500GB)
・Linux = Ubuntu 18.04LTS (SATA SSD: TS256GMTS400)

WSL2 のビルド時間が Linux を直接 install した場合に近くなっています。

なおテストは同じ PC を使っていますが、Windows と Linux で使用している SSD が違うので同一条件になっていません。速度差は OS の違いではなく SSD 性能差の可能性もあります。


以下スマートフォン他との比較

Device OS clang ビルド時間
Ryzen 9 3950X 3.5GHz WSL1 8 10s
Ryzen 7 1800X 3.6GHz WSL2 8 21s
Ryzen 7 1800X 3.6GHz WSL1 8 26s
Core i7-6700K 4.0GHz Ubuntu 18.04 8 27s
Core i7-6700K 4.0GHz WSL2 8 29s
Core i7-4790K 4.0GHz Ubuntu 18.04 8 31s
Google Pixel 3 Snapdragon 845 Termux 8 35s
Core i7-6700K 4.0GHz WSL1 8 40s
Essential Phone PH-1 Snapdragon 835 Termux 8 40s
Google Pixel 3 Snapdragon 845 UserLAnd 8 51s
Essential Phone PH-1 Snapdragon 835 UserLAnd 8 62s
A10-7870K 3.9GHz Ubuntu 18.04 8 69s
Huawei P30 Lite Kirin 710 Termux 9 71s
Huawei P30 Lite Kirin 710 UserLAnd 8 85s
Jetson Nano Tegra X1 Ubuntu 18.04 8 118s
ZenFone AR ZS571KL Snapdragon 821 Termux 8 125s
Raspberry Pi 4 BCM2711 (arm64) Ubuntu 19.10 8 146s
Nexus 5X Snapdragon 808 Termux 8 178s
Raspberry Pi 4 BCM2711 (armv7l) Raspbian 10 8 203s
Raspberry Pi 3 BCM2837 (arm64) Ubuntu 18.04 8 340s

・「ビルド時間」の単位は秒、値が小さい方が高速
・clang = clang の Version (コンパイラの Version によって速度が変わるので注意)

I/O 速度を見ると WSL2 だけで十分な気もしますが、WSL1 の方が手軽で便利な点もあります。例えばネットワークの場合 WSL2 には内部のローカルな IP Address が割り振られます。サーバーを立てて外部の PC からアクセスしたい場合、WSL1 は簡単に実現できますが WSL2 では外から直接見えなくなります。VirtualBox などの仮想マシンと同じで、port forwarding の設定が必要になるようです。

WSL1 同様、WSL2 からも Windows の exe を呼び出すことができました。つまり WSL 上で ssh server を起動しておき、Linux 側の ssh 経由で cmd.exe を実行することもできます。この辺が VirtualBox のような閉じた仮想マシンと異なるところです。

しばらくは使い分けながら併用してみたいと思います。


関連ページ
Compile Benchmark


関連エントリ
4倍速い Ryzen 9 3950X の UE4 コンパイル速度
Jetson Nano / Clang の Version とコンパイル速度の比較
Snapdragon 835 と 845 のコンパイル時間の比較&浮動小数点演算能力
Snapdragon 845 の浮動小数点演算速度
ARM CPU 上の開発環境とコンパイル時間の比較 (2) Pixel 3/UserLAnd
ARM CPU 上の開発環境とコンパイル時間の比較
AMD CPU Ryzen とコンパイル時間の比較 (2)
AMD CPU Ryzen とコンパイル時間の比較
ARM CPU の浮動小数点演算能力まとめ


Debian が Buster になっているので、1年前と比べて mozc の install が簡単になりました。Ubuntu と同じ手順でインストールできます。Wiki の手順を更新しました。

SSH + uim-mozc
VNC + fcitx-mozc

UserLAnd は Android 端末内に Linux 環境を構築するためのアプリです。Chromebook (ChromeOS) が Linux アプリに対応したり Windows 10 が WSL をサポートするのと同じように、共存しながら Android 上で Linux のソフトが走るようになります。

GitHub: UserLAnd


● lxde + VNC で mozc の環境を作る

◎ 1. UserLAnd を起動したら Debian または Ubuntu を選択。

◎ 2. Username, Password, VNC Password を入力して SSH を選択する

◎ 3. Terminal にログインして下記のスクリプトを実行

#!/bin/sh
. /etc/os-release

if [ "$ID" = "ubuntu" ];then
sudo apt update
sudo apt -y upgrade
sudo apt -y install lxde
sudo apt -y install language-pack-ja
sudo update-locale LANG=ja_JP.UTF-8
sudo apt -y install fcitx-mozc
sudo dpkg-reconfigure tzdata
fi

if [ "$ID" = "debian" ];then
sudo apt update
sudo apt -y upgrade
sudo apt -y install lxde
sudo apt -y install task-japanese
sudo apt -y install task-japanese-desktop
sudo dpkg-reconfigure locales
. /etc/default/locale
sudo apt -y install fcitx-mozc
sudo apt -y install dbus-x11
sudo dpkg-reconfigure tzdata
fi

if [ "$GTK_IM_MODULE" = "" ]; then
cat >> $HOME/.profile <<END
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export DefaultIMModule=fcitx
END

fi


ファイルに保存してから下記のように実行する。時間がかかります。

$ sh ./userland_fcitx.sh


◎ 4. 途中でいくつか選択肢あり

・Debian か Ubuntu かによって若干違います。選択例は Debian の場合です。

・[More] が出たら Enter (改行) キーで進めてください。

・以下選択例です。必ずしも下記の通りでなく環境に合わせて選んでください。(番号が異なっている場合があります)

Keyboard layout: 21  (Other)
 ↓
Country of origin for the keyboard: 55  (Japanese)
 ↓
Keyboard layout: 6  (Japanese - OADG 109A)

Users to add to the netdev group: 1

Locales to be generated: 285  (ja_JP.UTF-8 UTF-8)
 ↓
Default locale for the system environment: 3  (ja_JP.UTF-8)

地理的領域: 6  (アジア)
 ↓
時間帯: 79  (東京)


◎ 5. 解像度を選択します。

~/.vncrc に解像度が書き込んであるので任意の値に変更してください。

ハイエンドスマートフォンの場合、デフォルトだと文字が小さすぎる場合があるので最初に変更しておくことをおすすめします。

$geometry = "1280x720";


◎ 6. 一旦 exit

終了して UserLAnd のメイン画面に戻ります。

$ exit


◎ 7. SSH から VNC に変更して起動し直します。

 ・UserLAnd のメイン画面に戻ったら、Debian (または Ubuntu) を長押しして「Stop App」

 ・もう一度同じ場所を長押しして「App Info」を選択し、VNC を選択 (SSH → VNC)

 ・UserLAnd のメイン画面から Debian (または Ubuntu) を起動


◎ 8. bVNC Free をまだインストールしていない場合はストアに飛ぶのでインストール

 インストール完了したらもう一度 UserLAnd から起動する


◎ 9. VNC が起動するが lxde のデスクトップにならない場合は一旦手動で起動する

 コンソールから lxsession を実行する。

$ lxsession &

エラーダイアログが1つ出ますが無視して構いません。

また下記の内容で ~/.xsessionrc を作っておく。

. ~/.profile
lxsession &


◎ 10. メニューから LXTerminal を開く

 左下のアプリケーションメニューから「システムツール」→「LXTerminal」


◎ 11. LXTerminal 内で fcitx-autostart を実行

$ fcitx-autostart


◎ 12. fcitx の設定の確認

・左下のアプリケーションメニューから「設定」→「Fcitx 設定」

・「全体の設定」タブ→「入力メソッドのオンオフ」で任意のキーを選択する

・Android 9.0 以降は CTRL + SPACE が使えないので注意。

・切り替えキーが正しく反応するかどうかは LXTerminal 上で確認できます。


●その他

もし fcitx が自動で立ち上がらない場合 (毎回手動で fcitx-autostart を呼び出さないと日本語が入力できない場合) は、自動起動に登録してください。

・左下のアプリケーションメニューから「設定」→「LXSession のデフォルトアプリケーション」
・「自動立ち上げ」タブ→ 「+Add」の欄に "fcitx-autostart" を入れてから [+Add]

細かいメニューボタンの選択は、bVNC Free の「入力モード」→「タッチパッドシミュレーション」が便利です。


関連ページ
HYPERでんち: UserLAnd

関連エントリ
Android UserLAnd で PyTorch を使う。C++ API
Huawei P30 Lite/Fire HD 10(2019) のコンパイル速度と UserLAnd
Android: UserLAnd + Termux を Note PC 代わりに使う
Oculus Quest も文章書き&開発マシンにする
Android UserLAnd の更新と VNC 画面設定
UserLAnd : Android 9.0 で Ctrl + SPACE を使えるようにする
Android Termux で日本語入力を行う / UserLAnd との併用
Android 9.0 と Bluetooth Keyboard による日本語入力
Android で動く Linux 環境 UserLAnd が XServer XSDL に対応
Oculus Go を文章書き&開発マシンにする


数ヶ月走らせていますが安定して動いています。時間を意識してドアを慌てて閉めなくても良くなり、出入りのときに余裕ができました。

● Jetson Nano (5W mode)

作成時は Windows PC を使っていましたが、コード修正無しにそのまま動くことがわかったので今は Jetson Nano を使っています。Web カメラも PC で使っていたものそのままで USB 接続です。

常時起動しているので Jetson Nano は少電力モード (5W モード) に切り替えました。ワットモニターで計測したら本当に上限 5W でした。


● Logicool HD C270

使用しているカメラは Logicool の C270 です。室内の撮影は全体的に暗いので、わかりやすいように明るさを調整しています。Windows の場合標準のカメラ アプリの設定から「プロモード(フォト&ビデオの詳細コントロールを表示)」を有効にすると明るさの調整ができるようになります。Jetso Nano では v4l-utils を使っています。

$ v4l2-ctl -d /dev/video0 -c brightness=160


●判定の改良など

ロック中に更にロックコマンドを送信するのは問題がありそうだったので、事前に状態を確認するように変更しました。これで誤認識の度に lock コマンドを発行してしまう問題がなくなります。状態を確認する分だけ若干反応は遅れます。

・状態の確認
・結果の 'locked' が false の場合のみ 'lock' command 送信

ただし稀に正しく動かないケースがあります。鍵があいているのに 'locked' に true が返っているようです。手で直接ドアの鍵をあけた場合に起こるので、おそらく「解錠→ドア閉め」の時間が短すぎて、同期される前に状態を確認しているためだと思われます。


◎解錠からドア閉めまでの時間が極めて短い場合

人間 Sesame Server 開け閉め判定プログラム
手で解錠 解錠 true
ドアを開ける 状態の同期 開いたことを認識してフラグを立てる
ドアを閉める 閉めたことを認識。locked → true が返る。何もしない
同期完了 false
↑同期が完了する前に Server の状態を調べてしまう

確実に対処するなら 'sync' コマンドが必要になるようです。

・'sync' command 送信、状態をサーバーに同期する
・状態の確認
・結果の 'locked' が false の場合のみ 'lock' command 送信

ただ sync はあまり頻繁に呼ばない方が良いらしく、この手順だと誤認識の度に何度も sync してしまう可能性があります。気持ちゆっくりドアを開ければ今のところ問題ないので sync は一旦保留としました。


●解錠コマンド

判定を遅らせる代わりに解錠のタイミングを早める方法もあります。例えば直接手で鍵を開けないで、スマートスピーカーの音声コマンドで鍵を開けるようにすると十分な同期時間を稼げます。

せっかく認識用のカメラがあるので、オートロックと同じように何らかのハンドサインを認識して鍵を開けることもできそうです。

実際に解錠用の判定コードを追加してみたのですが、テストで走らせるにはかなりリスクが高いことがわかったので諦めました。施錠 (lock) だと誤動作してもさほど影響がないのですが、解錠 (unlock) の誤動作は問題になります。認識率の問題もありますし、プログラムのちょっとしたバグでも致命的になるかもしれません。時間をかけて検証する必要があります。

Amazon Echo (Alexa) のスマートスピーカーと連動した場合も、解錠は音声コマンドだけでなく合言葉(暗証番号)も必要な仕様となっていました。やはり解錠の自動化は慎重になった方が良さそうです。


●学習データ

判定対象となった画像を自動保存します。その中から定期的に判定が間違っているものだけ追加しておりデータは徐々に増えています。学習には時間がかかるので従来どおり PC を使っています。



関連エントリ
セサミmini、Web Camera でセンサー対応オートロックを作ってみる (PyTorch)
セサミmini、スマートロックを使って1年


スマートロックの セサミ mini を便利に使っています。(前回の記事) 物理的に鍵を使う必要がなく完全にワイヤレスで済みますし、施錠操作もオートロックのおかげで不要となりました。

非常に気に入っているのですが、セサミ mini のオートロックはドアの開閉と連動しているわけではなく時限式となっています。もたもたしているとドアを閉じる前に鍵が締まってしまうこともあります。時間を長めに設定すると、今度はロックが掛かるまで待つ場合に待ち時間が長くなります。

そこで、セサミの API を使ってドアの開け閉めと連動してロックが掛かる仕組みを作ってみました。本来なら、マグネット式のセンサーを使うのが最も簡単で確実だと思います。今回は手持ちの機材だけで何とかしようと思い、折角なので Web Camera で識別させてみました。


● Sesame API

CANDY HOUSE Developer Reference

さまざまなサイトに解説がありますので参考にさせていただきました。

Sesame API Version 3 のチュートリアル
APIキー取得方法とセサミIDの確認方法

API Key を取得したら Web 呼び出しで操作することができます。さらに鍵自体の Lock / Unlock 操作には、SmartLock 本体を識別するための Device ID が必要です。Device ID は API を通じて一覧から取得することもできますし、上の解説記事を見ると Web のダッシュボードで確認することもできるようです。

以下 Python 3.x を使っています。Device 一覧の取得です。

import requests
import json

API_URL= 'https://api.candyhouse.co/public/'
API_KEY= '<API_KEY>'

result= requests.get( API_URL+'sesames/', headers={ 'Authorization': API_KEY } )
device_list= json.loads( result.text )

for device in device_list:
    print( device['device_id'] )

毎回 Device ID を取得すると時間がかかるので file にキャッシュしておきます。これでサーバーに問い合わせるのは最初の一度だけになります。

import os
import requests
import json

API_KEY= '<API_KEY>'

class SesameAPI:
    CACHE_FILE= 'cache_file.txt'
    API_URL= 'https://api.candyhouse.co/public/'

    def __init__( self ):
        self.load_cache()

    def load_cache( self ):
        if os.path.exists( self.CACHE_FILE ):
            with open( self.CACHE_FILE, 'r' ) as fi:
                self.dev_list= json.loads( fi.read() )
        else:
            result= requests.get( self.API_URL+'sesames/', headers={ 'Authorization': API_KEY } )
            self.dev_list= json.loads( result.text )
            with open( self.CACHE_FILE, 'w' ) as fo:
                fo.write( json.dumps( self.dev_list ) )

    def get_device_id( self ):
        return  self.dev_list[0][ 'device_id' ]


print( SesameAPI().get_device_id() )

Device ID が判明したので、あとは状態の確認や Lock/Unlock コマンドの送信ができます。

class SesameAPI:
    ~

    def get_status( self ):
        result= requests.get( self.API_URL+'sesame/'+self.get_device_id(), headers={ 'Authorization': API_KEY } )
        status= json.loads( result.text )
        return  status['locked']

    def send_command( self, command ):
        requests.post( self.API_URL+'sesame/'+self.get_device_id(),
            headers={ 'Authorization': API_KEY, 'Content-type': 'application/json' },
            data=json.dumps( { 'command': command } ) )


sesame= SesameAPI()

# 状態の取得
print( sesame.get_status() )

# 施錠、解錠
sesame.send_command( 'lock' )
# sesame.send_command( 'unlock' )

Lock/Unlock コマンドの実行は時間がかかるので、結果を確認したい場合はあらためて問い合わせる必要があります。問い合わせには Command 実行時に得られる Task ID を使います。

↓成功か失敗を確実に返す (待つ) 場合。

    def get_task_result( self, task_id ):
        while True:
            time.sleep( 5.0 )
            result= requests.get( self.API_URL+'action-result?task_id='+task_id, headers={ 'Authorization': API_KEY } )
            status= json.loads( result.text )
            if status['status'] == 'terminated':
                if 'successful' in status:
                    return  status['successful']
                return  False

    def send_command( self, command ):
        result= requests.post( self.API_URL+'sesame/'+self.get_device_id(),
            headers={ 'Authorization': API_KEY, 'Content-type': 'application/json' },
            data=json.dumps( { 'command': command } ) )
        task= json.loads( result.text )
        task_id= task[ 'task_id' ]
        return  self.get_task_result( task_id )


sesame= SesameAPI()
print( sesame.send_command( 'lock' ) )


●画像による判定

機械学習 (Deep Learning) を使っています。モデルは非常に簡単な CNN で PyTorch を使いました。

USB の Web Camera を使ってドアの画像を撮影します。かなり少ないですが 640x480 で 1500枚ほど。様々な時間帯で人が写った出入り時の画像も含めます。先に完全に閉じている状態とそれ以外で分類しておきます。

Web Camera の撮影画像は少々暗く、夕方以降は見えないくらい真っ暗な写真が撮れることがあります。ドアを開けた方が明るいので、暗すぎる場合は閉まっているとみなしています。性能が良い最近のスマホの方が明るく写るので、スマホを Web Camera の代わりに使うともっと精度が上がるかもしれません。

学習は 128x128 dot にしましたがもっと小さくても良いと思います。Windows の GeForce GTX1070 でおよそ 15分くらいです。(BatchSize 32, Epoch 1000 の場合)

class Model_Sesame( nn.Module ):

    def __init__( self ):
        super().__init__()
        self.c0= nn.Conv2d( 3, 32, 5 )
        self.c1= nn.Conv2d( 32, 32, 5 )
        self.fc0= nn.Linear( 32*6*6, 128 )
        self.fc1= nn.Linear( 128, 64 )
        self.fc2= nn.Linear( 64, 2 )
        self.drop0= nn.Dropout( 0.25 )
        self.drop1= nn.Dropout( 0.25 )
        self.drop2= nn.Dropout( 0.5 )
        self.drop3= nn.Dropout( 0.5 )

    def forward( self, x ):
        x= F.relu( self.c0( x ) )
        x= F.max_pool2d( x, (4, 4) )
        x= self.drop0( x )
        x= F.relu( self.c1( x ) )
        x= F.max_pool2d( x, (4, 4) )
        x= self.drop1( x )
        x= x.view( x.size(0), -1 )
        x= F.relu( self.fc0( x ) )
        x= self.drop2( x )
        x= F.relu( self.fc1( x ) )
        x= self.drop3( x )
        x= self.fc2( x )
        return  x

開け閉めの判定部分では、OpenCV でキャプチャした画像をおよそ 0.5秒に一度推論に通しています。結果は完全に閉まっているかどうかの二択です。

    def main_loop( self ):
        cap= cv2.VideoCapture( 0 )
        PREDICT_TIME= 0.5
        SLEEP_TIME= 0.2
        STEP_PREDICT= int(PREDICT_TIME / SLEEP_TIME)
        counter= 0
        while( True ):
            ret,image= cap.read()
            cv2.imshow( 'preview', image )
            counter+= 1
            if counter > STEP_PREDICT:
                self.predict( image ) # 判定
                counter= 0
            time.sleep( SLEEP_TIME )
            key= cv2.waitKey(1) & 0xff
            if key == ord('q') or key == ord('\x1b'):
                break
        cap.release()
        cv2.destroyAllWindows()

データが少なく変化も乏しいので過学習している可能性はありますが、開閉のタイミングはそれっぽく取れているようです。あとは状態が変化したタイミングを捉えて、完全に閉まったときだけ lock command を送ります。

長時間走らせていると稀に誤判定が混ざることがあります。施錠されている状態をさらに施錠し直すだけなので、誤判定が紛れ込んでも特に問題はないです。ただそのたびに API が作動したという通知が来てしまうので少々気になります。

そこで確率の差が小さいあやふやな判定はある程度取り除くことにしました。さらに閉じたときは 4回以上連続、開けたときは 2回以上連続で同じ判定が続いたときだけ状態を反映させるようにしています。

    def predict( self, image ):
        size= IMAGE_SIZE # 64
        y,x,ch= image.shape
        ns= min(x,y)
        bx= (x - ns)//2
        by= (y - ns)//2
        crop_image= image[bx:bx+ns,by:by+ns]
        resize_image= cv2.resize( crop_image, (size,size) )
        ndata= np.zeros( (ch,size,size), dtype=np.float32 )
        for iy in (range(size)):
            for ix in (range(size)):
                for ic in range(ch):
                    ndata[ic,iy,ix]= resize_image[iy,ix,ic]
        fdata= ndata * (1.0/255.0)
        fdata= fdata.reshape( (1,ch,size,size) )
        x_data= torch.tensor( fdata, dtype=torch.float32, device=self.device )
        outputs_c= self.model( x_data ).to( 'cpu' ).detach().numpy() # 推論
        oy= outputs_c[0]
        self.sum= (self.sum + oy) * 0.5
        result= np.argmax(self.sum) # 0=Open, 1=Close

        open_state= self.open_state
        if result == self.prev_result:
            self.lock_count+= 1
            if result == 0:
                if self.lock_count >= 4:
                    self.lock_count= 0
                    open_state= 0
            elif result == 1:
                if self.lock_count >= 2:
                    self.lock_count= 0
                    open_state= 1
        else:
            self.lock_count= 0
            self.capture( image ) # 状態変化時の画像の保存
        self.prev_result= result

        if open_state != self.open_state:
            if open_state == 0:
                self.send_command( 'lock' ) # コマンドの送信
        self.open_state= open_state

Web Camera を設置して main_loop() を回します。これでドアを閉じたときに連動してセサミで施錠出来るようになりました。誤判定もなくなり明るい時間帯は安定して動いています。

まだまだデータの蓄積や改良が必要なので、判定結果が変化した時の画像を次の学習のために保存しています。最適化したら Raspberry Pi や Jetson Nano あたりで動かしたいと思っています。


●まとめ

API を利用して セサミ mini でもドアの動きに連動したオートロックを実現することができました。API が使えるのは非常に大きなメリットです。無い機能も簡単に作れますし、他にも様々な応用ができそうです。

最初にも書いたのですが、実用性を考えるなら普通にマグネットスイッチなどのセンサーを使った方が良いと思います。Web Camera を使う利点としては防犯カメラを兼用出来ることくらいでしょうか。もともと学習用データ収集のつもりだったのですが、ドアの開閉に連動して出入りした人の写真がきれいに残るようになりました。


関連エントリ
セサミmini、スマートロックを使って1年
Android UserLAnd で PyTorch を使う。C++ API
RADEON (ROCm) で PyTorch を使う。C++ API
Jetson Nano で TensorFlow の C 言語 API を使う


<<前のページ(日付が新しい方向) | 次のページ(日付が古い方向)>>