【AzureKinect】【Unity】点群からメッシュをリアルタイム生成する
まえがき
最近、AzureKinectを入手しました! 楽しいです。
本記事では、↓動画のようにリアルタイムで点群からメッシュ化する実装を紹介します。
#AzureKinect でとった点群を、リアルタイムでMesh化できました pic.twitter.com/nJJoyQ15uE
— Hancho@xR (@a_hancho) 2020年7月6日
目次
本編
点群を表示
まずはベースとなる点群を表示します。こちらの記事がとても参考になりました。
AzureKinectから取得できるColorImageとDepthImageから、それぞれ点の頂点カラーと座標を取得しています。
このようなものができます。
頂点からポリゴン生成
上記記事内のコードで、以下のような記述があります。
mesh.SetIndices(indices, MeshTopology.Points, 0);
こちらは、ポリゴンの形状と頂点を指定する関数です。点群を表す MeshTopology.Points
の場合、indices(int[]またはList<int>)
には 0 ~ 頂点の合計数
の数が順番に入っているとよさそうです。
こちらを MeshTopology.Triangles
に変更し、indicesを三角形を紡いでいくように指定すると、面が成形できます。
そこで、以下のように変更してみます。
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; } }
完成品がこちら
正面からみるとできているようですが、俯瞰で見ると大変なことに...。
positionが(0, 0, 0)の頂点を含む場合はポリゴン生成しない
失敗したメッシュをよく分析してみると、positionがVector3(0, 0, 0)の点が無数あって、そこから面が作られているのが原因でした。
ここで、DepthImageを確認してみましょう。 白に近いほどAzureKinectから近距離にあります。
人の周りなど、深度差が大きい箇所が真っ白になっています。この部分の頂点が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上で選択できる機能を追加し、自己流に改造してみたコードがこちら。 コピペで動くはずです。
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; } }
補足情報
リポジトリ
Assets/_Project/Scenes/_Mocks/KinectMeshSample.unity
: 本記事と同様バージョンAssets/_Project/Scenes/KinectMeshSimpleColorVertex.unity
: Extenject(Zenject)、UniRxを使用したバージョン
AzureKlnectに関する個人用勉強場所です。参考までに。