高速な人物切り抜きをTouchDesignerに実装してみた

シトロンです。
TouchDesignerの記事は初めて書きます！（ハードル下げ）
普段は、大学や研究所でTouchDesignerの力を借りて研究システムの開発や結果解析などをしています。
今回の執筆の趣旨は、TouchDesignerにonnx簡単に導入できるっぽいからみんな使おうぜ！です。


## RobustVideoMatting(RVM)とは
YouTubeのショートリール動画がわかりやすいです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/Jvzltozpbpk" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>



いわゆる人物切り抜きのネットワークモデルです。
RobustVideoMatting(RVM) はそのうちの一つですが、高品質かつ高速に動作するということで注目されています。
Cedroさんのツイート（より詳細な説明のブログあり）
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ブログを書きました！<br><br>　通常、動画の背景を後で編集したい場合は、その動画を撮影するときに背景をグリーンバックにしますが、これは結構面倒です。そこで、今回ご紹介するのは、撮影済みの動画の背景をグリーンバックに変更するRVMという技術です。<br><br>ブログ：<a href="https://t.co/PPI1FIdh0I">https://t.co/PPI1FIdh0I</a> <a href="https://t.co/pZktORRFGc">pic.twitter.com/pZktORRFGc</a></p>&mdash; cedro (@jun40vn) <a href="https://twitter.com/jun40vn/status/1462707427472818176?ref_src=twsrc%5Etfw">November 22, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

[Githubのプロジェクトページを見ると](https://github.com/PeterL1n/RobustVideoMatting)、このモデルのonnxが公開されているのでonnxruntimeを使用して、TouchDesigner上で動かしてみました！

このブログでは、全くゼロの状態からRVMを動かすまでを説明します。
だれでも簡単に導入できるっぽいから遊ぼうぜ！という意図で作りました。


### onnxruntimeとは

onnxruntimeとはDeepLearningとして学習されたモデルを動かすためのライブラリ(推論エンジン)です。
特徴として
- onnx形式に変換されたあらゆる学習モデルはonnxruntime上で動かせる
- onnxruntimeはWindows, Mac, Linux, Android, iOSに対応しているので、onnxに変換されたモデルはどのデバイスでも実行できる
*（ただし、現実には性能要件などに制限されます。）*
- CUDAに対応しているonnxruntime-gpuもあるので、NVIDIA GPUのパワーで高速に動かせる
- onnxruntimeはC++, C#, Pythonから利用できるため、使いやすい

と、強力なランタイムとなっています。
今回は、Python上からonnxruntimeを利用できるようにし、さらにTouchDesigner上でそのPythonを動かしたいと思います！

### pyenvとvenvとは (使用する理由)

pyenvとは、Pythonのバージョンを切り替えられる仕組みです。
また、特定のバージョンのPythonを導入するのにコマンド一発でインストールできるため便利です。今回は、TouchDesigner上で動いているPythonのバージョンと揃える必要があるため、利用します。

venvとは、virtual environment、仮想環境を作る仕組みです。
Pythonで使用するライブラリ(numpyやonnxruntimeなど)をどかっと揃えられます。さらに、プロジェクトごとに使用するライブラリ環境を切り替えられるため、複数のonnxを走らせたいときや違うTouchDesignerとの競合を発生させないために重宝します。

## Python(pyenv + venv)導入

### 最新のTouchdesignerをインストールする(2022.29850)

TouchDesigner上のpythonのバージョンを確認する

![ 2022-12-01 18.24.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/a5590079-cc86-0e59-9e2d-92f805045188.png)

左上からTextport and DATを押してPython version確認
渡しの場合は最新のTouchDesignerで、3.9.5でした。

### pyenvをインストールする
pyenvとは、複数versionPython共存させるシステムです。
pyenvを使ってPython 3.9.5をダウンロードします。

https://github.com/pyenv-win/pyenv-win#quick-start

にアクセスして、Quick startの章に従って**Powershell**上でpyenvをインストールします。

手順5の`pyenv install <version>`
は
`pyenv install 3.9.5`
と実行し、python 3.9.5をインストールします。
インストールが終わったら、
`pyenv global 3.9.5`
を実行し、python 3.9.5をアクティベートする。
Pythonを起動して無事3.9.5であることを確認しましょう！

### venvを入れる
まず、自分のプロジェクトフォルダをエクスプローラーなどで適当に作成して、そこに移動(cd)します。
`cd ディレクトリパス`
Powershell上でディレクトリを移動したら、
`python -m venv RVM`

などとして、RVM用の仮想環境を用意する

RVMの仮想環境は作成したので、それをPythonが利用するようにします。(アクティベート)

`.\RVM\Scripts\activate`

仮想環境の使用をやめるときは、

`deactivate`

で抜けられます。

## onnxruntime環境を構築

### onnxruntimeをinstallしてversionを確認する

まず、pipでonnxruntime-gpuを入れましょう（仮想環境をアクティベートした上でね！）

`pip install onnxruntime-gpu`

そしたら、インストールしたonnxruntimeのversionを確認します。(多分1.13)

pythonを起動して

```
import onnxruntime
onnxruntime.__version__
```

でバージョンが確認できます。
>Tips
`pip install onnxruntime-gpu==1.12`などとすることで、指定のバージョンのonnxruntimeをインストールできます。


### 対応したCUDAとcuDNNを導入

onnxruntimeとcudaとcudnnには依存関係があるので、公式のドキュメントを見ます。

https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html#requirements

私の環境では、onnxruntimeのバージョンは1.13だったので、CUDA11.6とcuDNN8.5.0.96を入れる必要があると分かりました。

https://developer.nvidia.com/cuda-toolkit-archive

上のページから11.6.2のCUDAをダウンロード

インストールを実行して進めていると

![スクリーンショット (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/e1d89c24-73a2-0747-066c-d1d5522bba16.png)

インストールオプションで高速（推奨）とカスタムがあるので、カスタムを選択する。
> 高速の説明を読むとわかるが、高速はDisplay driverを上書きしてしまう。既存の開発システムの依存関係を破壊してしまう可能性があるので、カスタムが良い

![スクリーンショット (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/c5054279-5ea5-a8c9-de25-b0414a21711b.png)

次のページのカスタムインストールオプションでは、CUDAにだけチェックを入れて、次へ進む

インストールが終わったら、次はcuDNNのダウンロード＆インストール

https://developer.nvidia.com/rdp/cudnn-archive

上のページから8.5.0をダウンロード

ここで、Nvidiaのアカウントが必要になります！持ってない人は作る必要があります！

![スクリーンショット (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/23c8a312-5966-54d2-f23b-1a7f3e7fe11d.png)
ダウンロードしてZIPを展開したら、bin, include, libの3つのフォルダをCUDAの所定の場所に移動します。

お世話になったサイト

[https://self-development.info/onnx-runtime（gpu版）のインストール/](https://self-development.info/onnx-runtime%EF%BC%88gpu%E7%89%88%EF%BC%89%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/)

[https://self-development.info/tensorflow-1系gpu版のためにcuda-10-0をインストール/](https://self-development.info/tensorflow-1%e7%b3%bbgpu%e7%89%88%e3%81%ae%e3%81%9f%e3%82%81%e3%81%abcuda-10-0%e3%82%92%e3%82%a4%e3%83%b3%e3%82%b9%e3%83%88%e3%83%bc%e3%83%ab/)

### **RVMに必要なmodelを導入する**
onnxファイルをダウンロードします。

https://github.com/PeterL1n/RobustVideoMatting

のダウンロードの章からrvm_resnet50_fp16.onnxを自分のプロジェクトフォルダにダウンロード

### TouchDesignerとPython仮想環境をつなげる

作成した仮想環境にTouchDesignerからアクセスできるようにします。

TouchDesignerを起動して、左上のEditからPreferenceを開く。
GeneralのページにAdd External Python to Search Pathというチェックがあるので、チェックされていることを確認する。

下にある、Python 64-bit Module Pathにvenvで作成した仮想環境フォルダのパスを入れる。

例えば、RVMという仮想環境名だったら、RVM/Lib/site-packagesまでの絶対パスを入れる。

SaveしてPreferenceを閉じる。

site-packagesが有効であることを確認するために、TouchDesignerを再起動し、textport DATを開き、

```
import onnxruntime
```

を実行してみる。なんのエラーも出なけれOK！

以下のtoxをダウンロードして、カメラ画像などを突っ込めば、結果が見れると思います！

以下、toxの解説part(本編)

### onnxを実行するプログラムを書く

[https://github.com/PeterL1n/RobustVideoMatting/blob/master/documentation/inference.md#onnx](https://github.com/PeterL1n/RobustVideoMatting/blob/master/documentation/inference.md#onnx)

Robust Video Mattingのプロジェクトページを見ると、モデルを読んで、推論ループを回すだけで良さそう。（かんたんでありがとう🙏）

大事なのは、specに書いてあるsrcのshapeすなわち次元構成を適切に用意してあげること。


numpyArrayとなったテクスチャは[h, w, c]の三次元でデータが格納されています。ここでhがy座標、wがx座標、cがRGBAのチャネルになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1769118/55a05728-2512-8844-910a-6748e3862280.png)
RVMのドキュメントを読むと、これを[B, C, H, W]の次元形式に再配置します。

そのために、以下のような変換をします。
1行目では、[h, w, c] -> [c, h, w]に変換しています。(同時に、16bitfloatにキャスト)
2行目では空のバッチ次元Bを追加し、c次元についてRGBAの4チャネルからRGBの3チャネルに変更しています。
(...はtempの残りの次元を表現する省略記法)

```Python
	temp = np.transpose(npArray, [2, 0, 1]).astype(np.float16)
	src = temp[np.newaxis, [0,1,2], ...]
```

結果として、onnxを回すスクリプトは以下のようになりました。
```Python
import numpy as np
import onnxruntime as ort

res = [op('script_gpu').inputs[0].width, op('script_gpu').inputs[0].height]

# load onnx file
sess = ort.InferenceSession('\\RVM\\model\\rvm_resnet50_fp16.onnx',
providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])

io = sess.io_binding()
rec = [ ort.OrtValue.ortvalue_from_numpy(np.zeros([1, 1, 1, 1], dtype=np.float16) , 'cuda')] * 4  # Must match dtype of the model.
downsample_ratio = ort.OrtValue.ortvalue_from_numpy(np.array([1.0], dtype= np.float32), 'cuda')
for name in ['fgr', 'pha', 'r1o', 'r2o', 'r3o', 'r4o']:
	io.bind_output(name, 'cuda')	



def onCook(scriptOp):
	global rec

	# download the input texture to the script TOP into a NumPy array
	npArray = scriptOp.inputs[0].numpyArray(delayed=True)

	
	# transpose for RVM
	temp = np.transpose(npArray, [2, 0, 1]).astype(np.float16)
	src = temp[np.newaxis, [0,1,2], ...]
	
	# run RVM
	io.bind_cpu_input('src', src)
	io.bind_ortvalue_input('r1i', rec[0])
	io.bind_ortvalue_input('r2i', rec[1])
	io.bind_ortvalue_input('r3i', rec[2])
	io.bind_ortvalue_input('r4i', rec[3])
	io.bind_ortvalue_input('downsample_ratio', downsample_ratio)
	
	sess.run_with_iobinding(io)

	# get output
	fgr, pha, *rec = io.get_outputs()	
	# convert to numpy array
	pha = pha.numpy()
	
	# copy the NumPy array into the texture
	scriptOp.copyNumpyArray(np.transpose(pha[0], [1,2,0]).astype(np.float32))
	return
```

# Result
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今回投稿するTouchDesignerのAdC<br>こんな感じの背景切り抜きができます！ <a href="https://t.co/TGpZvEwZJL">pic.twitter.com/TGpZvEwZJL</a></p>&mdash; シトロン (@CitronSeason) <a href="https://twitter.com/CitronSeason/status/1598667951070117889?ref_src=twsrc%5Etfw">December 2, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

実行結果はこんな感じで、60fpsで高精度に実行できました。
Nvidia Backgroundよりきれいかつ悪い撮影環境でもきれいな切り抜きが期待できそうです。
実はこの結果は1fだけ遅延していますが、
`scriptOp.inputs[0].numpyArray(delayed=True)`
を`delayed=Flase`とすれば遅延はなくなります。(すこしパフォーマンスが悪くなる。)


一方で、それでもシングルプロセスのTouchDesignerでは、この処理により律速してしまいfpsが低下する可能性があります。
その場合は、刻みTDやEngineCOMPでマルチプロセスかすると良さそうです！

https://qiita.com/shks/items/656186c5127869bfbe84

https://qiita.com/motoishmz/items/53b96dfa877a6d76d9c6

https://qiita.com/kyeshmz/items/fe1e89e1bf046f8e4de1

toxファイルを置いておきます！（環境を構築したら使えるはず！）
本当は、他のonnxも回してみたかったけど、とりあえずここまでで失礼します！

https://drive.google.com/file/d/1TOlYD9akfLMXSJ8lZMCUXxpcKNfvo2Oy/view?usp=share_link