NatML for Unityの導入とMLモデルを実際に動かしてみた

# はじめに
こちらは「[QualiArts Advent Calendar 2022](https://qiita.com/advent-calendar/2022/qualiarts)」 20日目の記事になります。

株式会社QualiArtsでUnityエンジニアをしています山本です。
今回はNatMLの紹介と導入、実際にMLモデルを動かすところまでやってみた記事になります。

# NatML for Unityとは
Unity Engine用の機械学習のランタイム。クロスプラットフォームかつアプリでMLモデルを実行することが可能になる。
ですので使い方次第でUXの可能性が広がるようなランタイムとなっています。

またNatMLは高性能の対話型アプリケーションを中心に設計されており、様々な特徴があります(*1)。
| 特徴 | 概要 |
|:-:|:-:|
|ユニバーサル機械学習 <br> Universal Machine Learning   |CoreML(.mlmodel), ONNX(.onnx), TensorFlow Lite(.tflite)モデルを直接Unityプロジェクトにドラッグ&ドロップすることで実行が可能   |
|ベアメタルパフォーマンス <br> Bare Metal Performance   |iOS・MacOSのCoreML, AndroidのNNAPI, WindowsのDirectMLなどハードウェア機械学習アクセラレータを利用することによって、Unity独自のBarracudaエンジンより数倍高速(*2)に動作する   |
|クロスプラトフォーム <br> Cross Platform   |iOS, MacOS, Android, Windowsのプラットフォームに対応し、アプリを一度ビルドしエディターでテスト、デバイスに展開などを1つのシームレスなワークフローで行うことが可能  |
|非常に使いやすい <br> Extremely Easy to Use   |使い慣れたデータ型を返す単純なクラスで機械学習モデルを公開 <br> 「Predictor(予測子)」と呼ばれるもので前処理スクリプトやシェーダーなど記述する必要がない  |
|成長中のカテゴリ <br> Growing Catalog   |アプリケーションに重点を置いて設計されていて、開発者がアプリケーションで迅速に発見して展開されるように予測子カタログを維持 <br> [詳しくはこちらから](https://hub.natml.ai/)  |
|コンピュータービジョン <br> Computer Vision   |オブジェクト分類・検出、セマンティックセグメーション、スタイル検出などのモデルをサポート   |
|拡張現実 <br> Augmented Reality   |作業をMLアクセラレータに委任することでGPUを解放してアプリをスムーズにレンダリングすることができるので拡張現実に適している   |
|軽量パッケージ <br> Lightweight Package   |自己完結型のパッケージで配布されているので、外部依存関係やセットアップは不要   |

(*1) [Bare Metal Performance](https://docs.natml.ai/natml/#bare-metal-performance)より

https://docs.natml.ai/natml/#bare-metal-performance

>NatML is designed specifically around high-performance interactive applications. Features include:
・Universal Machine Learning. With NatML, you can drop TensorFlow Lite (.tflite), CoreML (.mlmodel), and ONNX (.onnx) models directly into your Unity project and run them.
・Bare Metal Performance. NatML takes advantage of hardware machine learning accelerators, like CoreML on iOS and macOS, NNAPI on Android, and DirectML on Windows. As a result, it is multiple times faster than Unity's own Barracuda engine.
・Cross Platform. NatML supports Android, iOS, macOS, and Windows alike. As a result, you can build your app once, test it in the Editor, and deploy it to the device all in one seamless workflow.
・Extremely Easy to Use. NatML exposes machine learning models with simple classes that return familiar data types. These are called "Predictors", and they handle all of the heavy lifting for you. No need to write pre-processing scripts or shaders, wrangle tensors, or anything of that sort.
・Growing Catalog. NatML is designed with a singular focus on applications. As such, we maintain a growing catalog of predictors that developers can quickly discover and deploy in their applications. Check out NatML Hub.
・Computer Vision. NatML supports models for object classification, object detection, semantic segmentation, style transfer, and so much more.
・Augmented Reality. NatML is particularly suited for augmented reality because it delegates work to ML accelerators, freeing up the GPU to render your app smoothly.
・Lightweight Package. NatML is distributed in a self-contained package, with no external dependencies and no setup necessary.

(*2) NatMLとBarracudaのパフォーマンス比較

https://github.com/natmlx/ML-Bench


# 注意点
NatMLではCoreML(.mlmodel), ONNX(.onnx), TensorFlow Lite(.tflite)モデルをサポートしていますが、各々プラットフォームの制限が付きます(*3)。
| モデル | 対応プラットフォーム |
|:-:|:-:|
|CoreML   |iOS, macOS   |
|ONNX   |Windows   |
|TensorFlow Lite   |Android   |

ただしクロスプラットフォームで使用したい場合はモデルを[NatML Hub](https://hub.natml.ai/explore)にアップロードすると別のプラットフォームでも使用できるようになります。

:::note
NatML Hubとは
MLモデルを管理またはデプロイするためのプラットフォームです。
:::

https://hub.natml.ai

(*3) [Fetching Models](https://docs.natml.ai/natml/workflows/models) より

https://docs.natml.ai/natml/workflows/models

>There are restrictions on what platforms support a given ML model format:
　・CoreML models can only be used on iOS and macOS.
　・ONNX models can only be used on Windows.
　・TensorFlow Lite models can only be used on Android.
To use your ML model in cross-platform apps, upload the model to NatML Hub.



# 動作環境
- MacOS 12.6.1
- Unity 2021.3.0f1
- NatML 1.0.9



# 導入
基本的に [NatML for Unity](https://docs.natml.ai/natml/) の [Getting Started](https://docs.natml.ai/natml/prelims/getting-started) の通りに進めて行けばすぐ導入できる。
ただUnityにあまり詳しくない or 導入から知りたい方向けに簡単に解説します。

https://docs.natml.ai/natml/

https://docs.natml.ai/natml/prelims/getting-started

1\. 空のプロジェクトを用意
2\. packagesフォルダ内にある **manifest.json** の "scopedRegistries" と "dependencies" に以下の内容を追記

```json:manifest.json
{
  "scopedRegistries": [
   {
    "name": "NatML",
    "url": "https://registry.npmjs.com",
    "scopes": ["ai.natml"]
   }
  ],
  "dependencies": {
    "ai.natml.natml": "1.0.19",
    ...
  }
}
```
空のプロジェクトから作った場合、以下のようになっていればUnity側にNatMLのpackageがインポートされます
```json:manifest.json
{
  "scopedRegistries": [
   {
    "name": "NatML",
    "url": "https://registry.npmjs.com",
    "scopes": ["ai.natml"]
   }
  ],
  "dependencies": {
    "com.unity.collab-proxy": "1.15.15",
    "com.unity.feature.development": "1.0.1",
    "com.unity.ide.rider": "3.0.13",
    "com.unity.ide.visualstudio": "2.0.14",
    "com.unity.ide.vscode": "1.2.5",
    "com.unity.test-framework": "1.1.31",
    "com.unity.textmeshpro": "3.0.6",
    "com.unity.timeline": "1.6.4",
    "com.unity.ugui": "1.0.0",
    "com.unity.visualscripting": "1.7.6",
    "com.unity.modules.ai": "1.0.0",
    "com.unity.modules.androidjni": "1.0.0",
    "com.unity.modules.animation": "1.0.0",
    "com.unity.modules.assetbundle": "1.0.0",
    "com.unity.modules.audio": "1.0.0",
    "com.unity.modules.cloth": "1.0.0",
    "com.unity.modules.director": "1.0.0",
    "com.unity.modules.imageconversion": "1.0.0",
    "com.unity.modules.imgui": "1.0.0",
    "com.unity.modules.jsonserialize": "1.0.0",
    "com.unity.modules.particlesystem": "1.0.0",
    "com.unity.modules.physics": "1.0.0",
    "com.unity.modules.physics2d": "1.0.0",
    "com.unity.modules.screencapture": "1.0.0",
    "com.unity.modules.terrain": "1.0.0",
    "com.unity.modules.terrainphysics": "1.0.0",
    "com.unity.modules.tilemap": "1.0.0",
    "com.unity.modules.ui": "1.0.0",
    "com.unity.modules.uielements": "1.0.0",
    "com.unity.modules.umbra": "1.0.0",
    "com.unity.modules.unityanalytics": "1.0.0",
    "com.unity.modules.unitywebrequest": "1.0.0",
    "com.unity.modules.unitywebrequestassetbundle": "1.0.0",
    "com.unity.modules.unitywebrequestaudio": "1.0.0",
    "com.unity.modules.unitywebrequesttexture": "1.0.0",
    "com.unity.modules.unitywebrequestwww": "1.0.0",
    "com.unity.modules.vehicles": "1.0.0",
    "com.unity.modules.video": "1.0.0",
    "com.unity.modules.vr": "1.0.0",
    "com.unity.modules.wind": "1.0.0",
    "com.unity.modules.xr": "1.0.0",
    "ai.natml.natml": "1.0.19"
  }
}

```
↓ インポート後のProject > Packages配下に「 **NatML** 」と「 **NatML Hub** 」があることを確認
![スクリーンショット 2022-12-08 12.09.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/d04ddc6d-d7d4-5a14-520a-395478281cd3.png)

これで最低限の動作環境が整いました。

NatMLでは注意点でも記載しましたが自前のモデルも動作させることが可能なのです。
ですがNatML側で用意されているモデルを使用したい場合は[NatML Hub](https://hub.natml.ai/explore)から導入方法を確認しつつ動かすことが可能となっています。
私もいくつか気になるモデルがあったので実際に使ってみたいと思います。
![スクリーンショット 2022-12-08 12.21.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/40423f07-c9bc-51c5-7f00-c80f532c069d.png)


# NatML Hubのモデルを動かしてみる
### 1\. UnityとNatML Hub連携
① NatML Hubのモデルを使用するにはアクセスキーを取得する必要があるので、NatML HubのHPからサインアップしアカウントを登録。
② アカウントの登録が完了したら「User > Profile」からAccess Keyが確認できるので手元に控えておく。
![アクセスキーの確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/16f88eaf-7d9a-9b1d-f829-e0b7d08053b8.png)
③ 「導入」で使用したUnityプロジェクトの「Project Settings > NatML」から②で控えていたAccess Keyを入力
![AccessKey.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/c57a1953-578e-a59a-0c1e-54331a6171bb.png)
これでNatML Hubのモデルを取得できるようになります。

### 2\. NatML Hubのモデルを動かす
今回はハンドトラッキングが可能になるVisionの「[blazepalm-landmark](https://hub.natml.ai/@natml/blazepalm-landmark)」のモデルを用意したUnityプロジェクトで動かしてみたいと思います。
![blazepalm-landmark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/8a631c02-0fae-0468-d7c9-4ef9a57514ed.png)

① 上記の画像の「**Get**」と書かれているところをクリックすると[Github](https://github.com/natml-hub/BlazePalm)ページに飛ぶので、基本的にそちらを参考にしながら導入することができます。

https://github.com/natml-hub/BlazePalm

:::note warn
注意
今回はGithub上での公開でしたがモデルによってはGithub以外のサイトの場合があります。
:::


② BlazePalmのREADME.mdを確認してみると対応しているUnityバージョンと環境、Predictor導入のリンクがあるので事前に確認しておくと良いかも。
![UsingPredictor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/d62dc05c-efe0-fa56-ee3d-e31d85104c5c.png)
今回はUnity 2021.3.0f1とmacOS 12環境で動作確認しているので特に問題なさそうです。

③ Predictor導入のリンクから**Installing BlazePalm**の項目を確認してみると、manifest.jsonに追加でdependenciesを追加するとのことで追記しておく

```json:manifest.json
{
  "dependencies": {
    "ai.natml.vision.blazepalm": "1.0.0"
  }
}
```

④ モデルを動かすためのScriptと描画用のScriptをサンプルを参考に追加する
```c# TestHandTracking.cs
using UnityEngine;
using NatML.Devices;
using NatML.Devices.Outputs;
using NatML.Vision;
using NatML.Visualizers;

namespace NatML
{
    [MLModelDataEmbed("@natml/blazepalm-detector"), MLModelDataEmbed("@natml/blazepalm-landmark")]
    public class TestHandTracking : MonoBehaviour
    {
        /// <summary>
        /// Visualizer
        /// </summary>
        [Header(@"UI"), SerializeField]
        private BlazePalmVisualizer visualizer;

        /// <summary>
        /// Pipeline
        /// </summary>
        private BlazePalmPipeline pipeline;

        /// <summary>
        /// CameraDevice
        /// </summary>
        private CameraDevice cameraDevice;

        /// <summary>
        /// CameraDeviceで写したものをTextureに変換したもの
        /// </summary>
        private TextureOutput previewTextureOutput;
        
        private void OnDisable() => pipeline?.Dispose();

        private async void Start()
        {
            // カメラが許可されているか
            var permissionStatus = await MediaDeviceQuery.RequestPermissions<CameraDevice>();

            // 許可されていない場合はreturn
            if (permissionStatus != PermissionStatus.Authorized) return;

            // カメラ取得
            var query = new MediaDeviceQuery(MediaDeviceCriteria.CameraDevice);
            cameraDevice = query.current as CameraDevice;

            // カメラで写したものをTextureに変換
            previewTextureOutput = new TextureOutput();
            cameraDevice.StartRunning(previewTextureOutput);
            var previewTexture = await previewTextureOutput;
            visualizer.image = previewTexture;

            // BlazePalm detectorとpredictorをNatML Hubから取得しPipelineの作成
            var detectorModelData = await MLModelData.FromHub("@natml/blazepalm-detector");
            var predictorModelData = await MLModelData.FromHub("@natml/blazepalm-landmark");
            pipeline = new BlazePalmPipeline(detectorModelData, predictorModelData);
        }

        private void Update()
        {
            // Pipelineが作成されているか
            if (pipeline == null) return;

            // カメラで写している情報から手を予測
            var hands = pipeline.Predict(previewTextureOutput.texture);
            
            // 予測した情報を描画
            visualizer.Render(hands);
        }
    }
}
```

```c# BlazePalmVisualizer.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UI.Extensions;
using NatML.Vision;

namespace NatML.Visualizers
{
    [RequireComponent(typeof(RawImage), typeof(AspectRatioFitter))]
    public sealed class BlazePalmVisualizer : MonoBehaviour
    {
        [SerializeField]
        private Image keypoint;

        [SerializeField]
        private UILineRenderer bones;

        private RawImage rawImage;
        private AspectRatioFitter aspectFitter;
        private readonly List<GameObject> currentHands = new List<GameObject>();

        public Texture2D image
        {
            get => rawImage.texture as Texture2D;
            set
            {
                rawImage.texture = value;
                aspectFitter.aspectRatio = (float)value.width / value.height;
            }
        }

        void Awake()
        {
            rawImage = GetComponent<RawImage>();
            aspectFitter = GetComponent<AspectRatioFitter>();
        }

        /// <summary>
        /// KeyPointの追加
        /// </summary>
        private void AddKeypoint(Vector2 point)
        {
            // KeyPointの生成
            var prefab = Instantiate(keypoint, transform);
            prefab.gameObject.SetActive(true);
            // pointの座標にKeyPointを移動
            var prefabTransform = prefab.transform as RectTransform;
            var imageTransform = rawImage.transform as RectTransform;
            prefabTransform.anchorMin = 0.5f * Vector2.one;
            prefabTransform.anchorMax = 0.5f * Vector2.one;
            prefabTransform.pivot = 0.5f * Vector2.one;
            prefabTransform.anchoredPosition = Rect.NormalizedToPoint(imageTransform.rect, point);
            currentHands.Add(prefab.gameObject);
        }

        /// <summary>
        /// KeyPointとboneの描画
        /// </summary>
        public void Render(params BlazePalmPredictor.Hand[] hands)
        {
            // 現在のKeyPoint削除
            foreach (var t in currentHands)
            {
                Destroy(t.gameObject);
            }

            currentHands.Clear();

            // 描画
            var segments = new List<Vector2[]>();
            foreach (var hand in hands)
            {
                // Keypoints
                foreach (var p in hand.keypoints)
                {
                    AddKeypoint((Vector2)p);
                }

                // Bones
                segments.AddRange(new List<Vector3[]>
                {
                    new[]
                    {
                        hand.keypoints.wrist, hand.keypoints.thumbCMC, hand.keypoints.thumbMCP, hand.keypoints.thumbIP,
                        hand.keypoints.thumbTip
                    },
                    new[]
                    {
                        hand.keypoints.wrist, hand.keypoints.indexMCP, hand.keypoints.indexPIP, hand.keypoints.indexDIP,
                        hand.keypoints.indexTip
                    },
                    new[]
                    {
                        hand.keypoints.middleMCP, hand.keypoints.middlePIP, hand.keypoints.middleDIP,
                        hand.keypoints.middleTip
                    },
                    new[]
                    {
                        hand.keypoints.ringMCP, hand.keypoints.ringPIP, hand.keypoints.ringDIP, hand.keypoints.ringTip
                    },
                    new[]
                    {
                        hand.keypoints.wrist, hand.keypoints.pinkyMCP, hand.keypoints.pinkyPIP, hand.keypoints.pinkyDIP,
                        hand.keypoints.pinkyTip
                    },
                    new[]
                    {
                        hand.keypoints.indexMCP, hand.keypoints.middleMCP, hand.keypoints.ringMCP,
                        hand.keypoints.pinkyMCP
                    },
                }.Select(points => points.Select(p => (Vector2)p).ToArray()));
            }

            bones.Points = null;
            bones.Segments = segments;
        }
    }
}
```

⑤ Hierarchyにモデルを動かすためのGameObjectとKeyPointとBoneを描画するCanvasを追加し④で作成したScriptを追加する
TestHandTracking.csは空のGamgeObjectにAddComponentしました。
![モデルを動かすためのGameObject.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/8489e5f9-85eb-7581-893f-a4ec329ef95a.png)
BlazePalmVisualizer.csはCanvas内にPreviewというRawImageを追加しAddComponentしました。
BoneはLineRendererで、KeyPointは生成して使うので非表示にして作成しています。
![Canvas](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/4d2aaae4-480f-04f3-86f5-f487796a61e7.png)

⑥ あとはもうUnityを起動するだけで無事に動作することを確認することができます
![HandTracking_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/b4b8ed56-0492-68fe-0fd9-653e30f5a113.png)

ピースも難なくできますね。凄い...。
![HandTracking_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643763/15ae3d16-4838-5704-980c-f84d47f4b926.png)

# まとめ
今回はNatMLの導入とNatML HubにあるMLモデルを動かすところまでやってみました。
導入やMLモデルを動かすことについては特にこれといって難しいことはなく、誰でも簡単に実装することができるということがわかり、今後様々なコンテンツ開発が出来そうな感じがするので今後の発展が楽しみですね。
（自前で0からMLモデルを作成するところが一番大変...）