Soccer On Your Tabletop を AWS で動かして、HoloLens にデプロイしてみた

# Soccer On Your Tabletop

サッカー動画から半自動的に選手の3Dメッシュを作る [SoccerOnYourTabletop](https://github.com/krematas/soccerontable) をAWS上に環境構築し、HoloLensにデプロイした。その際、依存しているライブラリなどの影響で思いのほか修正する必要が出たので、それを共有しようと思う。

[![リンクが切れました。プロジェクトページから探してください](http://img.youtube.com/vi/eRGAB4QBS6U/0.jpg)](http://www.youtube.com/watch?v=eRGAB4QBS6U)
（画像クリックすると再生します）

## 環境構築編
[SoccerOnYourTabletop](https://github.com/krematas/soccerontable) が依存しているライブラリは

- Detectron
    - COCO API
- OpenPose
    - OpenCV3.1 + OpenCV_contrib
    - Eigen3

それに加えて pip3 で入れるものもある（ただ自分は conda で既に入れていたものもあったので、一部だけを pip で）
### 初期環境
- AWS [DeepLearningBaseAMI](https://aws.amazon.com/marketplace/pp/B077GCZ4GR?qid=1532926710884&sr=0-2&ref_=srh_res_product_title)
    - Ubuntu 16.04
    - CUDA-8.0/cudnn7.1.4
        - はじめから caffe とかが動作する AMI を使うのがいいかも
            - [DeepLearningAMI](https://aws.amazon.com/marketplace/pp/B077GCH38C?qid=1532926606578&sr=0-1&ref_=srh_res_product_title) とか
        - AMI の種類については [MarketPlace](https://aws.amazon.com/marketplace/search/results?searchTerms=deep+learning+ubuntu+&page=1&ref_=nav_search_box&x=0&y=0) から
    - p2.xlarge
- Python 3.6 と 2.7
    - どのように管理するかは任せるが自分は anaconda で環境を作った
- リモートデスクトップ接続環境
    - https://aws.amazon.com/jp/premiumsupport/knowledge-center/connect-to-linux-desktop-from-windows/ を参照のこと
    - SoccerOnTabletop で動画内のサッカーコートを正しく認識するため（カメラパラメータ推定のため）に GUI 操作が必要になるため必要（後述）

### [Detectron](https://github.com/facebookresearch/Detectron)
Facebook が state-of-the-art な物体検出アルゴリズムを複数実装したもの。Caffe2 で書かれている。 
Python2 を使っているので **環境を Python2 に切り替えて** から以下の操作を行うこと。

**<div>
1. Caffe2 のインストール
</div>**

- https://caffe2.ai/docs/getting-started.html?platform=windows&configuration=compile に従う
- ソースビルドしてもいいが、自分は `conda install -c caffe2 caffe2-cuda8.0-cudnn7` でインストールした
- 【要注意】 cudnn7 とあるが、 cudnn7.0.4 や cudnn7.0.5 では動かない。 cudnn7.1.4 が必要

**<div>
2. COCO API のビルド
</div>**

- COCO は物体検出用に作られた大規模な画像データセットで、このライブラリにはそれを扱うための API が用意されている
    - [COCO API](https://github.com/cocodataset/cocoapi)
- ビルドは以下を実行

```bash
# COCOAPI=/path/to/clone/cocoapi
git clone https://github.com/cocodataset/cocoapi.git $COCOAPI
cd $COCOAPI/PythonAPI
# Install into global site-packages
make install
# Alternatively, if you do not have permissions or prefer
# not to install the COCO API into global site-packages
python2 setup.py install --user
```
**<div>
3. Detectron のビルド
</div>**

- 以下を実行

```bash
# DETECTRON=/path/to/clone/detectron
# Detectron リポジトリのクローン
git clone https://github.com/facebookresearch/detectron $DETECTRON
# Python の依存パッケージのインストール
cd $DETECTRON && pip install -r requirements.txt
# Python モジュールのセットアップ
make
# Detectron のテスト
python2 $DETECTRON/detectron/tests/test_spatial_narrow_as_op.py
```

### [OpenPose](https://github.com/CMU-Perceptual-Computing-Lab/openpose)
人体のボーンを推定するライブラリ。 cudnn5.1 が求められているが、 caffe2 をインストールする都合上 cudnn7.1.4 のまま続行（結果としてはうまくいった）。

**<div>
1. リポジトリのクローン
</div>**

```bash
git clone https://github.com/CMU-Perceptual-Computing-Lab/openpose
```
**<div>
2. CMake GUI のインストール
</div>**

```bash
sudo apt-get install cmake-qt-gui
```
- CMake は GUI を使うほうが設定しやすい。せっかくリモートデスクトップ環境が用意できているので GUI を推奨

**<div>
3. 他の依存ライブラリのインストール
</div>**

```bash
sudo apt-get --assume-yes install libatlas-base-dev libprotobuf-dev libleveldb-dev libsnappy-dev libhdf5-serial-dev protobuf-compiler
sudo apt-get --assume-yes install --no-install-recommends libboost-all-dev
```

- [インストールガイド](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/installation.md)では ```sudo bash ./ubuntu/install_cmake.sh``` することを推奨しているが、cudnn5.1 が勝手に入れられてしまうので、必要な部分だけ抽出

**<div>
4. OpenCV3.1.0+contrib のビルド
</div>**

3.4.1 や 2.4.13 などで試してみたが、エラーが出たため、 **OpenCV3.1.0 が必要** 

- 詳細は[Ubuntu Desktop 16.04上で、OpenCV3.1.0をビルドする](http://thaim.hatenablog.jp/entry/2016/07/11/004631)を参考にした
    - Soccer On Tabletop では OpenPose の Python API は使用しないため、 OpenCV 側も Python API をインストールする必要がない。なので Python 部分は飛ばしてよい。その場合 CMake する際に `BUILD_opencv_python2` と `BUILD_opencv_python3` のチェックを外しておく
- 以下のページで OpenCV3.1 と Contrib が手に入る
    - https://github.com/opencv/opencv/releases/tag/3.1.0
    - https://github.com/opencv/opencv_contrib/releases/tag/3.1.0

**<div>
5. Eigen3 のインストール
</div>**

- OpenPose をビルドする際に `WITH_EIGEN` にチェックを入れれば自動でビルドしてくれる。それか以下を実行する。

```bash
sudo apt-get install libeigen3-dev
```

**<div>
6. OpenPose のビルド
</div>**

- 基本的に[インストールガイド](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/installation.md#openpose-configuration)に従う
- Caffe をまだインストールしていない場合は、 `BUILD_CAFFE` にチェックを入れる
    - make する際に `#error This file was generated by a newer version of protoc which is
` というようなエラーが出たら、 Caffe をビルドする際の protobuf のバージョンが求められているものより高いということになる。
    - `protoc --version` を実行し、 2.6.1 より高い場合、 conda 等でインストールしたものが使われている可能性がある。
    - `which protoc` を実行するとどの場所にインストールされているかわかるので、conda でインストールしたものっぽいと思ったら、環境変数 `PATH` を `echo $PATH` で確認する。
    -  `PATH` の上位に `which protoc` のフォルダが含まれていたら、そのフォルダを `PATH` の下位に来るよう書き換える
    - `protoc --version` で正しいバージョンになっているかを確認する
    - 例）

```bash
$ which protoc
/home/ubuntu/.pyenv/shims/protoc
$ echo $PATH
/home/ubuntu/.pyenv/shims:/usr/bin
# /home/ubuntu/.pyenv/shims が前に来ているので後ろにする
$ export PATH="/usr/local/bin:/home/ubuntu/.pyenv/shims"
/usr/bin:/home/ubuntu/.pyenv/shims
$which protoc
/usr/bin/protoc
# 変更された
``` 

- OpenCV を自分でビルドした場合は
    - `OpenCV_CONFIG_PATH` に `/usr/local/share/OpenCV/`
    - `OpenCV_DIR` に `/usr/local/share/OpenCV/` をセットする

**<div>
7. OpenPose の確認
</div>**

- https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/quick_start.md#quick-start にあるようなサンプルをいくつか試してみる

## 3Dメッシュ生成編
Detectron や OpenPose は SoccerOnTabletop プロジェクトの Readme が作られてからバージョンアップに伴う仕様変更が発生していたため、実際は数か所の修正が必要となった。
修正済みのコードは本家からフォークしたリポジトリ
https://github.com/robonich/soccerontable
に用意したので、よかったら使ってください。

**<div>
1. プロジェクトのクローンと Python パッケージのインストール
</div>**

```bash
# SOCCERCODE=/path/to/soccercode
git clone https://github.com/krematas/soccerontable $SOCCERCODE
# pip をするかは状況に合わせて
pip3 install -r requirements.txt
# 後ほど使うコードのビルド
bash compile.sh
```
**<div>
2. 入力動画（フレーム毎にjpgで保存されている）のサンプルをダウンロード
</div>**

```bash
wget http://grail.cs.washington.edu/projects/soccer/barcelona.zip
unzip barcelona.zip
# DATADIR=/path/to/barcelona
```
**<div>
3. Detectron による人物検出
</div>**

```bash
mkdir $DATADIR/detectron
# DETECTRON=/path/to/clone/detectron
cp utils/thirdpartyscripts/infer_subimages.py ./$DETECTRON/tools/
cd $DETECTRON
python2 tools/infer_subimages.py --cfg configs/12_2017_baselines/e2e_mask_rcnn_R-101-FPN_2x.yaml --output-dir $DATADIR/detectron --image-ext jpg --wts https://s3-us-west-2.amazonaws.com/detectron/35861858/12_2017_baselines/e2e_mask_rcnn_R-101-FPN_2x.yaml.02_32_51.SgT4y1cO/output/train/coco_2014_train:coco_2014_valminusminival/generalized_rcnn/model_final.pkl $DATADIR/images/
```

- このまま実行すると import エラーとなるので、コード変更を加える
    - Detectron 側でフォルダ構成が変更されており、core や utils フォルダが detectron の下に入ったため

変更前

```python
# infer_subimages.py:40
from core.config import assert_and_infer_cfg
from core.config import cfg
from core.config import merge_cfg_from_file
from utils.timer import Timer
import core.test_engine as infer_engine
import datasets.dummy_datasets as dummy_datasets
import utils.c2 as c2_utils
import utils.logging
import utils.vis as vis_utils
import pycocotools.mask as mask_util
# infer_subimages.py:217
utils.logging.setup_logging(__name__)
```

変更後

```python
# infer_subimages.py:40
from detectron.core.config import assert_and_infer_cfg
from detectron.core.config import cfg
from detectron.core.config import merge_cfg_from_file
from detectron.utils.timer import Timer
import detectron.core.test_engine as infer_engine
import detectron.datasets.dummy_datasets as dummy_datasets
import detectron.utils.c2 as c2_utils
import detectron.utils.logging
import detectron.utils.vis as vis_utils
import pycocotools.mask as mask_util
# infer_subimages.py:217
detectron.utils.logging.setup_logging(__name__)
```

- 次にこのようなエラーが起こる

```bash
Traceback (most recent call last):
  File "tools/infer_subimages.py", line 222, in <module>
    main(args)
  File "tools/infer_subimages.py", line 106, in main
    model = infer_engine.initialize_model_from_cfg()
TypeError: initialize_model_from_cfg() takes at least 1 argument (0 given)
```

- model 作成部分( `infer_engine.initialize_model_from_cfg()` ) が、新しいバージョンでは `weight_file` を引数に取るようになったから

```python
# detectron/core/test_engine.py:324
def initialize_model_from_cfg(weights_file, gpu_id=0):
```

- なので以下のように infer_subimages.py を書き換える

変更前

```python
# infer_subimages.py:102
model = infer_engine.initialize_model_from_cfg()
```

変更後

```
# infer_subimages.py:102
model = infer_engine.initialize_model_from_cfg(args.weights)
```

- ~~しょうもない~~ Typo も修正する

変更前

```python
# infer_subimage.py:111
for ii, im_name in enumerate(im_list):
# infer_subimage.py:144
if i == 0:
# このまま実行すると i なんてないと怒られる
```

変更後

```python
# infer_subimage.py:111
# i に直す
for i, im_name in enumerate(im_list):
# infer_subimage.py:144
if i == 0:
```

- `weight_file` はローカルにある重みファイルのパスを要求しているので、 `model_final.pkl' をダウンロードしてから再度プログラムを実行する

```bash
$ wget https://s3-us-west-2.amazonaws.com/detectron/35861858/12_2017_baselines/e2e_mask_rcnn_R-101-FPN_2x.yaml.02_32_51.SgT4y1cO/output/train/coco_2014_train:coco_2014_valminusminival/generalized_rcnn/model_final.pkl
$ python2 tools/infer_subimages.py --cfg configs/12_2017_baselines/e2e_mask_rcnn_R-101-FPN_2x.yaml --output-dir $DATADIR/detectron --image-ext jpg --wts model_final.pkl $DATADIR/images/
```   

- 実行後は `$DATADIR/detectron` に以下のような人物のマスク画像が生成されているはず
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/7ae78392-6aff-db4b-4169-ea02269a0e1a.png)

- もし真っ黒の画像しかなかった場合は Detectron が正常に動いていないので Caffe2 をインストールしなおしてみるなどをおすすめする
    - 筆者はここで結構時間がかかった

**<div>
4. カメラキャリブレーション
</div>**

試合撮影に使われたカメラのパラメータを推定する。このとき画像と理想のサッカーコートの対応点を4つ手動で教える必要があり、GUI操作が発生するので、リモートデスクトップ接続をしながら以下のコマンドを実行する。

- `demo` フォルダに入っている Python ファイルの先頭にすべて `import sys; sys.append('.')` を追記する
    - 一部のモジュールが読み込めなくなるため
- 以下を実行し、キャリブレーションを開始する

```bash
cd $SOCCERCODE
python3 demo/calibrate_video.py --path_to_data $DATADIR
```
-  
    - このとき、謎の `Segmentation Fault` が発生したら、 `matplotlib` が描画に使用するバックエンドを疑うとよい。筆者の場合 Qt がうまく動いていなかったため、 TkAgg に切り替えることで事なきを得た
    - 先ほど追記したものの *直後の行** に `import matplotlib as mlp; mlp.use('TkAgg)` を挿入し再度実行
- 以下の画面が出現する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/5f2b77d9-f628-3c6c-a73f-1cfc260ccf2c.png)

- 実画像とサッカー場の対応点を4つ選択する
    - このときはなるべくばらけるように対応点を教えてあげるとよい（カメラの歪みが分かるように）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/62720843-885c-1781-1fc8-c2cca7731d6f.png)

- ウィンドウを閉じるとキャリブレーションが始まり、しばらくするとキャリブレーション結果を表示してくれるので、問題がなさそうなら右側の `Save opt` を押す。満足いかないなら Discard を押して最初から
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/caef5322-8888-9509-4c90-8a2c1f9a2de2.png)

- プログラムが無事に終わると `$DATADIR/calib.mp4` が生成されているはずなので、確認してみるとよい

**<div>
5. OpenPose による選手のポーズ推定
</div>**

- 以下を実行する

```bash
# OPENPOSEDIR=/path/to/openpose/
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64/
python3 demo/estimate_poses.py --path_to_data $DATADIR --openpose_dir $OPENPOSEDIR
``` 
   
- するとエラーが発生する

```python
Traceback (most recent call last):
  File "demo/estimate_poses.py", line 23, in <module>
    db.refine_poses(keypoint_thresh=7, score_thresh=0.4, neck_thresh=0.4)
  File "./soccer3d/core.py", line 335, in refine_poses
    poses = [poses[ii] for ii in keep2]
  File "./soccer3d/core.py", line 335, in <listcomp>
    poses = [poses[ii] for ii in keep2]
IndexError: list index out of range
```

- `$DATADIR/tmp` に残されている、一部の OpenPose による選手の姿勢検出結果が正しそうであれば、その姿勢検出結果を読み込む際に何かしらの不具合が発生している、つまり `poses` に正しく結果が格納されていないことが分かる
- 原因は OpenPose のバージョンアップに伴う、検出結果を格納する `yml` ファイル中の記述方式が変わったことにあった
- `soccer3d/core.py` では `sizes` というキーがなければ推定された姿勢はないとしているが、`yml` にはそもそも `sizes` キーが存在せず、代わりに `rows` がその役割を果たしていた

```python
# soccer3d/core.py:267
with open(join(join(self.path_to_dataset, 'tmp'), '{0}_pose.yml'.format(j))) as data_file:
        for iii in range(2):
            _ = data_file.readline()
        data_yml = yaml.load(data_file)
        if 'sizes' not in data_yml: # sizes というキーは存在しない！
            continue
        sz = data_yml['sizes']
        n_persons = sz[0]
        keypoints = np.array(data_yml['data']).reshape(sz)
```

- 実際の `yml` ファイル

```yaml
pose_0: !!opencv-matrix
           rows: 1
           cols: 18
           dt: "3f"
           data: [ 2.09722397e+02, 1.72544205e+02, 8.75938118e-01,
               2.12439835e+02, 1.84927582e+02, 8.21689487e-01, 1.93255615e+02,
               1.89076950e+02, 8.30192327e-01, 1.71222092e+02, 2.04145599e+02,
               9.45078433e-01, 1.68364136e+02, 2.28972809e+02, 8.77120316e-01,
               2.26218674e+02, 1.82248367e+02, 9.07761514e-01, 2.34456604e+02,
               2.04210846e+02, 7.27888823e-01, 2.34459930e+02, 2.16570267e+02,
               4.25617874e-01, 2.05596100e+02, 2.37232941e+02, 8.36183906e-01,
               2.26199478e+02, 2.81217010e+02, 8.69147360e-01, 2.26268295e+02,
               3.25345306e+02, 8.77623141e-01, 2.23434952e+02, 2.37217804e+02,
               8.03246856e-01, 1.93230988e+02, 2.68889832e+02, 8.75034094e-01,
               1.61544891e+02, 2.90896118e+02, 8.80115747e-01, 2.04266312e+02,
               1.71097534e+02, 9.56580102e-01, 2.13780472e+02, 1.69820312e+02,
               9.55266714e-01, 2.02746841e+02, 1.71110229e+02, 7.24748433e-01,
               2.16576248e+02, 1.69770874e+02, 8.13678980e-01 ]
```

- そのため先ほどの `yml` ファイル読み込み部分を以下のように書き換える

```python
# soccer3d/core.py:267
with open(join(join(self.path_to_dataset, 'tmp'), '{0}_pose.yml'.format(j))) as data_file:
        for iii in range(2):
            _ = data_file.readline()
        data_yml = yaml.load(data_file)
        # 変更開始
        if 'rows' not in data_yml:
            continue
        sz = (data_yml['rows'], data_yml['cols'], -1)
        # 変更終了
        n_persons = sz[0]
        keypoints = np.array(data_yml['data']).reshape(sz)
```

- もう一か所 OpenPose のアップデートによる変更がある

変更前 （`--no_display` というオプションはもうない）

```python
# soccer3d/core.py:254
command = '{0} --model_pose COCO --image_dir {1} --write_keypoint {2} --no_display'.format(openposebin,
```

変更後（`--display 0 --render_pose 0` を代わりにつける）

```python
# soccer3d/core.py:254
command = '{0} --model_pose COCO --image_dir {1} --write_keypoint {2} --display 0 --render_pose 0'.format(openposebin,
```

- ほかの OpenPose のオプションに関しては[ここ](https://github.com/CMU-Perceptual-Computing-Lab/openpose/blob/master/doc/demo_overview.md)を参照
- これで再度実行し、 `$DATADIR/poses.mp4` が以下のように正しく選手の上にボーンを重畳させている形で生成されていたら成功
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/a110fac4-4699-06d8-d99f-7565c626db73.png)

**<div>
6. 各選手の切り出しを行う
</div>**

検出されたポーズの情報を使って、選手を中心とした画像の切り出しを行う。その後 Instance Segmentation によって選手のみの切り出しをする。

- 以下を実行する

```bash
# 選手を中心とした画像の切り出し
python3 demo/crop_players.py --path_to_data $DATADIR
export OMP_NUM_THREADS=8
# ステップ1 で instancesegm のビルドが終わっている必要がある
# 選手の Instance Segmentation
./soccer3d/instancesegm/instancesegm --path_to_data $DATADIR/players/ --thresh 1.5 --path_to_model ./soccer3d/instancesegm/model.yml.gz
```

- `$DATADIR/players` フォルダが作られ、各フレーム中に出現する選手を切り出した画像(images)や、そのマスク画像(cnn_masks)、姿勢画像(poseimgs)、姿勢マスク(pose_masks)が生成される
    - 姿勢マスクは完全に Instance Segment されたもの
- 補足： Instance Segmentation について
    - 領域分割（Segmentation）というタスクがあり、これは画像内のピクセルごとにラベルを予測する問題
    - Segmentation には以下の二つのラベル付けの仕方がある
        - Semantic Segmentation
            - 各ピクセルにクラスのラベルを付与する
            - 同じクラスに分類されるピクセルには同じラベルを付ける
        - Instance Segmentation
            - 個々の物体ごとに別のラベルを付与する
            - 同じクラスであっても、別々のラベルを付与する
    - 今回は選手ごとに分けたいから Instance Segmentation
    - 例） http://host.robots.ox.ac.uk/pascal/VOC/voc2012/segexamples/index.html

| 元画像 | Semantic | Instance |
|:-:|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/3148aa28-1340-565f-5b7c-58f00bbcebe7.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/08cbdbe5-c596-6bc5-e83d-00cc0e6f2d01.png) | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/84d21c15-e09f-e188-3a0d-d702b1e760be.png) |

**<div>
7. 深度情報を算出する
</div>**

先ほどの二つのマスクを統合して、そのマスクを入力すると深度情報を出力してくれるネットワークに通す

- 深度ネットワークの重みを[ここから](https://drive.google.com/file/d/1QBLyoNBrFu0oYr15WECzCfOgzuAAQW7w/view?usp=sharing)事前に `MODELPATH=/path/to/model/` にダウンロード
- 以下を実行

```bash
# MODELPATH=/path/to/model/
python3 demo/combine_masks_for_network.py --path_to_data $DATADIR
python3 soccer3d/soccerdepth/test.py --path_to_data $DATADIR/players --modelpath $MODELPATH
```

- 統合されたマスク(masks) と深度情報(predictions)が生成されていることを確認

**<div>
8. 深度情報を点群データに、そしてメッシュにする
</div>**

まずは深度情報を点群データに変換し、選手ごとにトラッキングして選手ごとのメッシュデータを作成する
    
- 以下を実行

```bash
# 点群データへ
python3 demo/depth_estimation_to_pointcloud.py --path_to_data $DATADIR
# 選手のトラッキング
# GUI 出力があるので RemoteDesktop で実行
python3 demo/track_players.py --path_to_data $DATADIR
# メッシュ生成
# GUI 出力があるので RemoteDesktop で実行
python3 demo/generate_mesh.py --path_to_data $DATADIR
```

- 点群データは `$DATADIR/players/meshes` に保存される
- トラッキング実行時に以下のような画面が確認できる
![image.png](https://qiita-image-store.s3.amazonaws.com/0/252487/7b346040-8311-0f6a-49fd-248ede3344b3.png)

- また `$DATADIR/tracks.mp4` が生成される
- テクスチャとメッシュデータは `$DATADIR/scene3d` に保存される

## HoloLens デプロイ編
1. [Unity プロジェクト](https://drive.google.com/open?id=1jA4MoAogphjj7Mvl-f2drkZIVPbtaGy9) のダウンロード
1. `$DATADIR/scene3d` をローカルにダウンロード
    - テクスチャとメッシュデータが混在しているので、事前に `meshes` と `images` のフォルダに分けておく
    - こんな感じで
        - `cd $DATADIR/scene3d;
mkdir images;
mkdir meshes;
mv *.jpg images;
mv *.obj *.mtl meshes;
`
1. プロジェクトの起動、youtube シーンを開き、シーンを別名で保存
1. `Assets/Resources/Images` にテクスチャ、 `Assets/Resources/Meshes/Players` にメッシュデータを配置
    - 既存のデータが入っているので、それらは消去しておく
1. Hierarchy で、 Scene->Players オブジェクトの子を消去してから、新しいメッシュデータを Players の子としてドロップ
    - このとき Players のインスペクタでLayerを `Do not show`に切り替える（子も同時に切り替える）
    - 選手がフィールドに半分埋まる状態になるので、全選手の y 座標を 1 にする
1. Hierarchy で、 MainCamera の AssignTextureToObj を変更
    - `STARTFRAME` には最初のフレーム番号（00はなくていい） 
    - `ENDFRAME` には最後のフレーム番号（00はなくていい）
    - `GAMEOBJECT_PREFIX` はフレーム番号の前に prefix があれば埋める。なければ空欄
    - `GAMEOBJECT_POSTFIX` はフレーム番号の後、拡張子の前に postfix があれば埋める。なければ空欄
    - `IMAGE_PREFIX` も同様
    - `IMAGEPATH` は `Assets` からの相対パス
1. MainCamera の ShowOneLayerPerFrame を変更
    - `STARTFRAME` `ENDFRAME` `GAMEOBJECT_PREFIX` `GAMEOBJECT_POSTFIX` は同上
    - `FPS` は動画のFPS
    - `GOOD_LAYER` 表示したいレイヤーの番号（ここだと9）
    - `BAD_LAYER` 表示したくないレイヤーの番号（ここだと8）
1. あとは通常の HoloLens アプリ通り UWP でビルドして、VisualStudio で HoloLens へデプロイ
このような画面が出ていればOK
<img width="664" alt="スクリーンショット 2018-09-03 15.53.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/252487/1e066860-0346-5bbc-b2f2-2f6ea19b1f55.png">

## 終わりに
筆者は他の動画でも試したいと思って元動画の別のシーンから切り出してみたが、 カメラキャリブレーションのフェーズで最初の数フレームはうまくできていても、後半になるとズレていくようになった。

| 成功フレーム | 失敗フレーム |
|:-:|:-:|
|<img width="1072" alt="スクリーンショット 2018-09-03 16.35.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/252487/7526284d-1ef3-78e0-1f4f-ae6ebd1f7d85.png">|<img width="1072" alt="スクリーンショット 2018-09-03 16.36.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/252487/746db7be-465e-8aa6-fc3f-37fbacfd2275.png">|

キャリブレーションフェーズではまず動画の最初のフレームのサッカー場との対応点を手動で指定することでカメラパラメータを求めるが、それ以降は、カメラパラメータはズームイン・ズームアウトによって変化してしまうため、補正を掛ける必要がある。
元プロジェクトでは [`calibrate_camera`](https://github.com/robonich/soccerontable/blob/master/soccer3d/core.py#L91) の  内の、 [`calibration.calibrate_from_initialization`](https://github.com/robonich/soccerontable/blob/master/soccer3d/core.py#L129) にて、最初のフレーム以降のカメラパラメータを推定しようとしているが、ここがうまくいっていないため、キャリブレーションの失敗が発生している。
本エントリではこの問題の解決に至っていないため、新しい動画を3D化したいと思う方は

1. カメラのズームイン・ズームアウトが発生しないサッカー動画を見つける
2. すべてのフレームを手動でキャリブレーションをする
3. `calibration.calibrate_from_initialization` での推定精度を向上させる

という3つの選択肢があることだけ述べておく。