3Dの姿勢推定のOnnxのモデルでUnityちゃんを動かしてみた

##はじめに
[以前の記事（iOSに3D姿勢推定を実装してみた）](https://qiita.com/yukihiko_a/items/727a03a35c9a6b14e038)では、pytorchで学習したモデルを、pytorch → Onnx → CoreMLと変換してiOSで3Dの姿勢推定が動くようにしていました。今回はOnnxに変換したモデルをUnityで動くようにして、動画やカメラから取得した画像から姿勢を推定した結果からUnityちゃんをリアルタイムに動かすようにしました。

 <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">3Dの姿勢推定でUnityちゃんを動かしてみました。動画を再生しながらのRealTimeでの推定です。pytorchのモデルをonnxにしてUnityで実装しています。顔の向きがまだ適当ですがそれっぽく動いているようです。 <a href="https://twitter.com/hashtag/poseestimation?src=hash&amp;ref_src=twsrc%5Etfw">#poseestimation</a><br><br>動画：ミソジサラリーマン様(<a href="https://twitter.com/keriwaza?ref_src=twsrc%5Etfw">@keriwaza</a>) <a href="https://t.co/pmjVCRXL1r">pic.twitter.com/pmjVCRXL1r</a></p>&mdash; Yukihiko Aoyagi (@yukihiko_a) <a href="https://twitter.com/yukihiko_a/status/1122754890982248448?ref_src=twsrc%5Etfw">2019年4月29日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">カメラ機能を追加しました。カメラ一台でリアルタイムにここまで認識できます。ノートPCのカメラなので画質が悪いですが、PC一台だけでVTuberも夢じゃないですね。背景や距離はまだ選びますけど。このままVtuber目指そかな。。。ソースはGitにあげておきます。 <a href="https://twitter.com/hashtag/poseestimation?src=hash&amp;ref_src=twsrc%5Etfw">#poseestimation</a> <a href="https://t.co/4b2ZRI6eQ1">pic.twitter.com/4b2ZRI6eQ1</a></p>&mdash; Yukihiko Aoyagi (@yukihiko_a) <a href="https://twitter.com/yukihiko_a/status/1124519787340517382?ref_src=twsrc%5Etfw">2019年5月4日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 目的
UnityでのOnnxの読み込み・実行はOpenCVSharpつまりは、OpenCVのモデルの読み込みの機能を使うことにしました。WindowsMLやML.Net、Onnx Runtimeなど試してみましたが、現状ではOpenCVSharpがUnityでも.Net環境でもほとんど同様に扱える為一番簡単に扱えると思います（OpenCVなので画像は一旦Matの形式にしないといけないため、Unityで使用する場合はひと手間増えます）。簡単に扱えますがあまりまとまった資料が見当たらなかったのでOnnxの実装まわりを簡単ですがまとめたいと思います。
OpenCVSharpはNugetから簡単に入れれるのでここではインストール等の説明は飛ばします。

## Onnx
Onnxに関するコードを下記に抜粋します。

```c#:ThreeDPoseScript.cs

    // Properties for onnx and estimation
    private Net Onnx;
    private Mat[] outputs = new Mat[4];

    private const int inputImageSize = 224;
    private const int JointNum = 24;
    private const int HeatMapCol = 14;
    private const int HeatMapCol_Squared = 14 * 14;
    private const int HeatMapCol_Cube = 14 * 14 * 14;

    char[] heatMap2Dbuf = new char[JointNum * HeatMapCol_Squared * 4];
    float[] heatMap2D = new float[JointNum * HeatMapCol_Squared];
    char[] offset2Dbuf = new char[JointNum * HeatMapCol_Squared * 2 * 4];
    float[] offset2D = new float[JointNum * HeatMapCol_Squared * 2];

    char[] heatMap3Dbuf = new char[JointNum * HeatMapCol_Cube * 4];
    float[] heatMap3D = new float[JointNum * HeatMapCol_Cube];
    char[] offset3Dbuf = new char[JointNum * HeatMapCol_Cube * 3 * 4];
    float[] offset3D = new float[JointNum * HeatMapCol_Cube * 3];

    public void InitONNX()
    {
        Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");
        for (var i = 0; i < 4; i++) outputs[i] = new Mat();
    }

    /// <summary>
    /// Predict
    /// </summary>
    /// <param name="img"></param>
    public void Predict(Mat img)
    {
        var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);
        Onnx.SetInput(blob);
        Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });

        // copy 2D outputs
        Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);
        Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);
        Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);
        Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);
        for (var j = 0; j < JointNum; j++)
        {
            var maxXIndex = 0;
            var maxYIndex = 0;
            jointPoints[j].score2D = 0.0f;
            for (var y = 0; y < HeatMapCol; y++)
            {
                for (var x = 0; x < HeatMapCol; x++)
                {
                    var l = new List<int>();
                    var v = heatMap2D[(HeatMapCol_Squared) * j + HeatMapCol * y + x];

                    if (v > jointPoints[j].score2D)
                    {
                        jointPoints[j].score2D = v;
                        maxXIndex = x;
                        maxYIndex = y;
                    }
                }

            }

            jointPoints[j].Pos2D.x = (offset2D[HeatMapCol_Squared * j + HeatMapCol * maxYIndex + maxXIndex] + maxXIndex / (float)HeatMapCol) * (float)inputImageSize;
            jointPoints[j].Pos2D.y = (offset2D[HeatMapCol_Squared * (j + JointNum) + HeatMapCol * maxYIndex + maxXIndex] + maxYIndex / (float)HeatMapCol) * (float)inputImageSize;
        }

        // copy 3D outputs
        Marshal.Copy(outputs[0].Data, heatMap3Dbuf, 0, heatMap3Dbuf.Length);
        Buffer.BlockCopy(heatMap3Dbuf, 0, heatMap3D, 0, heatMap3Dbuf.Length);
        Marshal.Copy(outputs[1].Data, offset3Dbuf, 0, offset3Dbuf.Length);
        Buffer.BlockCopy(offset3Dbuf, 0, offset3D, 0, offset3Dbuf.Length);
        for (var j = 0; j < JointNum; j++)
        {
            var maxXIndex = 0;
            var maxYIndex = 0;
            var maxZIndex = 0;
            jointPoints[j].score3D = 0.0f;
            for (var z = 0; z < HeatMapCol; z++)
            {
                for (var y = 0; y < HeatMapCol; y++)
                {
                    for (var x = 0; x < HeatMapCol; x++)
                    {
                        float v = heatMap3D[HeatMapCol_Cube * j + HeatMapCol_Squared * z + HeatMapCol * y + x];
                        if (v > jointPoints[j].score3D)
                        {
                            jointPoints[j].score3D = v;
                            maxXIndex = x;
                            maxYIndex = y;
                            maxZIndex = z;
                        }
                    }
                }
            }

            jointPoints[j].Now3D.x = (offset3D[HeatMapCol_Cube * j + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxXIndex / (float)HeatMapCol) * (float)inputImageSize;
            jointPoints[j].Now3D.y = (float)inputImageSize - (offset3D[HeatMapCol_Cube * (j + JointNum) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxYIndex / (float)HeatMapCol) * (float)inputImageSize;
            jointPoints[j].Now3D.z = (offset3D[HeatMapCol_Cube * (j + JointNum * 2) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)(maxZIndex - 7) / (float)HeatMapCol) * (float)inputImageSize;
        }
    }
```


まず、今回のモデルの入力は224x224のサイズの画像です。Outputの関節数は24個にしていてHeatmapは14x14としています。2D用のHeatmapは24x14x14、3D用のHeadmapは24x14x14x14となります。これにHeatmapからの座標のオフセット値として2Dは(x,y)の2x24x14x14、3Dは(x,y,z)の3x24x14x14x14のOutputとなります。

![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374813/8d0b2253-41d5-6e75-2292-d88da587c6a8.png)


```c#:ThreeDPoseScript.cs
    public void InitONNX()
    {
        Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");
        for (var i = 0; i < 4; i++) outputs[i] = new Mat();
    }
```

まず、InitONNX()でOnnxファイルを読み込みます。OpenCVのアウトプットはMatオブジェクトでかえってくるので4つの配列を用意しておきます。

```c#:ThreeDPoseScript.cs
    public void Predict(Mat img)
    {
        var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);
        Onnx.SetInput(blob);
        Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });

        // copy 2D outputs
        Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);
        Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);
        Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);
        Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);

```

Predictのメソッドの引数のMatオブジェクトはふつうのCV_8UC3のMatのイメージデータでです。これをOnnxに渡すにはBlobなMatに変換する必要があります。パラメターに今一つ自身がないですが、BlobFromImageでBlobに変換してInputに渡すだけです。Outputは"369", "373"が3D用のアウトプット、"361", "365"が2D用のアウトプットとなっています。Matのオブジェクトのままだと扱いにくいのでこれをfloatの配列に変換しています。あとはHeatmapのMaxを探すために関節数とHeatmapサイズ分ぐるぐる回しているだけです。3Dはかなり大きいループなのでもう少し工夫した方がよいと思いますが、今のところ十分な速さで動いているのでそのままにしています。

上記の処理でOnnxのモデルから結果が得られるので、あとは得られた座標値からUnityちゃんを動かすだけです。このあたりは下記の参考にあげさせていただいたリンクが詳しいのでそちらを参照ください。

このUnityのコードは、[Github（ThreeDPoseUnitySample）](https://github.com/yukihiko/ThreeDPoseUnitySample)にあげていますので詳細はそちらを参照ください。

## 参考

[【Unity】OpenPose ==> 3d-pose-baseline-vmd で出力した関節座標値を回転に変換して骨格アニメーションさせる](https://qiita.com/romaroma/items/ffbdae4ecfc4c8ff31cd)
各関節の動かし方の参考にさせていただきました。ほとんどそのまま使わせて頂いたいています。

[t-takasaka/VNect-VRM-Unity](https://github.com/t-takasaka/VNect-VRM-Unity)
　Unityでのカメラや動画の使い方を参考にさせていただきました。

[ミソジサラリーマンMisozi-Salaryman-game style action-](https://www.youtube.com/user/tanahiro814)
ミソジサラリーマン様のアクションの動画。今回もお世話になっております。