toio™️コアキューブをさわらずに手で動かす(MFTokyo2020出展記 実装編)

#MFTokyo2020に出られることになった

　[Tsukuba Mini Maker Faire](https://tmmf.jp/2020/)に続いて、[Maker Faire Tokyo 2020](https://makezine.jp/event/mft2020/)にも[toioで作ってみた！友の会（非公式）](https://www.facebook.com/groups/toiotomo/)枠で出られることになりました。
　というわけで何を展示するかですが...「[toio™️ core cubeをホワイトボード上で動かす](https://qiita.com/kenichih/items/acfad593e666b52b75b2)」の垂直展示方式でなんか出したい、というところまでは考えていました。が、具体的には何を出すか。「[pong作ってみた](http://www.youtube.com/watch?v=h7fT3_YGEd8)」を出そうかとも思いましたが、コントローラを手で操作する都合上、接触が発生してしまうので毎回拭き取り消毒作業が発生してしまう。なお今回、Covid-19禍のため、展示は「非接触」or「接触する場合は毎回拭き取りか使い捨て」が推奨という条件もありました。

[![toio™️でpong作ってみた](http://img.youtube.com/vi/h7fT3_YGEd8/0.jpg)](http://www.youtube.com/watch?v=h7fT3_YGEd8)
　「toio™️でpong作ってみた」(YouTube)

　毎回コントローラの拭き取り清掃とかはめんどくさいのでなんとか触らずに操作する方法はなんかないかと思案していたところ、[Google MeediaPipe](https://google.github.io/mediapipe/)というものがあるとか。こいつを使えば手、顔、人体ポーズの画像認識が簡単にできるらしい、と。これだ！
　カメラ画像で操作する人の手や人体ポーズを捉え、その変化に応じてtoio™️ core cubeを動かせば非接触で操作できるではないかと。

#MediaPipeの試用とtoio™️ core cubeを動かす

##MediaPipe
　[MeediaPipe](https://google.github.io/mediapipe/)はLinux、Windows、macOS、Android、iOSといった幅広いプラットフォームで動かすことができ、Webカメラやスマホカメラの画像から手(指の関節)の位置(hand、multi hand)、顔の特徴点の位置(face)、上半身の関節位置(pose)を推論、出力することができます。OpenCV、tensorflowを使っているので、動かすためにはそれぞれがそれなりの速度で動く環境である必要はあります。

　まずはPC Linux(Ubuntu18.04LTS)を使ってMediaPipeを動かし、得られた座標からtoio core cubeを動かすようにしてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/6d6d705b-d413-4f1b-1c3a-ee6362e353d1.jpeg" width=80% alt="動作検討環境" />
　MediaPipeのインストール手順は[Installing on Debian and Ubuntu](https://google.github.io/mediapipe/getting_started/install.html#installing-on-debian-and-ubuntu)に従えば大丈夫です。日本語解説は「[[MediaPipe の Hand Tracking を Ubuntu 18.04/20.04 で動かす](https://qiita.com/mml/items/ea719b1edb722618271c)」などが参考になります。

## toio™️ core cubeを動かすpythonスクリプトとの繋ぎ
　toio™️ core cubeを動かすのは何度も使わせていただいていて慣れているPythonライブラリの「[tomotoio](https://github.com/tomoto/tomotoio/tree/master/tomotoio)」を使いたいところですが、MediaPipeはC++で実装されたプログラムなので、C++プロセスとPythonスクリプトのプロセスの間を繋がないといけません。socketとかでもいいんだけどなんか簡単な方法はないかなーと探していたら「[MediaPipeからPythonのプロセスにデータを渡す](https://qiita.com/xiong_jie/items/fb8d2f02aed98cc1e595)」というまさに求めていたものが！　
　ZeroMQ(ZMQ)というメッセージングの仕組みでC++のプロセスからPythonスクリプトのプロセスにJSON形式のデータを渡すという方法です。これを参考にしてMediaPipeの画像認識プロセスとtoio™️ core cube操作用Pythonスクリプトをつなげました。
![MFT2020_sys.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/3b1cf2b4-b917-8a1a-cf58-ce7ba823495f.jpeg)

# 展示内容の検討
##どの認識機能を使うか
　MediaPipeとtoio™️ core cube操作をつなぐことはできましたが、展示デモの操作方法をどうするかが考え所です。MediaPipeの認識機能として使えそうなのは、hand(片手)、multi hand(複数の手)、pose(上半身)、face(顔)があります。
　とりあえずぱっと思いついたネタをいろいろ試してみました。

このもくもく会のときにいろいろ試してました(笑)
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/toiotomo?src=hash&amp;ref_src=twsrc%5Etfw">#toiotomo</a> のもくもく会参加中です〜<br>そしてもうすぐMaketFaireTokyo2020ということでみなさん作品作り<a href="https://twitter.com/hashtag/MFTokyo2020?src=hash&amp;ref_src=twsrc%5Etfw">#MFTokyo2020</a> <a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> <a href="https://t.co/ULm1UFVnxS">https://t.co/ULm1UFVnxS</a></p>&mdash; Akichika Tanaka (@akichika) <a href="https://twitter.com/akichika/status/1307916753243049984?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- [face detection](https://google.github.io/mediapipe/solutions/face_detection)で目鼻の位置や形をみて福笑い的にtoio™️ core cubeを動かす
    - 目や鼻や口をを大きく動かすのは難しいのと、そもそもお客さんはマスクしているので口の形は認識できない。
- multi [hand](https://google.github.io/mediapipe/solutions/hands)で右手、左手とそれぞれの指先の位置を得てうごかす
    - 右手のひらと左手のひらの位置で、それぞれtoio™️ core cubeををひとつづつわりあてて動かすデモをつくってみました。手の位置とtoio™️ core cubeの位置がほぼダイレクトに一致するので動きはそこそこ面白いのですが、右手、左手をときどき逆に認識してしまう問題が発生。このため、突然左右の手が入れ替わったかのようにtoio™️ core cubeが動いてしまうという問題(場所を入れ替えようとして衝突する)がありました。
- [pose](https://google.github.io/mediapipe/solutions/pose) こちらは上半身の肩からのつながった形で関節位置が出るので右手、左手が突然逆になったりはしません。
    - が、こちらは指先の動きがいまいち取れない。しかたがないので腕部分の関節位置を使って、両手首と両ひじ位置にtoio™️ core cubeを動かすようなものを作ってみましたが、あまり動きがおもしろくありませんでした。
- [hand](https://google.github.io/mediapipe/solutions/hands) こちらは片手だけなので右手、左手を間違えないというかひとつの手しか認識しないので左右入れ替わり問題はおきません
    - 指の関節位置もかなりの精度でとれます。
 結局、人間が簡単に変化をつけて動かせるのは手の先のほうなので、[hand](https://google.github.io/mediapipe/solutions/hands)を使って手のひらの位置と指の関節の位置を使ってどうにかすることにしました。

## ネタを考える
  toio™️の面白さはなんといっても「動く」ことで、「操縦」できるとさらに楽しいものです。片手で操縦できることはなんかないかな、ということでまずは単純に五本の指の指先の位置に合わせてtoio™️ core cube 5台が動くというものを作ってみましたが、これもあんまり動きとして面白くないというか、そもそも親指から小指の指先の並び自体がそんなに大きく変化できないし、toio™️ core cube 5台の形作るフォーメーションがあまり面白くないというか、なにかごちゃついた動きになります。
　指先座標そのままを使ってもおもしろくないことがわかったので、簡単なハンドサイン(グー、チョキ、パーなど)に応じた固定のフォーメーションに変化させることにしました。フォーメーション全体の位置は手のひらの位置を使います。

### 手のひら位置とハンドサインの認識方法
　MediaPipeの出力するデータは基本的に関節番号とそれぞれの関節の座標(カメラ画像上の位置)です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/af76d1f1-6fb1-2671-ff81-6257207cda75.png" width=50% alt="openposeのhand detection keypoints"/>
MediaPipe handで認識できる関節の番号(図はOpenPoseののものですが、MediaPipe handでも番号は共通のようでした)

　手の平の位置は中指の第二関節(10番)と手首(0番)の座標の中点をとります。また、手の平のかたむきは、同じく中指の第二関節(10番)と手首(0番)の座標からatan2()で角度を出します。
　ハンドサインは指を折った数を調べます。指を折ったかどうかは指先の座標と手首座標の距離と、第二関節の座標と手首の座標の距離を比較し、どちらが手首に近いかどうかで判別できます。(中指だったら12番と0番の距離と、10番と0番の距離を比較する。指を伸ばしているときは10番のほうが0番に近い、折った時は12番のほうが近い) 
　ただし、このこの方法では手を水平に突き出されるようなポーズだと判別不能になります。手のひらをしっかりカメラに写すような感じがベストです。(奥行き方向のZ座標も取れるので3次元座標で距離比較をするようにしていますので、手が水平に近くてもそこそこ大丈夫ではありましたが)
　ハンドサインは指の折った本数(親指をのぞく)で判別することにしました。なので0〜4の5値が取れますが、説明上わかりづらいので、グー(4)、人差し指(3)、チョキ(2)、三本指(1)、パー(0)で説明することにしました。今回の環境で安定してBLE接続ができたのは4台だったのと、動かすキャンバスとなるホワイトボードのサイズも勘案し、toio™️ core cubeは4台使うことにして、ハンドサインごとにフォーメーションをわりあてました。

| ハンドサイン | フォーメーション |
|:-:|:-:|
| パー |四角 |
| 一本指|横一列|
| チョキ|Y字|
| 三本指|三角(逆T)|
| グー |菱形|

　4本指の場合もパーと同じ扱いになりますが、新たなフォーメーションの種類を思いつかなかったのでまあいいかなと。2本指のチョキがY字になっているのは4台でV字を作ってみましたが、四角とよく似ていてたけわかりづかったためです。また、フォーメーションチェンジで位置を変える際にtoio core cube™️どうしがぶつかってひっかかり動かなくなることがあるため、なるべくひっかかりにくい配置を試行錯誤のうえ決定しましました。本当はフォーメーションチェンジのときにぶつからないように群制御をするべきではありますが、難易度高いのとめんどくさいので配置の工夫で逃げることにしました。ぶつかって動かなくなっても手を少し振ってもらうとほぐれて動くようになりますし。

### デモ環境の用意

　まずはUbuntu18.04のデスクトップPC環境でだいたいのところを作って試しますが、デスクトップPC一式を展示会場に持っていくのはちょっと大変です。ノートPCにするという手もありますが、ノートPCはMacBookしか持ってない。Jetson Nanoは持っていたのでそれでなんとかならんか？と思いましたが、「[【MediaPipe】Jetson Nanoで環境構築し、CPU/GPUで動かしてみた（v0.7.5）](https://dev.classmethod.jp/articles/mediapipe-install-on-jetson-nano-with-cpu-gpu/)」によると認識速度が4fps程度でちょっと遅いかなと。しかし「[【MediaPipe】Jetson Xavier NXで環境構築し、Multi Hand TrackingをGPUで動かしてみた（v0.7.6）](https://dev.classmethod.jp/articles/mediapipe-install-on-jetson-xavier-nx-with-gpu/)」によれば20fps出るとのこと。これだけでれば十分ですし、Jetson Xavier NX Dev.KitだとWiFi/BluetoothのM.2インタフェースが最初からついてくるので他に何も足さずに使うことができそうです。(運搬＆展示用にアクリルケースは買い足しましたが。あと、Jetson NanoにUSB Bluetoothインタフェースを付けて試したことはあるのですが、どうもBLE接続が途切れがちで安定しなかった経験があります。)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/dc093ec7-47df-c470-2421-a76cae10f503.jpeg" alt="展示用環境"/>
　PCで仮組みしていたシステムをそのままJetson Xavier NXに持っていって動かしてみたところ、期待どおりの性能。セルフビルドもそこそこ速いので、いろいろパラメータを調整して試すサイクルが大幅に短縮できました。まあ、安いノートPCが買えるぐらいの値段ではあるのですが、展示本番まであまり時間がなかったですし、時間を金で買ったと思えば。(笑)

### 調整、調整、調整
　Jetson Xavier NXの環境上でいい感じになるようにいろいろパラメータや処理を調整していきます。
　まず、20fpsで座標情報が来てもそれをそのままtoio™️ core cube群の位置に反映させると、機敏すぎるというかガクガク飛んでいくような動作になります。また、誤認識によるトンデモ位置になったりすることもあるので、簡単な平滑化処理(移動平均法など)をかけて座標変化を少し滑らかにし、さらにtoio™️ core cubeが新座標に移動するタイミングを調整します。また、細かい座標変化(人間の手はピタッと止まってないですこし揺れている)に追従してもやっぱりピクピク動いて気持ち悪いので、ある程度以上の距離移動しないとtoio™️ core cubeを動かさないようにします。逆に手が大きく動いた場合は移動速度を速めに、少しだけ動いた場合はゆっくりめになるようにします。といったパラメータを調整し、結局はtoio™️ core cubeが動くサイクルは2〜4fps程度になりました。この程度まで落としてもよかったんだったら実はJetson Nanoでもできたんじゃないかという気がしなくもないですが。(笑))
　また、ハンドサインの変化によるフォーメーションの変更も、機敏に反応しすぎると頻繁にフォーメーション変更をしようとしてごちゃついた動きになるので、ある程度の域値をもたせます。ハンドサインが変化したあと同じハンドサインがある程度の時間(フレーム数)持続したらフォーメーション変更をかけるようにしました。
　悩まされたのは手の誤認識で、ときどき背景の模様や服のシワを手だと認識してしまい、突然今までの手の位置から全然違う位置へ吹っ飛んでしまうことです。これは誤認識時のパターンを観察し、誤認識時は指の関節座標群がぐちゃっと密集することから、関節座標が一定の小ささに密集している場合は誤認識としてはじくようにしました。
　
　
### どのカメラを使うか
 Jetson Xavier NX Dev.KitではUSBカメラ以外にもMIPI CSI接続のIMX219カメラモジュール(いわゆるRasPi Camera V2)が使えます。また、今回の展示内容だともう少し広角なカメラのほうが手の幅広い動きを捉らえられて都合が良いので、FOV120°ぐらいの広角なUSBカメラとIMX219カメラモジュールを入手しました。(MediaPipeのサンプルコードはUSBカメラしか対応していないので、Jetson Xavier NXでMIPIカメラを使うにはOpenCVのVideoCapture()でgstreamerによる入力を指定するように変更する必要があります。)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/c619db8b-b914-f7cd-545b-fe873d3b3946.jpeg" width=30% alt="USBカメラ"/>
左側が広角のUSBカメラ Buffalo BSW505MBK

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/a832e681-2e8d-3c99-eb3d-f93af5269e33.jpeg" width=30% alt="MIPI CSIカメラモジュール" />
左側がRasPi Camera V2.1、右側がFOV120°のSainSmart IMX219カメラモジュール

　広角版のUSBカメラ、MIPI CSIカメラモジュールの両方でテストし、手を左右に動かしても画像からはみ出ないことを確認。さらにそれぞれのカメラ用に誤認識排除のパラメータ、toio™️ core cubeの移動制御パラメータを調整しました。(カメラ自体違い、カメラを繋ぐインタフェース、処理する環境で違ってきます)
　
# だいたいこんな感じに
　広角版のUSBカメラおよび広角版のMIPI CSIカメラのどちらを使うかですが、展示場の明るさとか、カメラをどう設置するかによって変わるだろうと思い、現地で写りのいいほうを使おうとUSBカメラ、MIPI CSIカメラの両方を持っていくことにしました。カメラの設置位置によってはケーブル長さの都合もありそうでしたし。
　
[![さわらずにtoio core cubeを動かす　自宅テスト](http://img.youtube.com/vi/f5b_eQZUifQ/0.jpg)](http://www.youtube.com/watch?v=f5b_eQZUifQ)
自宅でのテスト＆パラメータ調整後のうごき。(YouTube)

　ときどきtoio™️ core cubeがくるりと回転していたりしますが、これは裏面に貼り付けた磁石が微妙に中心から外れてしまったせいです。なんとなくかわいい動きなのでこれはこれでアリかなと。
　さて、自宅環境では動くようになりましたが、展示場現地に持ち込んでうごくかどうか？　続きは「[展示編](https://qiita.com/kenichih/items/5b4d9aeff73c62d08cb2)」で。