TECH SCAPE

AR関連多め(HoloLens, AzureKinect、SparkAR)

【AzureKinect】【Unity】点群からメッシュをリアルタイム生成する

f:id:a_hancho:20200721031530p:plain

まえがき

最近、AzureKinectを入手しました! 楽しいです。

本記事では、↓動画のようにリアルタイムで点群からメッシュ化する実装を紹介します。

目次

本編

点群を表示

まずはベースとなる点群を表示します。こちらの記事がとても参考になりました。

tks-yoshinaga.hatenablog.com

AzureKinectから取得できるColorImageとDepthImageから、それぞれ点の頂点カラーと座標を取得しています。

このようなものができます。

f:id:a_hancho:20200719193320g:plain

頂点からポリゴン生成

上記記事内のコードで、以下のような記述があります。

mesh.SetIndices(indices, MeshTopology.Points, 0);

こちらは、ポリゴンの形状と頂点を指定する関数です。点群を表す MeshTopology.Points の場合、indices(int[]またはList<int>) には 0 ~ 頂点の合計数 の数が順番に入っているとよさそうです。

こちらを MeshTopology.Triangles に変更し、indicesを三角形を紡いでいくように指定すると、面が成形できます。

https://cdn-ak.f.st-hatena.com/images/fotolife/s/soramamenatan/20190927/20190927152918.gif
> その30 気になる頂点インデックスの意義:より引用

そこで、以下のように変更してみます。

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

public class KinectScript : MonoBehaviour
{
    // --- 省略  ---

    private void InitMesh()
    {
        // --- 省略  ---

        List<int> indices = GetTriangleIndiceList();
        mesh.SetIndices(indices, MeshTopology.Triangles, 0);
        gameObject.GetComponent<MeshFilter>().mesh = mesh;
    }

    // triangleのindiceを取得する関数
    private List<int> GetTriangleIndiceList()
    {
        int width = kinect.GetCalibration().DepthCameraCalibration.ResolutionWidth;
        int height = kinect.GetCalibration().DepthCameraCalibration.ResolutionHeight;
        List<int> indicateList = new List<int>();

        for (int y = 0; y < height - 1; y++)
        {
            for (int x = 0; x < width - 1; x++)
            {
                int index = y * width + x;
                int a = index;
                int b = index + 1;
                int c = index + width;
                int d = index + width + 1;

                indicateList.Add(a);
                indicateList.Add(b);
                indicateList.Add(c);
                indicateList.Add(c);
                indicateList.Add(b);
                indicateList.Add(d);
            }
        }
        return indicateList;
    }
}

完成品がこちら

f:id:a_hancho:20200720150045p:plain

正面からみるとできているようですが、俯瞰で見ると大変なことに...。

positionが(0, 0, 0)の頂点を含む場合はポリゴン生成しない

失敗したメッシュをよく分析してみると、positionがVector3(0, 0, 0)の点が無数あって、そこから面が作られているのが原因でした。

ここで、DepthImageを確認してみましょう。 白に近いほどAzureKinectから近距離にあります。

f:id:a_hancho:20200721023546p:plain

人の周りなど、深度差が大きい箇所が真っ白になっています。この部分の頂点がVector3(0, 0, 0)になってしまうようです。

そこで、頂点のpositionのVector3.magnitudeが0の点を含んでいた場合はポリゴンの生成を行わないようにしてみます。

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

public class KinectScript : MonoBehaviour
{
    // --- 省略  ---

    private void InitMesh()
    {
        // --- 省略  ---

        // ここにあったmesh.SetIndicesは、loopしているスクリプトに移動

        gameObject.GetComponent<MeshFilter>().mesh = mesh;
    }

    private async Task KinectLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => kinect.GetCapture()).ConfigureAwait(true))
            {
                // --- 省略  ---

                mesh.vertices = vertices;
                mesh.colors32 = colors;
                mesh.RecalculateBounds();

                // indicesを更新
               int[] indices = GetIndices(vertices);
                mesh.SetIndices(indices, MeshTopology.Triangles, 0);
            }
        }
    }

    // triangleのindiceを取得する関数
    private int[] GetIndices(Vector3[] vertices)
    {
        int width = kinect.GetCalibration().DepthCameraCalibration.ResolutionWidth;
        int height = kinect.GetCalibration().DepthCameraCalibration.ResolutionHeight;
        List<int> indicateList = new List<int>();

        for (int y = 0; y < height - 1; y++)
        {
            for (int x = 0; x < width - 1; x++)
            {
                int index = y * width + x;
                int a = index;
                int b = index + 1;
                int c = index + width;
                int d = index + width + 1;

                bool isVaridA = vertices[a].magnitude != 0;
                bool isVaridB = vertices[b].magnitude != 0;
                bool isVaridC = vertices[c].magnitude != 0;
                bool isVaridD = vertices[d].magnitude != 0;

                if (isVaridA & isVaridB & isVaridC)
                {
                    indicateList.Add(a);
                    indicateList.Add(b);
                    indicateList.Add(c);
                }

                if (isVaridC & isVaridB & isVaridD)
                {
                    indicateList.Add(c);
                    indicateList.Add(b);
                    indicateList.Add(d);
                }
            }
        }
        return indicateList.ToArray();
    }
}

これで最初に乗せた動画のようにMesh化することができました。

完全版Script

TopologyをInspector上で選択できる機能を追加し、自己流に改造してみたコードがこちら。 コピペで動くはずです。

f:id:a_hancho:20200721011242p:plain

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

[RequireComponent(typeof(MeshFilter))]
public class KinectScript : MonoBehaviour
{
    [SerializeField] private MeshTopology _meshTopology = MeshTopology.Triangles;

    private Device _kinectDevice;
    private Transformation _kinectTransformation;

    private int _pointNum;
    private int _pointWidth;
    private int _pointHeight;

    private Mesh _mesh;
    private Vector3[] _meshVertices;
    private Color32[] _meshColors;

    private void Start()
    {
        InitKinect();
        InitMesh();
        Task task = KinectLoop();
    }

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

    //Kinectの初期化
    private void InitKinect()
    {
        // kinect device
        _kinectDevice = Device.Open(0);
        _kinectDevice.StartCameras(new DeviceConfiguration
        {
            ColorFormat = ImageFormat.ColorBGRA32,
            ColorResolution = ColorResolution.R720p,
            DepthMode = DepthMode.NFOV_2x2Binned,
            SynchronizedImagesOnly = true,
            CameraFPS = FPS.FPS30
        });
        _kinectTransformation = _kinectDevice.GetCalibration().CreateTransformation();

        // point count
        _pointWidth = _kinectDevice.GetCalibration().DepthCameraCalibration.ResolutionWidth;
        _pointHeight = _kinectDevice.GetCalibration().DepthCameraCalibration.ResolutionHeight;
        _pointNum = _pointWidth * _pointHeight;
    }

    //Mesh情報の初期化
    private void InitMesh()
    {
        _mesh = new Mesh();
        _mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        _meshVertices = new Vector3[_pointNum];
        _meshColors = new Color32[_pointNum];

        _mesh.vertices = _meshVertices;
        _mesh.colors32 = _meshColors;

        gameObject.GetComponent<MeshFilter>().mesh = _mesh;
    }

    //Kinectからデータを取得→描画
    private async Task KinectLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => _kinectDevice.GetCapture()).ConfigureAwait(true))
            {
                Image colorImage = _kinectTransformation.ColorImageToDepthCamera(capture);
                BGRA[] colorArray = colorImage.GetPixels<BGRA>().ToArray();

                Image xyzImage = _kinectTransformation.DepthImageToPointCloud(capture.Depth);
                Short3[] xyzArray = xyzImage.GetPixels<Short3>().ToArray();

                for (int i = 0; i < _pointNum; i++)
                {
                    _meshVertices[i].x = xyzArray[i].X * 0.001f;
                    _meshVertices[i].y = -xyzArray[i].Y * 0.001f;
                    _meshVertices[i].z = xyzArray[i].Z * 0.001f;

                    _meshColors[i].b = colorArray[i].B;
                    _meshColors[i].g = colorArray[i].G;
                    _meshColors[i].r = colorArray[i].R;
                    _meshColors[i].a = 255;
                }

                // update vertices and colors
                _mesh.vertices = _meshVertices;
                _mesh.colors32 = _meshColors;

                // update indices 
                List<int> indiceList = GetIndiceList(_meshVertices);
                _mesh.SetIndices(indiceList, _meshTopology, 0);

                _mesh.RecalculateBounds();
            }
        }
    }

    // meshのindicesを取得
    private List<int> GetIndiceList(Vector3[] vertices)
    {
        List<int> indiceList = new List<int>();

        if (_meshTopology == MeshTopology.Points)
        {
            for (int i = 0; i < _pointNum; i++)
            {
                bool isVaridPoint = vertices[i].magnitude != 0;
                if (isVaridPoint)
                {
                    indiceList.Add(i);
                }
            }
            return indiceList;
        }

        for (int y = 0; y < _pointHeight - 1; y++)
        {
            for (int x = 0; x < _pointWidth - 1; x++)
            {
                int index = y * _pointWidth + x;
                int a = index;
                int b = index + 1;
                int c = index + _pointWidth;
                int d = index + _pointWidth + 1;

                bool isVaridA = vertices[a].magnitude != 0;
                bool isVaridB = vertices[b].magnitude != 0;
                bool isVaridC = vertices[c].magnitude != 0;
                bool isVaridD = vertices[d].magnitude != 0;

                switch (_meshTopology)
                {
                    case MeshTopology.Triangles:
                        if (isVaridA & isVaridB & isVaridC)
                        {
                            indiceList.Add(a);
                            indiceList.Add(b);
                            indiceList.Add(c);
                        }
                        if (isVaridC & isVaridB & isVaridD)
                        {
                            indiceList.Add(c);
                            indiceList.Add(b);
                            indiceList.Add(d);
                        }
                        break;

                    case MeshTopology.Quads:
                        if (isVaridA && isVaridB && isVaridC && isVaridD)
                        {
                            indiceList.Add(a);
                            indiceList.Add(b);
                            indiceList.Add(d);
                            indiceList.Add(c);
                        }
                        break;

                    default:
                        if (isVaridA & isVaridB)
                        {
                            indiceList.Add(a);
                            indiceList.Add(b);
                        }
                        if (isVaridC & isVaridD)
                        {
                            indiceList.Add(c);
                            indiceList.Add(d);
                        }
                        break;
                }
            }
        }
        return indiceList;
    }
}

補足情報

リポジトリ

github.com

  • Assets/_Project/Scenes/_Mocks/KinectMeshSample.unity : 本記事と同様バージョン
  • Assets/_Project/Scenes/KinectMeshSimpleColorVertex.unity : Extenject(Zenject)、UniRxを使用したバージョン

AzureKlnectに関する個人用勉強場所です。参考までに。

他参考記事

qiita.com nn-hokuson.hatenablog.com