Azure Kinect DKで火属性マジシャンに転職する

初投稿です！

# はじめに
Azure Kinect DKを会社で購入してもらったので早速遊ん...技術検証してみます。
到達目標は火属性マジシャンになること。

# Azure Kinect DKとは
https://azure.microsoft.com/ja-jp/services/kinect-dk/
ざっくりと

* カメラだけでボディトラッキングできる（センサー取り付け不要）
* 深度センサーがついてる
* 価格ハードル低め（公式で5万円弱）

# 環境
* Windows10 Home
* Unity2019.2
* Azure Kinect SDK v1.4.1
* Azure Kinect Body Tracking SDK v1.0.1

# 準備
とてもわかりやすい記事がありましたので参考にさせていただきました。
[Azure KinectのBodyTracking公式サンプルをUnityで動かしてみる](https://qiita.com/drumath2237/items/b92399e71db1074814e3)
公式のボディトラッキングサンプルが動けば準備完了です。

# Hierarchy
新規にシーンを作成してオブジェクトを配置していきます。

### Main Camera
デフォルトで配置されているMain CameraはPositionを弄っています。
後述するカメラ映像を投影するスクリーンを適当な距離から映すためです。
![image_2020_10_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/3f8840cd-eac5-2fb4-b8ed-1eae2dd53289.png)

### FireBall Camera
火の玉を映すためのカメラです。
常にカメラ映像のスクリーンより手前に表示したいのでDepthを1にした上で、専用のFireBallレイヤーを作成してCulling Maskに指定しておきます。
![image_2020_10_29 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/5f21bf40-18a8-761a-a8c2-f5da98eb83b1.png)

### Screen
カメラ映像を投影するためのスクリーンです。
Create > 3D Object > Cubeから作成しておきます。
後でカメラからの映像サイズに合わせてサイズを変更するのでScaleは0で。
後述するスクリプト「Kinect Handler」をAdd Componentします。
![image_2020_10_29 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/12ba4daa-b12f-94d4-b5cc-5cc4425f83ac.png)

### FireBallBase
火の玉エフェクトの位置を調整するためだけの空GameObjectです。
前述のScreenのKinectHandler > Fire Ball 1,2にアタッチします。
![image_2020_10_29 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/6997a481-0813-8c43-a099-ce3d58d08d56.png)

### FireBall
火の玉エフェクトです。
両手用に二つ用意します。
今回は無料アセットの[Unity Particle Pack](https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-particle-pack-127325?locale=ja-JP)から拝借しました。
![image_2020_10_29 (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/2e8f1187-cb65-698e-a488-094febcb3629.png)

# スクリプト
最後にスクリプトを実装していきます。
書くのはKinectHandler.csのみ。
お手軽ですね。
同様の事例やサンプルコードがなかったので公式のSDKドキュメントを参考に書いてみました。
[Azure Kinect Sensor SDK](https://microsoft.github.io/Azure-Kinect-Sensor-SDK/master/namespace_microsoft_1_1_azure_1_1_kinect_1_1_sensor.html)
[Azure Kinect Body Tracking SDK](https://microsoft.github.io/Azure-Kinect-Body-Tracking/release/1.x.x/namespace_microsoft_1_1_azure_1_1_kinect_1_1_body_tracking.html)

```C#
using Microsoft.Azure.Kinect.BodyTracking;
using Microsoft.Azure.Kinect.Sensor;
using System.Threading.Tasks;
using UnityEngine;

public class KinectHandler : MonoBehaviour
{
    private Device kinect;
    private Transformation transformation;
    private Capture cameraCapture;

    private Texture2D texture;

    private Tracker tracker;

    [SerializeField]
    GameObject fireBall1; // 右手に出す火の玉

    [SerializeField]
    GameObject fireBall2; // 左手に出す火の玉

    void Start()
    {
        // Azure Kinectの初期設定および起動
        this.kinect = Device.Open(0);
        this.kinect.StartCameras(new DeviceConfiguration {
            ColorFormat = ImageFormat.ColorBGRA32,
            ColorResolution = ColorResolution.R720p,
            DepthMode = DepthMode.NFOV_2x2Binned,
            SynchronizedImagesOnly = true,
            CameraFPS = FPS.FPS30
        });

        this.transformation = kinect.GetCalibration().CreateTransformation();
        this.cameraCapture = kinect.GetCapture();

        // カメラ映像を描画するTexture2Dの初期設定
        // カメラからの情報を元にサイズを決定する
        var width = kinect.GetCalibration().ColorCameraCalibration.ResolutionWidth;
        var height = kinect.GetCalibration().ColorCameraCalibration.ResolutionHeight;
        this.transform.localScale = new Vector3(width / 100, height / 100, 0.1f);
        
        this.texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
        this.texture.Apply();
        this.GetComponent<Renderer>().material.mainTexture = texture;

        // BodyTrackingするためのトラッカーを初期化
        this.tracker = Tracker.Create(kinect.GetCalibration(), TrackerConfiguration.Default);

        // Kinectからの情報取得はタスクで回して繰り返し取得します
        Task t = KinectLoop();
    }

    private void OnDestroy()
    {
        // アプリ終了時にカメラを終了させる
        this.kinect.StopCameras();
    }

    private async Task KinectLoop()
    {
        while(true)
        {
            using (Capture capture = await Task.Run(() => this.kinect.GetCapture()).ConfigureAwait(true))
            {
                // 画像処理
                // 取得したピクセル毎の色情報をTexture2Dに書き込む
                var colorImage = capture.Color;
                var colorArray = colorImage.GetPixels<BGRA>().ToArray();
                var colors = new Color32[colorArray.Length];

                for (var i = 0; i < colorArray.Length; i++)
                {
                    colors[i].b = colorArray[i].B;
                    colors[i].g = colorArray[i].G;
                    colors[i].r = colorArray[i].R;
                    colors[i].a = 255;
                }

                this.texture.SetPixels32(colors);
                this.texture.Apply();

                // BodyTraking情報を取得して火の玉オブジェクトのPositionに指定します
                this.tracker.EnqueueCapture(capture);
                var frame = tracker.PopResult();
                if (frame.NumberOfBodies > 0) // この判定がないと撮影範囲から人がいなくなった場合に後の処理で止まる
                {
                    this.SetMarkPos(this.fireBall1, JointId.HandRight, frame); // 右手
                    this.SetMarkPos(this.fireBall2, JointId.HandLeft, frame); // 左手
                    // ↑指定するJointIdを変えれば腰や頭に火の玉を出すことも可能
                }
            }
        }
    }

    private void SetMarkPos(GameObject fireball, JointId jointId, Frame frame)
    {
        // 指定の関節情報から位置を取得してGameObjectの位置指定に
        var joint = frame.GetBodySkeleton(0).GetJoint(jointId);
        var offset = 50; // 取得した通りの値だと動きすぎるので少し調整してやります
        var pos = new Vector3(joint.Position.X / -offset, joint.Position.Y / -offset, joint.Position.Z / offset);
        fireball.transform.localPosition = pos;
    }
}
```

# 完成！！
UnityEditorで実行状態にしてKinectのカメラ範囲に入ると自動でトラッキングしてくれます。
無事に火属性マジシャンに転職することができました。
![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/0fd00b38-15be-b508-7861-e1f5bb7d85ab.gif)

# 蛇足
JointIdを腰（JointId.Pelvis）にしてやれば特定部位のオート追尾モザイクもできちゃいます。
夢が広がりますね！
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478145/e758a7fa-ccee-d499-136c-2b5381039de3.gif)