UnityとAIで実現する！MoveNet + Sentisでモーショントラッキング その2

その1はこちらから

https://qiita.com/tome_Horizon/items/b5b2f473460b4f6f9d84

その1ではSentisやMoveNetについて長々と説明したところで、いよいよUnityプロジェクトの準備に取り掛かりましょう。ここからは手を動かす作業がメインになります。

# Unityプロジェクトの準備
1. **Unity Hubから新規プロジェクトを作成**
Unity Hubを開き、「新規プロジェクト」を選択します。今回は3Dアプリケーションとして開発を進めるので、「3D Core」テンプレートを選びました
2. **Sentisパッケージのインポート**
プロジェクトが開き、Unityエディターが表示されたら、Sentisパッケージをインポートします。
Unityエディター上部のメニューから `Window` > `Package Manager`を選択します。
Package Managerウィンドウが開いたら、左上のドロップダウンを`Unity Registry`に変更します。
検索バーに「Sentis」と入力し、「Unity Sentis」を見つけます。
「Install」ボタンをクリックして、パッケージをプロジェクトに導入します。
3. **Assetsフォルダへのモデルの配置**
作成したMoveNetのONNXファイルを、Unityプロジェクトの Assetsフォルダ内の任意の場所にドラッグ＆ドロップしてください。
UnityはONNXファイルを自動的に認識し、インポート処理を行います。インポートが完了すると、モデルファイルがUnityのアセットとして利用可能になります。

:::note warn
付属のサンプルがありますが、警告が大量に出るものとまともに動かないものがあります。
私の環境が悪い可能性(いわゆるおま環)もありますが、おそらくいくつかのサンプルは動かないと思います。
:::
:::note alert
Sentisのパッケージは`unsafe`修飾子が使用されているようです。`allow unsafe code`を`true`にしないとおそらく動きません。デフォルトではされていないので許可してください。私はたまたまOpenCVを使おうとしていたプロジェクトを使っていたので既にやっていました。
**[やり方はこのページ一番下を参照。](#unsafe-codeを許可する)**
:::

# Webカメラの準備と処理
**WebカメラのデータをUnityで使用する**
Unityでは`WebCamTexture`クラスを使用することで、簡単にWebカメラの映像を取得できます。以下のようなC#スクリプトを作成し、カメラからの映像を取得・表示する準備をします。

<details>
<summary>
Webカメラの初期化コード
</summary>

```csharp:PoseEstimater.cs
[SerializeField] string targetWebcamName = "";
[SerializeField] int webcamWidth = 1280;
[SerializeField] int webcamHeight = 720;
[SerializeField] int webcamFPS = 30;
IEnumerator InitializeWebcamAndSentis()
{
    yield return Application.RequestUserAuthorization(UserAuthorization.WebCam);

    WebCamDevice[] devices = WebCamTexture.devices;
    WebCamDevice selectedDevice = devices[0];
    foreach (WebCamDevice device in devices)
    {
        if (device.name == targetWebcamName)
        {
            selectedDevice = device;
            break;
        }
    }
    webcamTexture = new WebCamTexture(selectedDevice.name, webcamWidth, webcamHeight, webcamFPS);
    uiRawImage.texture = webcamTexture;
    webcamTexture.Play();

    yield return new WaitUntil(() => webcamTexture.didUpdateThisFrame && webcamTexture.width > 16);
    isCameraInitialized = true;

    preprocessedRenderTexture = new RenderTexture(movenetInputSize.x, movenetInputSize.y, 0, RenderTextureFormat.ARGB32);
    preprocessedRenderTexture.Create();
    preprocessedTexture2D = new Texture2D(movenetInputSize.x, movenetInputSize.y, TextureFormat.RGB24, false);

}
```
</details>

**Sentisの推論エンジンの初期化**
Sentisでモデルを実行するには、まずモデルをロードし、推論エンジン（Worker）を初期化する必要があります。実際にはWebカメラの初期化の直後にやるのがいいでしょう。
この時、MoveNetを実行する際のバックエンドタイプを選択できます。GPUが乗っているPCなので`GPUCompute`を使用しました。
<details>
<summary>
SentisとMoveNetの初期化コード
</summary>

```csharp
[SerializeField] ModelAsset movenetModelAsset;
[SerializeField] BackendType backendType = BackendType.GPUCompute;
Model runtimeModel;
Worker worker;
IEnumerator InitializeWebcamAndSentis()
{
    // ==============
    // Webカメラ初期化
    // ==============
    // Webカメラ初期化の続きから

    runtimeModel = ModelLoader.Load(movenetModelAsset);
    if (runtimeModel == null)
    {
        enabled = false;
        yield break;
    }
    
    worker = new Worker(runtimeModel, backendType);
    if (worker == null)
    {
        enabled = false;
        yield break;
    }
}
```
</details>

**カメラデータの変換**
Render TextureやWebCamTextureから取得した映像は、そのままではSentisの入力テンソルとしては使えません。MoveNet thunderは256x256を入力とするので、リサイズしてからが期待する入力形式`Tensor<T>`に変換しなくてはなりません。
また、ONNXアセットをクリックしてヒエラルキーで見てみると、Input(1, 256, 256, 3)といった文言が書かれていると思います。
**これにのっとった順番でTensorも作成しなくてはなりません。**
<details>
<summary>
カメラデータの変換コード
</summary>

```csharp
void Update()
{
    // Sentisへの入力画像準備
    Graphics.Blit(webcamTexture, preprocessedRenderTexture);
    RenderTexture.active = preprocessedRenderTexture;
    preprocessedTexture2D.ReadPixels(new Rect(0, 0, movenetInputSize.x, movenetInputSize.y), 0, 0);
    preprocessedTexture2D.Apply();
    RenderTexture.active = null;
    
    // モデルのInputに合った形式でTensorを作成 今回(1, 256, 256, 3)
    Tensor<float> inputTensor = new Tensor<float>(new TensorShape(1, movenetInputSize.y, movenetInputSize.x, 3));
    var transform = new TextureTransform();
    transform.SetTensorLayout(TensorLayout.NHWC);
    transform.SetDimensions(preprocessedTexture2D.width, preprocessedTexture2D.height, 3);
    TextureConverter.ToTensor(preprocessedTexture2D, inputTensor, transform);
}
```
</details>

# SentisによるMoveNetモデルの実行

**MoveNetへの入力と推定**
前のセクションで`Tensor<T>`に変換した画像データをSentisの`Worker`に渡し推定を実行します。

<details>
<summary>
MoveNetへの入力と推定コード
</summary>

```csharp
void Update()
{
    worker.Schedule(inputTensor);
}
```
</details>

# MoveNet推定結果の解析とキーポイントの取得
推論が完了したら、`Worker`から結果のテンソルを取得します。MoveNetモデルは通常、検出されたキーポイントの座標と信頼度スコアを含むテンソルを出力します。
この`outputTensor`の中には、MoveNetが推論した身体のキーポイントの座標データと、それぞれのキーポイントの検出に対する信頼度スコアが含まれています。

<details>
<summary>
キーポイントの取得コード
</summary>

```csharp
// "movenet_inference_layer"は変換コードで指定した名前
Tensor<float> outputTensor = worker.PeekOutput("movenet_inference_layer") as Tensor<float>;

```
</details>

# 検出したキーポイントの可視化
キーポイントの座標データと信頼度スコアが取得できました。いよいよ、これらのデータをUnityの画面上に描画し、モーショントラッキングの結果を「見える化」する段階です。
ここでは、主に以下の2つの方法でキーポイントを可視化します。
1. **キーポイントの描画**
検出された各キーポイント（鼻、目、肩など）を小さな点やSphereで表示します。
1. **ボーンの描画**
キーポイント同士を線で結び、人間の骨格を表現します。これにより、体の動きがより直感的に理解できるようになります。

<details>
<summary>
キーポイントの可視化コード
</summary>

```csharp
private List<GameObject> keypointObjects = new List<GameObject>();
int numKeypoints = outputTensorCPU.shape[2];
Vector2[] currentKeypointPositions = new Vector2[numKeypoints];
bool[] keypointVisible = new bool[numKeypoints];
Vector2 rawImageSize = uiRawImage.rectTransform.rect.size;

for (int i = 0; i < numKeypoints; i++)
{
    float y = outputTensorCPU[0, 0, i, 0]; // normalized y (0-1)
    float x = outputTensorCPU[0, 0, i, 1]; // normalized x (0-1)
    float confidence = outputTensorCPU[0, 0, i, 2]; // confidence (0-1)
    if (confidence >= confidenceThreshold)
    {
        keypointObjects[i].SetActive(true);
        keypointVisible[i] = true;

        // キーポイントのX座標を反転させるかどうかの設定を適用
        float displayX = flipKeypointsX ? (1f - x) * rawImageSize.x : x * rawImageSize.x;

        // キーポイントのY座標を反転させるかどうかの設定を適用
        float displayY = flipKeypointsY ? (1f - y) * rawImageSize.y : y * rawImageSize.y;

        Vector2 localPos = new Vector2(
            displayX - (rawImageSize.x / 2f),
            displayY - (rawImageSize.y / 2f)
        );

        keypointObjects[i].GetComponent<RectTransform>().anchoredPosition = localPos;
        currentKeypointPositions[i] = localPos;
    }
    else
    {
        keypointObjects[i].SetActive(false);
        keypointVisible[i] = false;
    }
}

for (int i = 0; i < bones.GetLength(0); i++)
{
    int kp1Index = bones[i, 0];
    int kp2Index = bones[i, 1];

    if (keypointVisible[kp1Index] && keypointVisible[kp2Index])
    {
        GameObject boneObj = boneObjects[i];
        RectTransform rt = boneObj.GetComponent<RectTransform>();

        Vector2 start = currentKeypointPositions[kp1Index];
        Vector2 end = currentKeypointPositions[kp2Index];
        Vector2 diff = end - start;
        float length = diff.magnitude;
        float angle = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;

        rt.anchoredPosition = start + diff * 0.5f;
        rt.sizeDelta = new Vector2(length, boneWidth);
        rt.localRotation = Quaternion.Euler(0, 0, angle);

        boneObj.SetActive(true);
    }
    else
    {
        boneObjects[i].SetActive(false);
    }
}

```
</details>

# おわりに
これにて"その2"はおしまいです。
次回はこれを使った応用などの記事を書く予定ですが、この記事を書いている段階ではまだ完成していないので、"その3"が出るのはいつになるのやら...
興味がありましたら、いいねやストック、フォローするなりして気長にお待ちください。
ここにリンクを張り付けるのでストックしていただけたら通知が飛ぶかもしれません。
ではまた

# unsafe codeを許可する
1. `Edit` > `Project Settings...`
1. 開いたウィンドウの左側で`Player`を選択
1. `Other Settings`
1. `Configuration` > `Allow unsafe code`のチェックボックスをオン
1. 再コンパイル