スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった

>:santa:この記事は、 [North Detail Advent Calendar 2019](https://qiita.com/advent-calendar/2019/northdetail) の22日目の記事です:christmas_tree:

# 概要
### やりたいこと
1. ピアノ演奏中の手のモーションをスマホカメラで記録する
2. Unityでアニメーションしてピアノ演奏する

### 技術的なこと
1. google/mediapipeでハンドトラッキング用のiOSアプリをビルド
2. iPhoneカメラで手のモーションを記録
3. Blenderでアニメーション化 & ピアノオブジェクト作成
4. Unityに取り込んで再生、指と鍵盤の当たり判定で音を鳴らす


# 成果物
**※雑音が流れるのでご注意を**

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Unityでピアノを弾いてみたかった<br>（雑音が流れますのでご注意を） <a href="https://t.co/fVuHP9Xb0K">pic.twitter.com/fVuHP9Xb0K</a></p>&mdash; ヤマト (@yamatohkd) <a href="https://twitter.com/yamatohkd/status/1208514221643722752?ref_src=twsrc%5Etfw">December 21, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

　
......はい(出落ち)

# 作業環境
・MacBook Pro (Retina, 15-inch, Mid 2014) : Mojave 10.14.6 
・iPhoneXs : iOS 13.2.2

・Xcode : 11.3
・Blender : 2.79
・Unity : 2019.2.12

今回はiPhoneのカメラを利用しますが、AndroidやPCのカメラでも同じことができるはず
※[後述](#0-pcで実行したい人向け)しますが、**PCカメラ利用の場合はLinuxでGPUが利用できる環境推奨**

# Google / MediaPipeでモーション記録用のアプリをビルド
### [MediaPipe](https://github.com/google/mediapipe)ってなぁに？
>MediaPipe is a framework for building multimodal (eg. video, audio, any time series data) applied ML pipelines. With MediaPipe, a perception pipeline can be built as a graph of modular components, including, for instance, inference models (e.g., TensorFlow, TFLite) and media processing functions.
>
MediaPipeは、マルチモーダル（ビデオ、オーディオ、時系列データなど）を適用したMLパイプラインを構築するためのフレームワークです。 MediaPipeを使用すると、たとえばパイプラインモデル（TensorFlow、TFLiteなど）やメディア処理機能など​​、モジュラーコンポーネントのグラフとして認識パイプラインを構築できます。

......なるほど:thinking:(？)

どうやら機械学習パイプライン（画像処理→モデル推論→描画 など）の構築や、
それをグラフとして視覚化できるフレームワークらしい

サンプルではTensorFlowやTensorFlow Liteなどを利用して、
顔認識や物体の識別、手のモーション取得（ハンドトラッキング）などができる

Linux/Win/Macで実行したり、iOS/Android用にアプリをビルドしたりできる

### MediaPipeをインストール
##### 0. PCで実行したい人向け
モバイルアプリのビルドではなく、PCで直接実行したい人は**Linux環境(OS Xは含まない)推奨**です

2019/12/11現在、MediaPipeではデスクトップ用サンプルは**LinuxのみGPUサポートが対応**しています
(Win/Macでも`CPU実行モード`はあるのですが私の環境では即座にフリーズしました......)

また、**VMやdocker上のLinux環境ではホストGPUが使えない**ため、
MacならデュアルブートでOSインストールなどが必要です
(これを知らずにVirtualBoxのUbuntu上で作業していて土日まるまる潰しました......超頑張ればできないこともないとかなんとか?)


##### 1. 事前準備
[インストールガイドページ](https://github.com/google/mediapipe/blob/master/mediapipe/docs/install.md)の`Installing on macOS`を参考に進めていきます

- [Homebrew](https://brew.sh/)をインストール
- [Xcode](https://developer.apple.com/xcode/)と`Command Line Tools `をインストール

- Pythonバージョンの確認
私の環境では**Python 2.7ではビルドが通らなかった**ため、下記サイトを参考に`Python 3.7.5`をインストールしました
参考：[pyenvを使ってMacにPythonの環境を構築する](https://qiita.com/1000ch/items/93841f76ea52551b6a97)

- "six"ライブラリをインストール

```shell:
$ pip install --user future six
```

##### 2. MediaPipeリポジトリをクローン
```shell:
$ git clone https://github.com/google/mediapipe.git
$ cd mediapipe
```

##### 3. Bazelをインストール
二通りの方法がありますが、今回は`Option 1`の方法でやります
**※2019/12/11現在、MacではBazel 1.1.0より上のバージョンは対応していないため注意**

```shell:
# Bazel 1.1.0より上のバージョンがインストールされている場合はアンインストール
$ brew uninstall bazel

# Install Bazel 1.1.0
$ brew install https://raw.githubusercontent.com/bazelbuild/homebrew-tap/f8a0fa981bcb1784a0d0823e14867b844e94fb3d/Formula/bazel.rb
$ brew link bazel
```

##### 4. OpenCVとFFmpegをインストール
こちらも二通りあるので`Option 1`の方法でインストール

```shell:
$ brew install opencv@3
```

##### 5. [Hello World desktop example](https://github.com/google/mediapipe/blob/master/mediapipe/docs/hello_world_desktop.md)を実行

```shell:
$ export GLOG_logtostderr=1
$ bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
    mediapipe/examples/desktop/hello_world:hello_world

# しばし待ったのち以下が表示されたらOK
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
# Hello World!
```

ここで私の環境ではPython2.7で以下のエラーが発生しました
同じ現象が起こった人は[1. 事前準備](#1-事前準備)を参考にPythonのバージョンを上げてみてください

```
ERROR: An error occurred during the fetch of repository 'local_config_git':
   Traceback (most recent call last):
```

### モバイルアプリのビルド
今回は両手のモーションを取得するモバイルアプリをビルドしたいので、以下のサンプルを利用します
[Multi-Hand Tracking (GPU)](https://github.com/google/mediapipe/blob/master/mediapipe/docs/multi_hand_tracking_mobile_gpu.md)

*※PCで直接実行したい場合は以下を利用してください
[Multi-Hand Tracking on Desktop](https://github.com/google/mediapipe/blob/master/mediapipe/docs/multi_hand_tracking_desktop.md)*

#### Androidの場合
Androidの方が簡単にビルドできます
トラッキングには2Dと3Dモードの二通りあるので、3Dモードでビルドします

```bash:
$ bazel build -c opt --config=android_arm64 --define 3D=true mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu
```

かなり時間がかかるので待ちましょう
下記ディレクトリにapkファイルができるので実機にインストールすれば完了です！

```bash:
mediapipe/bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/multihandtrackinggpu/multihandtrackinggpu.apk
```

#### iOSの場合
iOSの場合は[こちらの設定](https://github.com/google/mediapipe/blob/master/mediapipe/docs/mediapipe_ios_setup.md)がもうひと手間必要です
始めの方にあるXcodeやBazelのインストールは既に完了しているので不要です

(しかもこちらの方法でBazelをインストールすると最新バージョンを取得して動かなくなるという罠。間違えてインストールしてしまった場合は[前述の方法](#3-bazelをインストール)でv1.1.0に置き換えてください)

##### 1. Provisioning Profileの用意
AppleDeveloperProgram加入者は[デベロッパサイト](https://developer.apple.com/jp/)でProvisioning Profileを作成してダウンロードしてください

そうでない方は以下のサイトなどを参考に作成してください
参考：[[Xcode][iOS] 有料ライセンスなしでの実機インストール 全工程解説！](https://dev.classmethod.jp/smartphone/run-on-devices-without-apple-developer-program-license/)

**Bundle Identifierを固有のものにしないとエラーが起きる**ので注意
どこかの誰かが使っているIDだと`Failed to create provisioning profile.`と怒られます
作成されたファイルは以下のpathにあります

```bash:
~/Library/MobileDevice/Provisioning Profiles/
```

用意したファイルは`provisioning_profile.mobileprovision `にリネームして、
`mediapipe/mediapipe/`に配置します

##### 2. BUILDファイルの修正
`mediapipe/mediapipe/examples/ios/multihandtrackinggpu/BUILD`45行目のbundle_idを、
Provisioning Profileで設定した`Bundle Identifier`に変更

```bash:mediapipe/examples/ios/multihandtrackinggpu/BUILD：45

bundle_id = "com.google.mediapipe.MultiHandTrackingGpu",
　　　　　　　　　↓
bundle_id = "hoge.fuga.piyo",
```

##### 3. アプリのビルド
3Dモードでビルドします

```bash
$ bazel build -c opt --config=ios_arm64 --define 3D=true mediapipe/examples/ios/multihandtrackinggpu:MultiHandTrackingGpuApp
```

長いのでしばらく待ちます
ビルドが完了すると下記ディレクトリにipaファイルができます

```bash
bazel-bin/mediapipe/examples/ios/multihandtrackinggpu/
```

##### 4. 実機にインストール
Xcodeの`Window > Devices and Simulators`から、
USB接続した実機デバイスを選択して、
画面下部の+ボタンからipaファイルをインストールすれば完了です！

# 手の動きをトラッキング
### ビルドしたアプリを動かしてみる
![hand_tracking.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/f97d2a5c-40a8-04cf-1af0-002ead7c0cff.gif)

おお！両手の動きがリアルタイムで反映されていますね

このときのiPhoneのプロセス状態を、USB接続したMacの`コンソール`アプリで確認してみましょう
<img width="910" alt="スクリーンショット 2019-12-12 23.31.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/7110fafb-f453-970b-9e4f-e36e18aaadbc.png">
`MultiHandTrackingGpuApp`というプロセス名で何やらログが表示されています
`hand[0]`と`hand[1]`の二つあり、それぞれ21個の`Landmark`を持っています

先程のgifの**片手のポイントの数が21**なので、その**座標**のようですね
それぞれの手のログは約0.05秒毎に表示されているようです

ちなみに、ログ出力のコードは下記ファイルに記述されていました
・`mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm : L177あたり`

フロントカメラではなく**背面カメラを使用したい場合**は、同ファイルの以下を変更してビルドし直すと可能です

```objective_c:mediapipe/mediapipe/examples/ios/multihandtrackinggpu/ViewController.mm
// 100行目をYESからNOに
_renderer.mirrored = NO;

// 109行目を~Frontから~Backに
_cameraSource.cameraPosition = AVCaptureDevicePositionBack;
```

### Unityで表示してみる
取得した座標をUnityの3D空間に浮かべてみます
<img width="1161" alt="スクリーンショット 2019-12-12 21.45.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/29b6cc9b-e055-865e-c2a8-6a820275939e.png">
なぁにこれぇ？

ログをよくみると**z座標だけ数値が異様に大きい**ですね
倍率を変更してみます
<img width="1165" alt="スクリーンショット 2019-12-12 22.03.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/64775366-2e14-9431-af56-5a2ae888b4da.png">
おっ！なんかそれっぽくなった！
向きと大きさを調整して線で繋いでみると・・・
<img width="1166" alt="スクリーンショット 2019-12-12 23.28.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/95e123b7-d637-0e9d-a533-afb62e9a3811.png">
**完全に手だコレ！ ＼\ ٩( 'ω' )و/／**

あとはピアノを演奏しているところを撮影して座標を取得するだけです


# Blenderでアニメーション化

##### [Blender](https://www.blender.org/)とは？
* 3Dモデル作成、アニメーション作成、レンダリングなどができる
* 高機能・高品質、だけど高難度
* オープンソースで無料で使える

最近 v2.8がリリースされ、UIや操作方法が変わり直感的に操作できるようになりました
参考サイトはv2.7xの方が多く、古いプロジェクトをインポートすると壊れてしまう場合もあるため今回は[v2.79](https://www.blender.org/download/releases/2-79/)を使用します

「Blender　アニメーション　作成」で検索すると、
**手動でボーンを動かしてキーフレーム毎にポーズを設定する方法**が主流のようです
参考：[かんたんBlender講座 アニメーションの基本](http://krlab.info.kochi-tech.ac.jp/kurihara/lecture/cg/BlenderWeb_Hayashi/html/animation.html)

この方法は単純なアニメーションだとよいのですが、
ミリ秒単位で複雑な動きをさせる場合は骨が折れます(ボーンだけに)

そのため今回はスクリプトで作成します

### Pythonでスクリプト作成
BlenderではPythonスクリプトが実行できます
MediaPipeで取得した座標をCSVファイルにまとめて、スクリプトからアニメーション化します

##### 1. 座標CSVファイルの作成
こんな感じでCSVファイルを作成しました
左から以下の値となってます

* 手(0=左手、1=右手)
* Landmark
* 開始からの秒数
* x座標
* y座標
* z座標

<img width="760" alt="スクリーンショット 2019-12-22 3.21.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/9d168218-969a-5559-7dbe-3a994f76ddb5.png">



##### 2. Landmarkオブジェクトの作成
以下のSphereオブジェクトを追加
これをLandmarkポイントとして動かします

* Left.000 ~ Left.020
* Right.000 ~ Right.020

##### 3. スクリプトの作成
Pythonスクリプトはこんな感じ

~~投稿時間を過ぎてしまったので説明は省略...:bow_tone1:~~
(倍率とかfpsとか適当。。。)

**コメント追記しました**

```python
import bpy
import os

leftLandmarks = []
rightLandmarks = []
fps = 24

scale = (0.015,0.015,0.015)

# Landmarkオブジェクトを割当
for i in range(21):
    leftLandmarks.append(bpy.data.objects["Left.0" + str(i).zfill(2)])
    leftLandmarks[i].scale = scale
    
for i in range(21):
    rightLandmarks.append(bpy.data.objects["Right.0" + str(i).zfill(2)])
    rightLandmarks[i].scale = scale

# csvのあるディレクトリ
dirpath = "/Path/to/csv"
filename = "animation.csv"

filepath = os.path.join(dirpath, filename)

# csvを一行ずつ処理
with open(filepath, mode='rt', encoding='utf-8') as f:
    for line in f:
        params = line.split(",")
        
        # 左手 or 右手
        landmarks = leftLandmarks if params[0] == "0" else rightLandmarks

        # 座標を指定
        landmarks[int(params[1])].location = (float(params[3]),float(params[4]),float(params[5])/512)
        # 指定した座標、フレームにキーフレームを設定
        landmarks[int(params[1])].keyframe_insert(data_path="location", frame=int(float(params[2])*fps))
```



##### 4. スクリプトの実行
Editor Type「Text Editor」にスクリプトを貼り付けてRun Script！

##### 5. Animationの再生
Editor Type「Timeline」から再生！

![pianoplay.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/3851e5c9-5ee8-aad4-b356-7a63a538eee5.gif)


勝ったな:smirk:(フラグ)

##### 6. オブジェクトのエクスポート
File > Export > FBXなどでエクスポートすれば完了！
.blendファイルもUnityでそのまま読み込めます

### Blenderでピアノ作成
白鍵と黒鍵をCubeで並べていきます
アニメーションと合わせるので、実物の鍵盤と同じサイズ比を意識します

<img width="1552" alt="スクリーンショット 2019-12-18 22.36.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/e42822b2-2bc6-6260-c24d-5b8b990eb553.png">




# Unityに取り込んで再生
まずは用意した素材を取り込みます

* アニメーションを適用した3Dモデル
* ピアノオブジェクト

### ピアノの設定
##### 1. 音源Assetのインポート
ここにきて大誤算
**ピアノの単音のフリー音源ってあまり無い**んですね。。。

自前の電子ピアノから音源を作成しました:weary:

コーラス音源であれば、以下のUnity Assetを無料で利用できます
(同じ販売者から「[グランドピアノ音源](https://assetstore.unity.com/packages/audio/grand-piano-sound-pack-7-octaves-27630)」も出品されているのですが$50もする・・・)

* [Choir Piano Sound Pack (7 Octaves)](https://assetstore.unity.com/packages/audio/choir-piano-sound-pack-7-octaves-27633)

##### 2. 鍵盤の動きを設定
鍵盤は以下の条件で動かします

* 動きはy座標回転のみ(xyz位置とxz回転を制限)
* 指との衝突で回転
* 天井と床(鍵盤のみと衝突する)を作り、元の位置より上下しないようにする

鍵盤にはRigidbodyとBoxColliderで当たり判定を付与します

<img width="1161" alt="スクリーンショット 2019-12-22 5.00.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63718/e5de0f25-e381-27db-6812-d3eef4f99727.png">


##### 3. 打鍵すると音が鳴るよう設定
以下の条件で音が鳴るようにしました

* ハンドオブジェクトと当たり判定がある(OnCollisionStay関数)
* かつ鍵盤の角度が一定以下

### アニメーションの設定
* アニメーションコントローラの作成 

* 3Dモデルに適用

### サイズ比率の調整
手と鍵盤とのサイズ比を調整します
サイズ比や位置の調整用に別途アニメーションを作成しておくとよいかもしれません

今回調整用のアニメーションも作成していたのですが、
手がドリルしていて使えませんでした。。。

### 再生
雑音が流れました


# まとめ
MediaPipe面白い技術ですね！

想像していたよりはきれいにアニメーションしてくれました
ちゃんと調整すればメロディーを奏でそうなムーブではある

簡単な曲　→　複雑な曲というシナリオだったのですが思い通りにいかず

本当は3Dモデルにアニメーションさせたかったのですが、
時間も技術も足りませんでした。。。

アプリのログからアニメーションを作成するという方法もスマートじゃないですね
Linuxで実行できたらもう少しやりようがあったかもしれません
MediaPipe開発者が[**Unityのサポートについて反応**](https://github.com/google/mediapipe/issues/36)しているので今後に期待ですね

また先日、VRヘッドセットの**Oculus Quest**にハンドトラッキングが実装されました
**Unity対応のSDK**も今月中にリリースとのことなので、Oculus Questをお持ちの方は是非試してみてください
参考：[「Oculus Quest」にハンドトラッキング機能実装。コントローラなしでメニューなどの操作が可能に](https://www.4gamer.net/games/436/G043602/20191211028/)

ちなみに演奏していた曲は「We Wish You a Merry Christmas」でした
よいクリスマスを✨:santa: