TECH SCAPE

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

【AzureKinect】【Unity】RGBカメラと深度センサの情報を画像として表示する

f:id:a_hancho:20200805201723p:plain

AzureKinectのRGBカメラと深度センサの情報は、最大30fpsの速さで取得することができます。 以下記事では、これらの情報からメッシュを生成しました。

a-hancho.hateblo.jp

今回は、各種情報をTexture2Dに変換して表示します。 AzureKinectをUnity上で開発する際に、基本となる実装になるでしょう。

基本実装

AzureKinectの起動や終了等、基盤となるスクリプトです。

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

public class KinectImageViewer : MonoBehaviour
{
    private Device _kinectDevice = null;

    private void Start()
    {
        Init();
        StartLoop();
    }

    private void OnDestroy()
    {
        _kinectDevice.StopCameras();  // Kinectの終了処理
    }

    private void Init()
    {
        _kinectDevice = Device.Open(0);
        _kinectDevice.StartCameras(new DeviceConfiguration
        {
            ColorFormat = ImageFormat.ColorBGRA32,
            ColorResolution = ColorResolution.R1080p,
            DepthMode = DepthMode.NFOV_2x2Binned,
            SynchronizedImagesOnly = true,
            CameraFPS = FPS.FPS30
        });  // Kinectの開始処理。設定はお好みで。
    }

    private async void StartLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => _kinectDevice.GetCapture()).ConfigureAwait(true))
            {
                // ここで画像の生成、更新処理を行う
            }
        }
    }

ループ部分のcapture に、今後の実装に必要なことが入っています。 Unity向けに改変して表示していきます。

なお、Unity上でAzureKinectを初めて使用する方は、以下記事を参考にセットアップしてください。 tks-yoshinaga.hatenablog.com

RGB画像の表示

capture. capture.Color から1ピクセルずつの色情報を表すBGRA配列を取得し、テクスチャーを生成します。

そのままの順番で表示すると、上下左右が反転される点に注意しましょう。 UnityEngine.UIMicrosoft.Azure.Kinect.Sensor のImageが被るのでちょっとだけ厄介...。

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

public class KinectImageViewer : MonoBehaviour
{
    [SerializeField] private UnityEngine.UI.RawImage _viewerRawImage = null;

    // --- 省略  ---

    private async void StartLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => _kinectDevice.GetCapture()).ConfigureAwait(true))
            {
                // 必要な情報を用意
                Image colorImage = capture.Color;
                int pixelWidth = colorImage.WidthPixels;
                int pixelHeight = colorImage.HeightPixels;
                BGRA[] bgraArr = colorImage.GetPixels<BGRA>().ToArray();
                Color32[] colorArr = new Color32[bgraArr.Length];

                // BGRA配列 => Color32配列
                for (int i = 0; i < colorArr.Length; i++)
                {
                    int index = colorArr.Length - 1 - i;
                    colorArr[i] = new Color32(
                        bgraArr[index].R,
                        bgraArr[index].G,
                        bgraArr[index].B,
                        bgraArr[index].A
                    );
                }

                // Texture2Dの作成
                Texture2D resultTex = new Texture2D(pixelWidth , pixelHeight );
                resultTex.SetPixels32(colorArr);
                resultTex.Apply();

                // RawImageの更新
                _viewerRawImage.texture = GetTexture2D(pixelWidth, pixelHeight, colorArr);
                _viewerRawImage.rectTransform.sizeDelta = new Vector2(width, height); // rectTransformのサイズ変更
        }
    }
}

その辺にあったガムやルアーを配置して撮影してみました

f:id:a_hancho:20200805190506g:plain
RGBカメラ映像

深度画像の表示

capture.Depth から震度情報のushort配列を取得し、テクスチャーを生成します。 深度の範囲を指定できるようにしておくと、いい感じに画像を調整できます。

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

public class KinectImageViewer : MonoBehaviour
{
    [SerializeField] private UnityEngine.UI.RawImage _viewerRawImage = null;
    [SerializeField] private int _depthDistanceMin = 200;
    [SerializeField] private int _depthDistanceMax = 3000;

    // --- 省略  ---

    private async void StartLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => _kinectDevice.GetCapture()).ConfigureAwait(true))
            {
                // 必要な情報を用意
                Image depthImage = capture.Depth;
                int pixelWidth = depthImage.WidthPixels;
                int pixelHeight = depthImage.HeightPixels;
                ushort[] depthByteArr = depthImage.GetPixels<ushort>().ToArray();
                Color32[] colorArr = new Color32[depthByteArr.Length];

                // ushort配列 => Color32配列
                for (int i = 0; i < colorArr.Length; i++)
                {
                    int index = colorArr.Length - 1 - i;

                    int depthVal = 255 - (255 * (depthByteArr[index] - _depthDistanceMin) / _depthDistanceMax);  // 近いほど値が大きくなるよう計算
                    if (depthVal < 0)
                    {
                        depthVal = 0;
                    }
                    else if (depthVal > 255)
                    {
                        depthVal = 255;
                    }

                    colorArr[i] = new Color32(
                        (byte)depthVal,
                        (byte)depthVal,
                        (byte)depthVal,
                        255
                    );
                }

                // Texture2Dの作成
                Texture2D resultTex = new Texture2D(pixelWidth , pixelHeight );
                resultTex.SetPixels32(colorArr);
                resultTex.Apply();

                // RawImageの更新
                _viewerRawImage.texture = GetTexture2D(pixelWidth, pixelHeight, colorArr);
                _viewerRawImage.rectTransform.sizeDelta = new Vector2(width, height); // rectTransformのサイズ変更
        }
    }
}

結果はこんな感じ。RGBとDepthでは画角が異なります。

f:id:a_hancho:20200805192200g:plain
深度画像

完成版スクリプト

f:id:a_hancho:20200805171751p:plain

Inspector上で以下を変更できるようにしてみました。

  • 表示する画像を選択(RGB or Depth)
  • 深度の範囲
using Microsoft.Azure.Kinect.Sensor;
using System.Threading.Tasks;
using UnityEngine;

public enum KinectImageType
{
    RGB = 0,
    Depth = 1,
}

public class KinectImageViewer : MonoBehaviour
{
    [SerializeField] private KinectImageType _imageType = KinectImageType.RGB;
    [SerializeField] private UnityEngine.UI.RawImage _viewerRawImage = null;
    [SerializeField] private int _depthDistanceMin = 500;
    [SerializeField] private int _depthDistanceMax = 5000;

    private Device _kinectDevice = null;

    private void Start()
    {
        InitKinect();
        StartLoop();
    }

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

    private void InitKinect()
    {
        _kinectDevice = Device.Open(0);
        _kinectDevice.StartCameras(new DeviceConfiguration
        {
            ColorFormat = ImageFormat.ColorBGRA32,
            ColorResolution = ColorResolution.R1080p,
            DepthMode = DepthMode.NFOV_2x2Binned,
            SynchronizedImagesOnly = true,
            CameraFPS = FPS.FPS30
        });
    }

    private async void StartLoop()
    {
        while (true)
        {
            using (Capture capture = await Task.Run(() => _kinectDevice.GetCapture()).ConfigureAwait(true))
            {
                if (_imageType == 0)
                {
                    ViewColorImage(capture);
                }
                else
                {
                    ViewDepthImage(capture);
                }
            }
        }
    }

    // RGB情報をRawImageに表示
    private void ViewColorImage(Capture capture)
    {
        Image colorImage = capture.Color;
        int pixelWidth = colorImage.WidthPixels;
        int pixelHeight = colorImage.HeightPixels;

        BGRA[] bgraArr = colorImage.GetPixels<BGRA>().ToArray();
        Color32[] colorArr = new Color32[bgraArr.Length];

        for (int i = 0; i < colorArr.Length; i++)
        {
            int index = colorArr.Length - 1 - i;
            colorArr[i] = new Color32(
                bgraArr[index].R,
                bgraArr[index].G,
                bgraArr[index].B,
                bgraArr[index].A
            );
        }

        _viewerRawImage.texture = GetTexture2D(pixelWidth, pixelHeight, colorArr);
    }

    // 深度情報をRawImageに表示
    private void ViewDepthImage(Capture capture)
    {
        Image colorImage = capture.Depth;
        int pixelWidth = colorImage.WidthPixels;
        int pixelHeight = colorImage.HeightPixels;

        ushort[] depthByteArr = colorImage.GetPixels<ushort>().ToArray();
        Color32[] colorArr = new Color32[depthByteArr.Length];

        for (int i = 0; i < colorArr.Length; i++)
        {
            int index = colorArr.Length - 1 - i;

            int depthVal = 255 - (255 * (depthByteArr[index] - _depthDistanceMin) / _depthDistanceMax);
            if (depthVal < 0)
            {
                depthVal = 0;
            }
            else if (depthVal > 255)
            {
                depthVal = 255;
            }

            colorArr[i] = new Color32(
                (byte)depthVal,
                (byte)depthVal,
                (byte)depthVal,
                255
            );
        }

        _viewerRawImage.texture = GetTexture2D(pixelWidth, pixelHeight, colorArr);
    }

    // カラー配列 -> Texture2D
    private Texture2D GetTexture2D(int width, int height, Color32[] colorArr)
    {
        _viewerRawImage.rectTransform.sizeDelta = new Vector2(width, height);

        Texture2D resultTex = new Texture2D(width, height);
        resultTex.SetPixels32(colorArr);
        resultTex.Apply();
        return resultTex;
    }
}

参考記事

以下の公式サンプル github.com

Unityを使用せずに画像を表示している記事 tks-yoshinaga.hatenablog.com tks-yoshinaga.hatenablog.com

を参考にさせていただきました。