Unity上でHoloLensのカメラプレビューを表示する

投稿者: | 2017-08-30

前回は2D AppにおけるMediaCaptureの基本的な使い方についてまとめました。今回はUnityで作る3D AppにてMediaCaptureで取得したフレーム(映像)を表示する方法を説明したいと思います。また、空間上に表示しているCGと映像を同時にキャプチャするMixed Reality Captureの実装方法も併せて説明したいと思います。

なお、今回作ったクラスライブラリのサンプルは以下のリポジトリを参考にしています。

また、nityで使えるUWPのクラスライブラリの作り方については以下を参考にしてください。


MediaFrameSourceGroupを指定してMediaCaptureをインスタンス化する

前回同様、まずはMediaCaptureのインスタンスを作るのですが、最終的にUnityのTexture2Dに変換するためにはフレームをバイナリで取得する必要があります。その一つの方法としてMediaFrameReaderを使って取得する方法があります。この方法を使うためにはフレームソースグループ(MediaFrameSourceGroup)を指定してMediaCaptureのインスタンスを作る必要があります。

フレームソースとはデバイスが取得できるフレーム(映像)の種別を表しており、RGBカメラ、Depthカメラ、IRカメラなどが挙げられます。また、フレームソースグループとはデバイスが同時に利用できるフレームソースのセットです。例えばkinectの場合ですと、RGBカメラ、Depthカメラ、IRカメラを同時に起動してフレームを取得できるので、この3つのフレームソースを含んだフレームソースグループをアプリから取得できます。これをMediaCaptureに渡すことによって3つのカメラから同時にフレームを取得する、といったことも可能です。

フレームソースのグループは次のようにして取得することができます。


// ファクトリ経由でクラスのインスタンスを作成したときの完了時コールバックのデリゲート
public delegate void CaptureObjectCreatedCallback(CameraPreviewCapture createdObject);

public sealed class CameraPreviewCapture
{
    public event FrameArrivedCallback OnFrameArrived;

    private MediaFrameSourceInfo _frameSourceInfo;
    private MediaCapture _mediaCapture;

    /// コンストラクタ
    private CameraPreviewCapture(MediaFrameSourceInfo frameSourceInfo)
    {
        _frameSourceInfo = frameSourceInfo;
    }

    /// CameraPreviewCaptureのファクトリメソッド
    public static async Task CreateAync(CaptureObjectCreatedCallback onCreatedCallback)
    {
        // カメラプレビューが可能なRGBカメラを含むMediaFrameSourceGroupの一覧を取得する
        var allFrameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();
        var candidateFrameSourceGroups = allFrameSourceGroups.Where(group =>
            group.SourceInfos.Any(sourceInfo => 
                sourceInfo.MediaStreamType == MediaStreamType.VideoPreview && 
                sourceInfo.SourceKind == MediaFrameSourceKind.Color
            )
        );
        // 取得した一覧から先頭のMediaFrameSourceGroupを取得する
        var selectedFrameSourceGroup = candidateFrameSourceGroups.FirstOrDefault();
        if (selectedFrameSourceGroup == null)
        {
            onCreatedCallback?.Invoke(null);
            return;
        }
        // 選択したMediaFrameSourceGroupから、カメラプレビューが可能なRGBカメラのMediaFrameSourceInfoを取得
        var selectedFrameSourceInfo = selectedFrameSourceGroup.SourceInfos
            .Where(sourceInfo => 
                sourceInfo.SourceKind == MediaFrameSourceKind.Color && 
                sourceInfo.MediaStreamType == MediaStreamType.VideoPreview)
            .FirstOrDefault();
        if (selectedFrameSourceInfo == null)
        {
            onCreatedCallback?.Invoke(null);
            return;
        }

        // MediaFrameSourceが属するデバイスのDeviceInformationを取得する
        var deviceInformation = selectedFrameSourceInfo.DeviceInformation;
        if (deviceInformation == null)
        {
            onCreatedCallback?.Invoke(null);
            return;
        }

        // インスタンス化
        var videoCapture = new CameraPreviewCapture(selectedFrameSourceInfo);
        // MediaCaptureのインスタンスを作成
        var result = await videoCapture.CreateMediaCaptureAsync(selectedFrameSourceGroup, deviceInformation);

        if (result)
        {
            // インスタンスをコールバックに渡す
            onCreatedCallback?.Invoke(videoCape);
        } else
        {
            onCreatedCallback?.Invoke(null);
        }
    }

~以下略~

フレームソースグループが取得出来たら、それを使ってMediaCaptureのインスタンスを作成します。前回同様、MediaCaptureInitializationSettingsを使って初期化します。


/// MediaCaptureインスタンスの作成
private async Task CreateMediaCaptureAsync(MediaFrameSourceGroup frameSourceGroup, DeviceInformation deviceInfo)
{
    if (_mediaCapture != null)
    {
        return false;
    }
    _mediaCapture = new MediaCapture();
    // 設定オブジェクトの作成、MediaFrameSourceGroupを指定している
    // 取得した画像(フレーム)をバイナリで取得するため、
    // フレームをSoftwareBitmapとして取得できるようMemoryPreferenceを設定する
    var settings = new MediaCaptureInitializationSettings()
    {
        VideoDeviceId = deviceInfo.Id,
        SourceGroup = frameSourceGroup,
        MemoryPreference = MediaCaptureMemoryPreference.Cpu,
        StreamingCaptureMode = StreamingCaptureMode.Video
    };
    // MediaCaptureの初期化
    try
    {
        await _mediaCapture.InitializeAsync(settings);
        _mediaCapture.VideoDeviceController.Focus.TrySetAuto(true);
        return true;
    } catch(Exception)
    {
        // 何かしらの例外が発生
        _mediaCapture.Dispose();
        _mediaCapture = null;
        return false;
    }
}

MediaFrameReaderを作ってキャプチャを開始する

前回説明したMediaCaptureの使い方では、メソッドをコールするだけでキャプチャを画面に表示することができましたが、今回は取得したフレームのバイナリを直接操作したいので、MediaFrameReaderを使ってキャプチャを行います。MediaFrameReaderはキャプチャしたいフレームソース(MediaFrameSource)から作ることができます。なお、作成したMediaFrameReaderでキャプチャを開始すると、新しいフレームを取得する度にイベントが発生します。サンプルではMediaFrameReaderを作成し、同時にキャプチャを開始するメソッドを用意しました。


/// カメラプレビューを開始する
public async Task StartVideoModeAsync(bool IsCapturedHologram)
{
    // MediaFrameSourceを取得する
    // MediaFrameSourceはMediaFrameSourceGroupから直接取得することはできず
    // MediaCapture経由で取得する必要がある
    var mediaFrameSource = _mediaCapture.FrameSources[_frameSourceInfo.Id];

    if (mediaFrameSource == null)
    {
        return false;
    }
    // Unityのテクスチャに変換できるフォーマットを指定
    var pixelFormat= MediaEncodingSubtypes.Bgra8;
    // MediaFrameReaderの作成
    _frameReader = await _mediaCapture.CreateFrameReaderAsync(mediaFrameSource, pixelFormat);
    // フレームを取得したときのイベントハンドラを設定
    _frameReader.FrameArrived += HandleFrameArrived;
    // フレームの取得を開始する
    var result = await _frameReader.StartAsync();
    // デバイスがサポートするビデオフォーマットの一覧を取得する
    // ここではHoloLensがサポートする896x504 30fpsに絞って取得している
    var allPropertySets = _mediaCapture.VideoDeviceController
                            .GetAvailableMediaStreamProperties(MediaStreamType.VideoPreview)
        .Select(x => x as VideoEncodingProperties)
        .Where(x =>
        {
            if (x == null) return false;
            if (x.FrameRate.Denominator == 0) return false;

            double frameRate = (double)x.FrameRate.Numerator / (double)x.FrameRate.Denominator;

            return x.Width == 896 && x.Height == 504 && (int)Math.Round(frameRate) == 30;
        });
    // 取得したフォーマット情報を使ってキャプチャするフレームの解像度とFPSを設定する
    VideoEncodingProperties properties = allPropertySets.FirstOrDefault();
    await _mediaCapture.VideoDeviceController.SetMediaStreamPropertiesAsync(MediaStreamType.VideoPreview, properties);
    // Mixed Reality Captureの設定
    IVideoEffectDefinition ved = new MixedRealityCaptureSetting(IsCapturedHologram, false, 0, IsCapturedHologram ? 0.9f : 0.0f);
    await _mediaCapture.AddVideoEffectAsync(ved, MediaStreamType.VideoPreview);

    return true;
}

取得するフレームの解像度やフレームレートもここで指定しています。ここではパフォーマンスを考えて解像度を最低にしています。デバイスがサポートする解像度やフレームレートはVideoDeviceController.GetAvailableMediaStreamPropertiesメソッドからコレクションを取得できます。このコレクションの要素には、カメラ関連のプロパティを表すVideoEncodingProperties型の要素が含まれており、ここから解像度やフレームレートを取得できます。
詳細はリファレンスを参照してください。

キャプチャしたフレームをTexture2Dに変換する

新しいフレームが取得できるようになるとMediaFrameReaderのFrameArrivedイベントが発生します。サンプルではイベントハンドラ内でMediaFrameReader.TryAcquireLatestFrameメソッドから新しいフレームを取得し、内部のSoftwareBitmap型の変数に確保しています。また、クラスライブラリ内でもイベントを定義しており、新しいフレームが利用できるようになったことを外部に通知するようにしています。


public delegate void FrameArrivedCallback(int frameLength);

private SoftwareBitmap _bitmap;

/// 新しいフレームを取得したときのハンドラ
private void HandleFrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
    // プラグイン外からイベントハンドラが設定されていない場合は何もしない
    if (OnFrameArrived == null)
    {
        return;
    }
    // 最新のフレームを取得
    using (var frame = _frameReader.TryAcquireLatestFrame())
    {
        if (frame != null)
        {
            // SoftwareBitmapとして保持する
            _bitmap = frame.VideoMediaFrame.SoftwareBitmap; 
            // サブスクライバには新しいフレームが確保できたタイミングでSoftwareBitmapのサイズを通知
            OnFrameArrived?.Invoke(4 * _bitmap.PixelHeight * _bitmap.PixelWidth);
        }
    }
}

今回サンプルとして用意したクラスライブラリの主要なコードは以上となります。
続いてこのクラスライブラリを利用するUnityのスクリプトとして、次のようなものを用意しました。


using CameraPreview;
using HoloToolkit.Unity;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CameraPreviewTest : MonoBehaviour {
    // DLLで作ったクラス
    private CameraPreviewCapture  _cameraPreviewCapture;
    // カメラプレビューをレンダリングするためのテクスチャ
    private Texture2D texture;

    [SerializeField]
    private List rawImageList;

    public Texture2D previewTexture {
        get { return texture; }
    }
    
    private byte[] _latestImageBytes;

    private void Awake()
    {
        // テクスチャオブジェクトを作成
        texture = new Texture2D(896, 504, TextureFormat.BGRA32, false);
        // RawImageのテクスチャを設定
        foreach(var rawImage in rawImageList)
        {
            rawImage.texture = texture;
        }
        // ファクトリメソッドをコールしてCameraPreviewCaptureオブジェクトを取得
        CameraPreviewCapture.CreateAync(CameraPreviewCapture_OnCreated);
    }

    /// ファクトリメソッドを呼び出したときのコールバック
    /// カメラプレビューを開始する
    private async void CameraPreviewCapture_OnCreated(CameraPreviewCapture captureObject)
    {
        if(captureObject == null)
        {
            throw new Exception("Failed to create CameraPreviewCapture instance");
        }
        _cameraPreviewCapture = captureObject;
        // 新しいフレームを取得したときのイベントハンドラを設定
        _cameraPreviewCapture.OnFrameArrived += CameraPreviewCapture_OnFrameArrived;
        // カメラプレビューの開始
        // ここでMixed Reality Captureを有効化するかどうか設定できる(詳細は後述)
        var result = await _cameraPreviewCapture.StartVideoModeAsync(false);
        
        if(!result)
        {
            throw new Exception("Failed to start camera preview");
        }
    }

    /// 新しいフレームを取得したときのイベントハンドラ
    /// フレームのバイナリデータを取得しテクスチャに変換する
    private void CameraPreviewCapture_OnFrameArrived(int frameLength)
    {
        // フレームのデータサイズが通知されるので、それにあわせてバッファを作成する
        if (_latestImageBytes == null || _latestImageBytes.Length < frameLength)
        {
            _latestImageBytes = new byte[frameLength];
        }
        // クラスライブラリにバイナリバッファを渡して、フレームのバイナリをセットしてもらう
        _cameraPreviewCapture.CopyFrameToBuffer(_latestImageBytes);
        UnityEngine.WSA.Application.InvokeOnAppThread(() =>
        {
            // 読み込んだバイナリをテクスチャ化
            texture.LoadRawTextureData(_latestImageBytes);
            texture.Apply();
        }, false);
    }
}

クラスライブラリでは取得したフレームをSoftwareBitmapとして保持していますが、UnityではUWP系のクラスであるSoftwareBitmapを利用することはできません。そこでバイナリバッファを用意し、そのバッファをクラスライブラリに渡すことにより、フレームのバイナリを取得します。SoftwareBitmapからバイナリへの変換はクラスライブラリにてヘルパーメソッドを用意しています。


/// SoftwareBitmapとして保持しているフレームを引数のバイナリバッファに渡す
public void CopyFrameToBuffer(byte[] buffer)
{
    if(buffer == null)
    {
        throw new ArgumentException("buffer is null");
    }
    if(buffer.Length < 4 * _bitmap.PixelWidth * _bitmap.PixelHeight)
    {
        throw new IndexOutOfRangeException("buffer is not big enough");
    }
    if(_bitmap != null)
    {
        _bitmap.CopyToBuffer(buffer.AsBuffer());
        _bitmap.Dispose();
    }
}

Unityのスクリプトでは変換したテクスチャをRawImageに張り付けています。1点注意事項としてUWPの座標系とUnityの座標系は異なっており、UWPの座標系は右手系で+Y軸は床方向になります。そのため、そのまま表示すると上下反転した状態なので、RawImageのScaleのy成分をマイナス値にする必要があります。

ためしに複数のRawImageを用意し、キャプチャしたフレームのテクスチャを参照するようにしてみたところ、次のようになりました。

Mixed Reality Captureを実装する

MediaCaptureでは取得したフレームに対し、手ブレ補正や顔認識といった各種エフェクトをかけることができます。Mixed Reality Captureもこのエフェクトの一種として設定することができます。エフェクトはIVideoEffectDefinitionインタフェースを実装したクラスを、MediaCapture.AddVideoEffectAsyncメソッドに渡すことによってかけることができます。Mixed Reality Captureエフェクトをかける場合は、次のようなクラスを用意してそのインスタンスをMediaCaptureに渡してやります。

【参考】https://developer.microsoft.com/en-us/windows/mixed-reality/mixed_reality_capture_for_developers


public class MixedRealityCaptureSetting : IVideoEffectDefinition
{
    public string ActivatableClassId {
        get {
            return "Windows.Media.MixedRealityCapture.MixedRealityCaptureVideoEffect";
        }
    }

    public IPropertySet Properties {
        get; private set;
    }

    public MixedRealityCaptureSetting(bool HologramCompositionEnabled, bool VideoStabilizationEnabled, int VideoStabilizationBufferLength, float GlobalOpacityCoefficient)
    {
        Properties = (IPropertySet)new PropertySet();
        // CGも同時にキャプチャするかどうか
        Properties.Add("HologramCompositionEnabled", HologramCompositionEnabled);
        // 手ブレ補正を行うかどうか
        Properties.Add("VideoStabilizationEnabled", VideoStabilizationEnabled);
        // 手ブレ補正に使用するバッファ
        Properties.Add("VideoStabilizationBufferLength", VideoStabilizationBufferLength);
        // キャプチャしたCGの透過度
        Properties.Add("GlobalOpacityCoefficient", GlobalOpacityCoefficient);
    }
}

キャプチャを開始するStartVideoModeAsyncメソッドをコールするときに渡すbool値によって、Mixed Reality Captureを有効化するかどうかを指定できるようにしました。


/// カメラプレビューを開始する
public async Task StartVideoModeAsync(bool IsCapturedHologram)
{
    ~中略~

    // Mixed Reality Captureの設定
    IVideoEffectDefinition ved = new MixedRealityCaptureSetting(IsCapturedHologram, false, 0, IsCapturedHologram ? 0.9f : 0.0f);
    // エフェクトをMediaCaptureに設定する
    await _mediaCapture.AddVideoEffectAsync(ved, MediaStreamType.VideoPreview);

    return true;
}

カメラプレビューを表示するようなものでMixed Reality Captureを有効化した場合は鏡合わせのような見え方になってしまいますが、以下のようなものとなります。

まとめ

MediaCaptureを使ってUnityアプリ上でカメラプレビューを表示する方法と、Mixed Reality Captureを自アプリで使う方法についてまとめました。
今回のようなカメラプレビューにMixed Reality Captureを有効化してもいいことはあまりありませんが、Skypeのようにビデオチャット中に送信するといった使い方ならばいろいろと使い道はありそうです。

なお、今回サンプルとして作成したクラスライブラリとUnityプロジェクトは以下のリポジトリにアップしました。よろしければ参考にしてください。