初心者が Unity で始める AzureKinect

3D Sensor Advent Calendar 2019 の 10 日目の記事です。
## 概要
3D センサー歴 3 か月程度の初心者ですが、もう一度、 AzureKinect で頑張ってみます。
前回は、AzureKinect をVisual Studio で動かしてみました。今回の目標としては、初心者レベルでUnity の使用と点群表示、画面を移動するところまで解説してみたいと思います。

## 環境
- windows 10 pro
- Unity 2019.2.12
- Microsoft .NET Framework Version 4.8.03752
- Azure Kinect Sensor SDK 1.3.0

環境としては、Unityと前回の Visual Studio、それと Azure Kinect Sensor SDK 3.0 がインストールされている状態で開始します。

## 環境について

今回もできるだけ初心者（私）向けに最小限でやりたいのですが、Unity だと、以下のような手順を踏みます
1. Azure Kinect のソースからコンパイル、C/C++ SDK 作成
2. Azure Kinect の SDK -> SDK を呼び出す C# wrapper 作成
3. Unity から呼び出し

となります。準備するだけでも手間がかかる・・・でも、前回の知識で省略できるところがあります。

### 1.Azure Kinect のソースからコンパイル
最終的にはSensor SDK の k4a.dll / depthengine_2_0.dll というコンパイル済のモジュールが Unity では必要になります。しかし、これは、Azure Kinect Sensor SDK をサイトからダウンロードしていれば以下の場所にありますので、これを使うことにします。
これを Unity でネイティブプラグイン、今回なら、AzureKinect Sensor SDKや、C# Wrapper といった、別言語でコンパイルし多様なモジュールを読み込むための特殊フォルダ「Plugins」に格納すれば、読み込めるようになります。SDKはWindowsの64bit版だと
以下のデフォルトの場所に配置されます。

```C:\Program Files\Azure Kinect SDK v1.3.0\sdk\windows-desktop\amd64\release\bin```

![コメント 2019-12-06 080310.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/039a7994-6769-e6a6-cc1c-53a4b5286b5a.png)


### 2.Azure Kinect の SDK -> SDK を呼び出す C# wrapper 作成
これは、本来なら C# wrapper をコンパイルして Microsoft.Azure.Kinect.Sensor モジュールを配置する必要があります。
実はこれもまた、 Sensor SDK 内にあるのですが・・・

```C:\Program Files\Azure Kinect SDK v1.3.0\sdk\netstandard2.0\release```

![コメント 2019-12-06 080545.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/d5f394aa-d25e-82cb-4b5c-3f94e51f7d40.png)


最小限のサンプルという意味では、AzureKinect の Azure Kinect SensorSDK と、SDK C#Wrapper、これだけあれば済むかと思いきや、とりあえずやってみると・・・

![コメント 2019-12-06 074457.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/46f9d25e-d386-168d-43fe-dbcbf573a5ee.png)

### おや・・・？

![コメント 2019-12-06 074655.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/5214507d-cf4c-0209-bf08-cdd5b2369237.png)

これは、System.Memoryというモジュールが足りない、と言われているようですね。それがないのでC# wrapper 本体がloadできないようです。（他のエラーは別原因でした）

```
The type 'Memory<>' is defined in an assembly that is not referenced. You must add a reference to assembly 
'System.Memory, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'.
```

どうやら、C# SDK 内のモジュールをPlugins に配置するだけでは動かないようです。これは 3DSensor Advent Calendar 二日目に書いた、「NuGet」を使用したいと思います（visual studioと同じ資源が使用できるのは非常に便利ですね。作成者に感謝です）。
それでは、以下に手順を書きます

1.先に「NuGetForUnity」をWebからダウンロードします
https://github.com/GlitchEnzo/NuGetForUnity/releases
NuGetForUnity.2.0.0.unitypackage をダウンロード

2.Unityを起動します。


3.NuGetForUnity.2.0.0.unitypackage をUnity のAssetsフォルダにドラッグしインストールします。

![コメント 2019-12-06 084908.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/5992b1b2-75d8-bc11-73a3-699d3234d120.png)


4.Nugetを選択します

![コメント 2019-12-06 085744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/6e33b1e8-a9b2-f011-b82a-50aa5640b698.png)

5. Azure Kinect で検索し、Azure Kinect Sensor SDK 1.3.0 をインストールします。（これで、SensorSDK以外に必要なモジュールも自動で入る）

![コメント 2019-12-06 091943.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/1fee332e-be06-25e0-5748-505df0911fc4.png)

6.```C:\Program Files\Azure Kinect SDK v1.3.0\sdk\windows-desktop\amd64\release\bin```
から「k4a.dll」「depthengine_2_0.dll」をPluginsフォルダに配置します。

7.これで環境構築は終了です。

## Azure Kinect での point cloud 表示
では、本題の point cloud つまり点群を表示してみようと思います。

調べていると点群を Unity で表示する方法は二つあって、
1.mesh を使う方法
2.particle を使う方法

があります。どちらもスクリプトの用意さえできれば難しくないのですが、より簡単だと思った particle を使う方法を紹介します。

### 点群表示までの手順 

1. ヒエラルキーを右クリックし、Effects>Particleで Particle を生成します
2. 
![コメント 2019-12-08 111402.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/ea126358-673a-823d-a5ff-90d3996130b4.png)

2.Particle System にカーソルを合わせ、右側のInspectorをプロパティのデフォルトから一部変更します。まず、Transform のRotation を X=0,Y=0,Z=-180に変更します。その次に、Play on Awake と Auto Random Seed のチェックボックスを外してください。

![コメント 2019-12-08 111643.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/2a0a13a0-8b74-5a25-94be-7a058aea637d.png)

3.Assetフォルダを右クリックして Create>C#script を指定します。名前はTestPointCloudにしますが任意の名前で大丈夫です。

![コメント 2019-12-08 112155.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/bc5ffb60-d9ec-cd4f-50ca-14858f414532.png)

4.TestPointCloud.cs の中身を以下で書き換えてください。

```csharp:TestPointcloud.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.Azure.Kinect.Sensor;
using System;


public class TestPointCloud : MonoBehaviour
{
    public GameObject particleSystemGameObject;
    private ParticleSystem pointCloudParticleSystem;
    private ParticleSystem.Particle[] particles;

    private Device device = null;
    private Capture capture = null;
    private Transformation transformation = null;

    private Color pointColor = Color.white;
    private float size = 0.02f;
    private float scale = 10.0f;

    void Start()
    {
        particleSystemGameObject = GameObject.Find("Particle System");


        device = Device.Open(0);
        device.StartCameras(new DeviceConfiguration
        {
            ColorFormat = ImageFormat.ColorBGRA32,
            ColorResolution = ColorResolution.R720p,
            DepthMode = DepthMode.NFOV_2x2Binned,
            SynchronizedImagesOnly = true,
            CameraFPS = FPS.FPS30
        });
        transformation = device.GetCalibration().CreateTransformation();
    }


    void Update()
    {
        capture = device.GetCapture();

        Image colorImage = capture.Color;
        Image depthImage = capture.Depth;

        int colorImageWidth = colorImage.WidthPixels;
        int colorImageHeight = colorImage.HeightPixels;

        List<Vector3> vertices = new List<Vector3>(colorImageWidth * colorImageHeight);
        List<Color32> colors = new List<Color32>(colorImageWidth * colorImageHeight);

        convertRGBDepthToPointXYZRGB(capture, ref vertices, ref colors);

        particles = new ParticleSystem.Particle[colorImageWidth * colorImageHeight];
        for (int i = 0; i < colorImageWidth * colorImageHeight; ++i)
        {
            particles[i].position = vertices[i];
            particles[i].startColor = colors[i];
            particles[i].startSize = size;
            if (vertices[i].z == 0.0f)
            {
                particles[i].startSize = 0;
            }
        }

        pointCloudParticleSystem = particleSystemGameObject.GetComponent<ParticleSystem>();
        pointCloudParticleSystem.Clear();
        pointCloudParticleSystem.SetParticles(particles, particles.Length);
    }
    public struct Point : IEquatable<Point>
    {
        private short x;
        private short y;
        private short z;

        public Point(short x, short y, short z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public short X { get => x; set => x = value; }
        public short Y { get => y; set => y = value; }
        public short Z { get => z; set => z = value; }

        public bool Equals(Point other)
        {
            return (this.x == other.x) && (this.y == other.y) && (this.z == other.z);
        }
    }
    public void convertRGBDepthToPointXYZRGB(Capture capture, ref List<Vector3> vertices, ref List<Color32> colors)
    {
        Image colorImage = capture.Color.Reference();
        int colorImageWidth = colorImage.WidthPixels;
        int colorImageHeight = colorImage.HeightPixels;

        Image transformedDepthImage = new Image(ImageFormat.Depth16, colorImageWidth, colorImageHeight, colorImageWidth * sizeof(short));
        Image pointCloudImage = new Image(ImageFormat.Custom, colorImageWidth, colorImageHeight, colorImageWidth * 3 * sizeof(short));

        transformation.DepthImageToColorCamera(capture, transformedDepthImage);
        transformation.DepthImageToPointCloud(transformedDepthImage, pointCloudImage, CalibrationDeviceType.Color);

        for (int h = 0; h < colorImageHeight; ++h)
        {
            for (int w = 0; w < colorImageWidth; ++w)
            {
                short x = pointCloudImage.GetPixel<Point>(h, w).X;
                short y = pointCloudImage.GetPixel<Point>(h, w).Y;
                short z = pointCloudImage.GetPixel<Point>(h, w).Z;

                Vector3 xyz = new Vector3(
                    (float)x / 1000.0f,
                    (float)y / 1000.0f,
                    (float)z / 1000.0f
                    );

                Color32 rgb = new Color32(
                    colorImage.GetPixel<BGRA>(h, w).R,
                    colorImage.GetPixel<BGRA>(h, w).G,
                    colorImage.GetPixel<BGRA>(h, w).B,
                    Byte.MaxValue);

                vertices.Add(xyz);
                colors.Add(rgb);
            }
        }
    }

    private void OnDestroy()
    {
        device.StopCameras();
    }

}

```

4.Particle System にカーソルを合わせ、右側のInspectorの一番下のAddComponentで今作成したTestPointCloud.csを指定します。

![コメント 2019-12-08 113432.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/30ee2da0-e822-d92f-3be1-7ab5207fe768.png)
![コメント 2019-12-08 113817.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/d4f10a23-26d4-6c58-12da-7160b9227abf.png)

5.指定したTestPointCloud.cs にはParticle System という部分が GameObject(None) 、つまり何も設定されていない状態になっていますので、ここに、左側のヒエラルキーから「Particle System」をドラッグして設定しましょう。

![コメント 2019-12-08 114027.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/fff515d1-81a0-370f-1d45-a59fc74a61f8.png)

最後に上のPlayボタンで、実行します。うまくいったらカメラから点群が見えるようになります。

![コメント 2019-12-08 114637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389500/787119e9-7253-ac8b-0432-eeccc96cb184.png)


・・・この処理では、カメラの移動などの機能はないのですが、Unity では Scene から視点を変えられるので、立体的に表示されているのも分かると思います。


## 振り返り

- 今回は、 C# から Unity まで手を伸ばしました。
- 実は、AzureKinectの Unity 初心者向けは既に吉永崇さんが、ハンズオンセミナーや、記事にまとめています。（私もこれで学びました）
- まだあまり中身が説明できていない or 自分でも分かっていないので、これは別に書いてみようかなと思います。

3D Sensor は奥が深く、多様な使い方ができるし、もっと役に立つ使い方があると思うので、色々と考えたいなと・・・