MediaFrameReader を使ったメディア フレームの処理

この記事では、MediaCapture と共に MediaFrameReader を使って、色、深度、赤外線カメラ、オーディオ デバイスなどの 1 つ以上の利用可能なソースや、スケルタル トラッキング フレームを生成するようなカスタム フレーム ソースから、メディア フレームを取得する方法を示します。 この機能は、拡張現実アプリや奥行きを検出するカメラ アプリなど、メディア フレームのリアルタイム処理を実行するアプリで使用するために設計されました。

一般的な写真アプリなど、ビデオや写真を単にキャプチャすることに興味がある場合は、MediaCapture でサポートされているその他のキャプチャ手法のいずれかを使う方がよいでしょう。 利用可能なメディア キャプチャ手法のリストとそれらの使用方法を示す記事については、「カメラ」をご覧ください。

注意

この記事で説明している機能は、Windows 10 バージョン 1607 以降でのみ利用できます。

注意

MediaFrameReader を使ってカラー カメラ、深度カメラ、赤外線カメラなど、さまざまなフレーム ソースからのフレームを表示する方法を示す、ユニバーサル Windows アプリのサンプルがあります。 詳しくは、「カメラ フレームのサンプル」をご覧ください。

注意

Windows 10、バージョン 1803 では、オーディオ データで MediaFrameReader を使用するための新しい API セットが導入されました。 詳しくは、「MediaFrameReader を使ったオーディオ フレームの処理」をご覧ください。

プロジェクトの設定

MediaCapture を使う他のアプリと同様に、カメラ デバイスにアクセスする前にアプリが webcam 機能を使うことを宣言する必要があります。 アプリがオーディオ デバイスからキャプチャする場合は、microphone デバイス機能も宣言する必要があります。

アプリ マニフェストに機能を追加する

  1. Microsoft Visual Studio のソリューション エクスプローラーで、package.appxmanifest 項目をダブルクリックしてアプリケーション マニフェストのデザイナーを開きます。
  2. [機能] タブを選択します。
  3. [Web カメラ] のボックスと [マイク] のボックスをオンにします。
  4. 画像ライブラリとビデオ ライブラリにアクセスするには、[画像ライブラリ] のボックスと[ビデオ ライブラリ] のボックスをオンにします。

この記事のサンプル コードでは、既定のプロジェクト テンプレートに含まれている API に加えて、次の名前空間の API が使っています。

using Windows.Media.Capture.Frames;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Media.MediaProperties;
using Windows.Graphics.Imaging;
using System.Threading;
using Windows.UI.Core;
using System.Threading.Tasks;
using Windows.Media.Core;
using System.Diagnostics;
using Windows.Media;
using Windows.Media.Devices;
using Windows.Media.Audio;

フレーム ソースとフレーム ソース グループを選択する

メディア フレームを処理する多くのアプリは、デバイスの色、深度カメラなど、複数のソースからフレームを一度に取得する必要があります。 MediaFrameSourceGroup オブジェクトは、同時に使用できるメディア フレーム ソースのセットを表します。 静的メソッド MediaFrameSourceGroup.FindAllAsync を呼び出して、現在のデバイスでサポートされているフレーム ソースのすべてのグループの一覧を取得します。

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

DeviceInformation.CreateWatcherMediaFrameSourceGroup.GetDeviceSelector から返される値を使って DeviceWatcher を作成して、外付けカメラが接続されたときなど、デバイス上の利用可能なフレーム ソース グループが変更されたときに通知を受け取ることもできます。 詳しくは、「デバイスの列挙」をご覧ください。

MediaFrameSourceGroup には、グループに含まれるフレーム ソースを記述する MediaFrameSourceInfo オブジェクトのコレクションがあります。 デバイスで利用可能なフレーム ソース グループを取得した後、目的のフレーム ソースを公開するグループを選択できます。

次の例は、フレーム ソース グループを選択する最も簡単な方法を示しています。 このコードは、すべての利用可能なグループをループで処理してから、SourceInfos コレクション内の各項目をループで処理します。 各 MediaFrameSourceInfo について、目的の機能をサポートしているかどうかがチェックされます。 この場合は、MediaStreamType プロパティでデバイスがビデオ プレビュー ストリームを提供するかどうかを示す値 VideoPreview がチェックされ、SourceKind プロパティでソースが色のフレームを提供するかどうかを示す値 Color がチェックされます。

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

MediaFrameSourceGroup selectedGroup = null;
MediaFrameSourceInfo colorSourceInfo = null;

foreach (var sourceGroup in frameSourceGroups)
{
    foreach (var sourceInfo in sourceGroup.SourceInfos)
    {
        if (sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
            && sourceInfo.SourceKind == MediaFrameSourceKind.Color)
        {
            colorSourceInfo = sourceInfo;
            break;
        }
    }
    if (colorSourceInfo != null)
    {
        selectedGroup = sourceGroup;
        break;
    }
}

目的のフレーム ソース グループとフレーム ソースを識別するこの方法は、簡単なケースでは機能しますが、より複雑な条件に基づいてフレーム ソースを選択する場合は難しくなります。 別の方法として、Linq 構文と匿名オブジェクトを使って選択する方法があります。 次の例では、Select 拡張メソッドを使って、frameSourceGroups 一覧内の MediaFrameSourceGroup オブジェクトを 2 つのフィールドを持つ匿名オブジェクトに変換します。2 つのフィールドは、グループ自体を表す sourceGroup と、グループ内の色のフレーム ソースを表す colorSourceInfo です。 colorSourceInfo フィールドは、指定された述語が true に解決される最初のオブジェクトを選ぶ FirstOrDefault の結果に設定されます。 この場合、述語が true になるのは、ストリーム タイプが VideoPreview、ソースの種類が Color で、カメラがデバイスのフロント パネルにある場合です。

上記のクエリから返された匿名オブジェクトの一覧から、Where 拡張メソッドを使って、colorSourceInfo フィールドが null でないオブジェクトのみを選択します。 最後に、FirstOrDefault が呼び出されて一覧内で最初の項目が選択されます。

これで、選択したオブジェクトのフィールドを使って、選択した MediaFrameSourceGroup とカラー カメラを表す MediaFrameSourceInfo オブジェクトへの参照を取得できます。 後でこれらを使って、MediaCapture オブジェクトを初期化し、選択したソースの MediaFrameReader を作成します。 最後に、ソース グループが null であるかどうかをテストする必要があります。これは、現在のデバイスが要求されたキャプチャ ソースを持っていないことを意味します。

var selectedGroupObjects = frameSourceGroups.Select(group =>
   new
   {
       sourceGroup = group,
       colorSourceInfo = group.SourceInfos.FirstOrDefault((sourceInfo) =>
       {
           // On Xbox/Kinect, omit the MediaStreamType and EnclosureLocation tests
           return sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
           && sourceInfo.SourceKind == MediaFrameSourceKind.Color
           && sourceInfo.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front;
       })

   }).Where(t => t.colorSourceInfo != null)
   .FirstOrDefault();

MediaFrameSourceGroup selectedGroup = selectedGroupObjects?.sourceGroup;
MediaFrameSourceInfo colorSourceInfo = selectedGroupObjects?.colorSourceInfo;

if (selectedGroup == null)
{
    return;
}

次の例では、上記と同様の手法を使って、カラー カメラ、深度カメラ、赤外線カメラなどのソース グループを選択します。

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Infrared),
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo infraredSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[2];

注意

Windows 10、バージョン 1803 以降では、MediaCaptureVideoProfile クラスを使用して、目的の機能セットを備えたメディア フレーム ソースを選択できます。 詳しくは、後ほどこの記事の「ビデオ プロファイルを使用してフレーム ソースを選択する」で説明します。

選択したフレーム ソース グループを使うように MediaCapture オブジェクトを初期化する

次に、前の手順で選択したフレーム ソース グループを使うように MediaCapture オブジェクトを初期化します。

通常、MediaCapture オブジェクトはアプリ内の複数の場所から使用できるため、それを保持するクラス メンバー変数を宣言する必要があります。

MediaCapture mediaCapture;

コンス トラクターを呼び出して、MediaCapture オブジェクトのインスタンスを作成します。 次に、MediaCapture オブジェクトの初期化に使われる MediaCaptureInitializationSettings オブジェクトを作成します。 この例では、次の設定を使用しています。

  • SourceGroup - どのソース グループを使ってフレームを取得するかをシステムに知らせます。 ソース グループは、同時に使用できるメディア フレーム ソースのセットを定義することに注意してください。
  • SharingMode - キャプチャ ソース デバイスに関して排他的な制御が必要かどうかをシステムに知らせます。 これを ExclusiveControl に設定すると、生成するフレームの形式など、キャプチャ デバイスの設定を変更することができますが、別のアプリが既に排他的制御を持っている場合、自分のアプリはメディア キャプチャ デバイスを初期化しようとすると失敗します。 これを SharedReadOnly に設定すると、別のアプリで使われてもフレーム ソースからフレームを受け取ることができますが、デバイスの設定を変更することはできません。
  • MemoryPreference - CPU を指定すると、システムは CPU メモリを使います。この場合、フレームが到着したとき、それらを SoftwareBitmap オブジェクトとして利用できることが保証されます。 Auto を指定すると、システムはフレームを格納するのに最適なメモリの場所を動的に選択します。 システムが GPU メモリの使用を選択した場合、メディア フレームは SoftwareBitmap としてではなく、IDirect3DSurface オブジェクトとして到着します。
  • StreamingCaptureMode - これを Video に設定すると、オーディオをストリーミングする必要がないことが指定されます。

InitializeAsync を呼び出して、目的の設定で MediaCapture を初期化します。 初期化が失敗する場合は、必ず try ブロック内でこれを呼び出してください。

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

フレーム ソースの優先形式を設定する

フレーム ソースの優先形式を設定するには、ソースを表す MediaFrameSource オブジェクトを取得する必要があります。 このオブジェクトを取得するには、初期化した MediaCapture オブジェクトの Frames ディクショナリにアクセスし、使用するフレーム ソースの識別子を指定します。 これは、フレーム ソース グループを選択していたときに MediaFrameSourceInfo オブジェクトを保存したからです。

MediaFrameSource.SupportedFormats プロパティには、フレーム ソースのサポートされている形式を記述する MediaFrameFormat オブジェクトの一覧が含まれています。 Where Linq 拡張メソッドを使って、目的のプロパティに基づいて形式を選択します。 この例では、幅が 1080 ピクセルで、32 ビット RGB 形式のフレームを提供できる形式が選択されています。 FirstOrDefault 拡張メソッドは一覧内で最初のエントリを選択します。 選択された形式が null の場合、要求された形式はそのフレーム ソースでサポートされません。 形式がサポートされている場合は、SetFormatAsync を呼び出すことでソースがこの形式を使うことを要求できます。

var colorFrameSource = mediaCapture.FrameSources[colorSourceInfo.Id];
var preferredFormat = colorFrameSource.SupportedFormats.Where(format =>
{
    return format.VideoFormat.Width >= 1080
    && format.Subtype == MediaEncodingSubtypes.Argb32;

}).FirstOrDefault();

if (preferredFormat == null)
{
    // Our desired format is not supported
    return;
}

await colorFrameSource.SetFormatAsync(preferredFormat);

フレーム ソースのフレーム リーダーを作成する

メディア フレーム ソースのフレームを受け取るには、MediaFrameReader を使います。

MediaFrameReader mediaFrameReader;

初期化した MediaCapture オブジェクトで CreateFrameReaderAsync を呼び出して、フレーム リーダーをインスタンス化します。 このメソッドの最初の引数は、フレームを受け取るフレーム ソースです。 使用するフレーム ソースごとに、別々のフレーム リーダーを作成できます。 2 番目の引数で、フレームが到着するときの出力形式をシステムに知らせます。 これによって、フレームが到着したときに独自の変換を行う必要がなくなります。 フレーム ソースでサポートされていない形式を指定すると例外がスローされるため、その値が SupportedFormats コレクションにあることを確認してください。

フレーム リーダーを作成した後、ソースから新しいフレームが利用可能になったときに発生する FrameArrived イベントのハンドラーを登録します。

StartAsync を呼び出して、ソースからフレームの読み取りを開始するようにシステムに伝えます。

mediaFrameReader = await mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await mediaFrameReader.StartAsync();

フレームの到着イベントを処理する

新しいフレームが利用可能になると、MediaFrameReader.FrameArrived イベントが発生します。 到着したすべてのフレームを処理するか、必要なときのみフレームを使うかを選択できます。 フレーム リーダーは自身のスレッドでイベントを発生させるため、何らかの同期ロジックを実装して、複数のスレッドからの同じデータにアクセスしていないことを確認する必要があります。 このセクションでは、XAML ページのイメージ コントロールへの色フレームの描画を同期する方法を示します。 このシナリオでは、XAML コントロールへのすべての更新を UI スレッドで実行するように要求する追加の同期制約について検討します。

XAML でフレームを表示するときの最初の手順は、イメージ コントロールの作成です。

<Image x:Name="imageElement" Width="320" Height="240" />

コード ビハインド ページで、SoftwareBitmap 型のクラス メンバー変数を宣言します。これは、すべての着信イメージのコピー先のバック バッファーとして使用されます。 イメージ データ自体はコピーされず、オブジェクト参照だけがコピーされます。 また、UI 操作が現在実行されているかどうかを追跡するブール値を宣言します。

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

フレームは SoftwareBitmap オブジェクトとして到着するため、SoftwareBitmap を XAML Control のソースとして使用できるようにする SoftwareBitmapSource オブジェクトを作成する必要があります。 フレーム リーダーを開始する前に、コード内のどこかでイメージ ソースを設定する必要があります。

imageElement.Source = new SoftwareBitmapSource();

ここで、FrameArrived イベント ハンドラーを実装します。 ハンドラーが呼び出されると、sender パラメーターにイベントを発生させた MediaFrameReader オブジェクトへの参照が含まれます。 このオブジェクトで TryAcquireLatestFrame を呼び出して、最新のフレームの取得を試みます。 名前からわかるように、TryAcquireLatestFrame はフレームを返すことに失敗することがあります。 そのため、VideoMediaFrame プロパティ、SoftwareBitmap プロパティの順にアクセスするときは、必ず null を検査してください。 この例では、null 条件演算子 ? を使って SoftwareBitmap にアクセスした後、取得したオブジェクトで null がチェックされています。

Image コントロールは、プリマルチプライ処理済みまたはアルファなしの BRGA8 形式でのみイメージを表示できます。 到着するフレームがその形式でない場合は、静的メソッド Convert を使ってソフトウェア ビットマップを正しい形式に変換します。

次に、Interlocked.Exchange メソッドを使って、到着するビットマップの参照をバックバッファー ビットマップと交換します。 このメソッドは、スレッド セーフであるアトミック操作でこれらの参照を交換します。 交換後、softwareBitmap 変数に格納されている古いバックバッファー イメージは、リソースをクリーンアップするために破棄されます。

次に、Image 要素に関連付けられている CoreDispatcher を使って、RunAsync を呼び出して UI スレッドで実行されるタスクを作成します。 非同期タスクはタスク内で実行されるため、RunAsync に渡されるラムダ式は async キーワードを付けて宣言されます。

タスク内で、_taskRunning 変数をチェックして、タスクの 1 つのインスタンスだけが一度に実行されていることを確認します。 タスクが既に実行されていない場合は、タスクがもう一度実行されることを防ぐために _taskRunning を true に設定します。 while ループで、バックバッファー イメージが null になるまで、Interlocked.Exchange を呼び出してバックバッファーから一時的な SoftwareBitmap にコピーします。 一時的なビットマップが入力されるたびに、ImageSource プロパティを SoftwareBitmapSource にキャストし、SetBitmapAsync を呼び出してイメージのソースを設定します。

最後に、次回にハンドラーが呼び出されたときにタスクをもう一度実行できるように、_taskRunning 変数を false に戻します。

注意

MediaFrameReferenceVideoMediaFrame プロパティが提供する SoftwareBitmap オブジェクトまたは Direct3DSurface オブジェクトにアクセスすると、これらのオブジェクトへの強参照がシステムによって作成されます。そのため、それらが含まれている MediaFrameReference に対して Dispose を呼び出してもこれらのオブジェクトは破棄されません。 それらのオブジェクトを即座に破棄するには、SoftwareBitmap または Direct3DSurfaceDispose メソッドを明示的に直接呼び出す必要があります。 そうしない場合、最終的にはガーベジ コレクターによってこれらのオブジェクトのメモリが解放されますが、それがいつになるかは不明であり、割り当てられたビットマップやサーフェスの数がシステムによって許容される最大数を上回った場合、新しいフレームのフローが停止します。 この制約に対処するには、取得したフレームをたとえば SoftwareBitmap.Copy メソッドによってコピーし、その後、元のフレームを解放することができます。 また、オーバーロード CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) または CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype) を使用して MediaFrameReader を作成した場合、介されるフレームは元のフレーム データのコピーであるため、それらのフレームを保持しても取得は停止しません。

private void ColorFrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
    var mediaFrameReference = sender.TryAcquireLatestFrame();
    var videoMediaFrame = mediaFrameReference?.VideoMediaFrame;
    var softwareBitmap = videoMediaFrame?.SoftwareBitmap;

    if (softwareBitmap != null)
    {
        if (softwareBitmap.BitmapPixelFormat != Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8 ||
            softwareBitmap.BitmapAlphaMode != Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)
        {
            softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }

        // Swap the processed frame to _backBuffer and dispose of the unused image.
        softwareBitmap = Interlocked.Exchange(ref backBuffer, softwareBitmap);
        softwareBitmap?.Dispose();

        // Changes to XAML ImageElement must happen on UI thread through Dispatcher
        var task = imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
            async () =>
            {
                // Don't let two copies of this task run at the same time.
                if (taskRunning)
                {
                    return;
                }
                taskRunning = true;

                // Keep draining frames from the backbuffer until the backbuffer is empty.
                SoftwareBitmap latestBitmap;
                while ((latestBitmap = Interlocked.Exchange(ref backBuffer, null)) != null)
                {
                    var imageSource = (SoftwareBitmapSource)imageElement.Source;
                    await imageSource.SetBitmapAsync(latestBitmap);
                    latestBitmap.Dispose();
                }

                taskRunning = false;
            });
    }

    mediaFrameReference.Dispose();
}

リソースをクリーンアップする

フレームの読み込みが終わったら、必ず StopAsync を呼び出してメディア フレーム リーダーを停止して、FrameArrived ハンドラーの登録を解除し、MediaCapture オブジェクトを破棄してください。

await mediaFrameReader.StopAsync();
mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
mediaCapture.Dispose();
mediaCapture = null;

アプリケーションが中断されたときのメディア キャプチャ オブジェクトのクリーンアップについて詳しくは、「カメラ プレビューの表示」をご覧ください。

FrameRenderer ヘルパー クラス

ユニバーサル Windows のカメラ フレームのサンプルは、アプリで色、赤外線、および深度のソースからフレームを表示するのを容易にするヘルパー クラスを提供します。 通常、深度や赤外線のデータを画面に表示するだけでなく、データを使ってそれ以上のことを行いたいですが、このヘルパー クラスは、フレーム リーダーの機能を示したり、独自のフレーム リーダーの実装をデバッグしたりするための便利なツールです。

FrameRenderer ヘルパー クラスは、次のメソッドを実装します。

  • FrameRenderer コンストラクター - このコンストラクターは、メディア フレームを表示するために渡す XAML Image 要素を使うようにヘルパー クラスを初期化します。
  • ProcessFrame - このメソッドは、コンストラクターに渡した Image 要素に、MediaFrameReference で表されるメディア フレームを表示します。 通常、FrameArrived イベント ハンドラーからこのメソッドを呼び出し、TryAcquireLatestFrame から返されるフレームを渡します。
  • ConvertToDisplayableImage - このメソッドは、メディア フレームの形式をチェックし、必要な場合は表示可能な形式に変換します。 カラー イメージの場合は、色の形式が BGRA8 であり、ビットマップのアルファ モードがプリマルチプライ済みであることを確認します。 深度または赤外線フレームの場合は、各スキャン ラインを処理して、深度または赤外線の値が疑似カラー グラデーションに変換します。これには、サンプルに含まれていて以下に記載される PseudoColorHelper クラスを使います。

注意

SoftwareBitmap イメージ上でピクセル操作を行うには、ネイティブ メモリ バッファーにアクセスする必要があります。 これを行うには、以下のコードに含まれている IMemoryBufferByteAccess COM インターフェイスを使う必要があり、アンセーフ コードのコンパイルを許可するようにプロジェクトのプロパティを更新する必要があります。 詳しくは、「ビットマップ画像の作成、編集、保存」をご覧ください。

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

class FrameRenderer
{
    private Image _imageElement;
    private SoftwareBitmap _backBuffer;
    private bool _taskRunning = false;

    public FrameRenderer(Image imageElement)
    {
        _imageElement = imageElement;
        _imageElement.Source = new SoftwareBitmapSource();
    }

    // Processes a MediaFrameReference and displays it in a XAML image control
    public void ProcessFrame(MediaFrameReference frame)
    {
        var softwareBitmap = FrameRenderer.ConvertToDisplayableImage(frame?.VideoMediaFrame);
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }



    // Function delegate that transforms a scanline from an input image to an output image.
    private unsafe delegate void TransformScanline(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes);
    /// <summary>
    /// Determines the subtype to request from the MediaFrameReader that will result in
    /// a frame that can be rendered by ConvertToDisplayableImage.
    /// </summary>
    /// <returns>Subtype string to request, or null if subtype is not renderable.</returns>

    public static string GetSubtypeForFrameReader(MediaFrameSourceKind kind, MediaFrameFormat format)
    {
        // Note that media encoding subtypes may differ in case.
        // https://docs.microsoft.com/en-us/uwp/api/Windows.Media.MediaProperties.MediaEncodingSubtypes

        string subtype = format.Subtype;
        switch (kind)
        {
            // For color sources, we accept anything and request that it be converted to Bgra8.
            case MediaFrameSourceKind.Color:
                return Windows.Media.MediaProperties.MediaEncodingSubtypes.Bgra8;

            // The only depth format we can render is D16.
            case MediaFrameSourceKind.Depth:
                return String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.D16, StringComparison.OrdinalIgnoreCase) ? subtype : null;

            // The only infrared formats we can render are L8 and L16.
            case MediaFrameSourceKind.Infrared:
                return (String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L8, StringComparison.OrdinalIgnoreCase) ||
                    String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L16, StringComparison.OrdinalIgnoreCase)) ? subtype : null;

            // No other source kinds are supported by this class.
            default:
                return null;
        }
    }

    /// <summary>
    /// Converts a frame to a SoftwareBitmap of a valid format to display in an Image control.
    /// </summary>
    /// <param name="inputFrame">Frame to convert.</param>

    public static unsafe SoftwareBitmap ConvertToDisplayableImage(VideoMediaFrame inputFrame)
    {
        SoftwareBitmap result = null;
        using (var inputBitmap = inputFrame?.SoftwareBitmap)
        {
            if (inputBitmap != null)
            {
                switch (inputFrame.FrameReference.SourceKind)
                {
                    case MediaFrameSourceKind.Color:
                        // XAML requires Bgra8 with premultiplied alpha.
                        // We requested Bgra8 from the MediaFrameReader, so all that's
                        // left is fixing the alpha channel if necessary.
                        if (inputBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8)
                        {
                            System.Diagnostics.Debug.WriteLine("Color frame in unexpected format.");
                        }
                        else if (inputBitmap.BitmapAlphaMode == BitmapAlphaMode.Premultiplied)
                        {
                            // Already in the correct format.
                            result = SoftwareBitmap.Copy(inputBitmap);
                        }
                        else
                        {
                            // Convert to premultiplied alpha.
                            result = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
                        }
                        break;

                    case MediaFrameSourceKind.Depth:
                        // We requested D16 from the MediaFrameReader, so the frame should
                        // be in Gray16 format.
                        if (inputBitmap.BitmapPixelFormat == BitmapPixelFormat.Gray16)
                        {
                            // Use a special pseudo color to render 16 bits depth frame.
                            var depthScale = (float)inputFrame.DepthMediaFrame.DepthFormat.DepthScaleInMeters;
                            var minReliableDepth = inputFrame.DepthMediaFrame.MinReliableDepth;
                            var maxReliableDepth = inputFrame.DepthMediaFrame.MaxReliableDepth;
                            result = TransformBitmap(inputBitmap, (w, i, o) => PseudoColorHelper.PseudoColorForDepth(w, i, o, depthScale, minReliableDepth, maxReliableDepth));
                        }
                        else
                        {
                            System.Diagnostics.Debug.WriteLine("Depth frame in unexpected format.");
                        }
                        break;

                    case MediaFrameSourceKind.Infrared:
                        // We requested L8 or L16 from the MediaFrameReader, so the frame should
                        // be in Gray8 or Gray16 format. 
                        switch (inputBitmap.BitmapPixelFormat)
                        {
                            case BitmapPixelFormat.Gray16:
                                // Use pseudo color to render 16 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor16BitInfrared);
                                break;

                            case BitmapPixelFormat.Gray8:
                                // Use pseudo color to render 8 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor8BitInfrared);
                                break;
                            default:
                                System.Diagnostics.Debug.WriteLine("Infrared frame in unexpected format.");
                                break;
                        }
                        break;
                }
            }
        }

        return result;
    }



    /// <summary>
    /// Transform image into Bgra8 image using given transform method.
    /// </summary>
    /// <param name="softwareBitmap">Input image to transform.</param>
    /// <param name="transformScanline">Method to map pixels in a scanline.</param>

    private static unsafe SoftwareBitmap TransformBitmap(SoftwareBitmap softwareBitmap, TransformScanline transformScanline)
    {
        // XAML Image control only supports premultiplied Bgra8 format.
        var outputBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8,
            softwareBitmap.PixelWidth, softwareBitmap.PixelHeight, BitmapAlphaMode.Premultiplied);

        using (var input = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
        using (var output = outputBitmap.LockBuffer(BitmapBufferAccessMode.Write))
        {
            // Get stride values to calculate buffer position for a given pixel x and y position.
            int inputStride = input.GetPlaneDescription(0).Stride;
            int outputStride = output.GetPlaneDescription(0).Stride;
            int pixelWidth = softwareBitmap.PixelWidth;
            int pixelHeight = softwareBitmap.PixelHeight;

            using (var outputReference = output.CreateReference())
            using (var inputReference = input.CreateReference())
            {
                // Get input and output byte access buffers.
                byte* inputBytes;
                uint inputCapacity;
                ((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputBytes, out inputCapacity);
                byte* outputBytes;
                uint outputCapacity;
                ((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputBytes, out outputCapacity);

                // Iterate over all pixels and store converted value.
                for (int y = 0; y < pixelHeight; y++)
                {
                    byte* inputRowBytes = inputBytes + y * inputStride;
                    byte* outputRowBytes = outputBytes + y * outputStride;

                    transformScanline(pixelWidth, inputRowBytes, outputRowBytes);
                }
            }
        }

        return outputBitmap;
    }



    /// <summary>
    /// A helper class to manage look-up-table for pseudo-colors.
    /// </summary>

    private static class PseudoColorHelper
    {
        #region Constructor, private members and methods

        private const int TableSize = 1024;   // Look up table size
        private static readonly uint[] PseudoColorTable;
        private static readonly uint[] InfraredRampTable;

        // Color palette mapping value from 0 to 1 to blue to red colors.
        private static readonly Color[] ColorRamp =
        {
            Color.FromArgb(a:0xFF, r:0x7F, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x7F, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0xFF, b:0x00),
            Color.FromArgb(a:0xFF, r:0x7F, g:0xFF, b:0x7F),
            Color.FromArgb(a:0xFF, r:0x00, g:0xFF, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x7F, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0x7F),
        };

        static PseudoColorHelper()
        {
            PseudoColorTable = InitializePseudoColorLut();
            InfraredRampTable = InitializeInfraredRampLut();
        }

        /// <summary>
        /// Maps an input infrared value between [0, 1] to corrected value between [0, 1].
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]  // Tell the compiler to inline this method to improve performance

        private static uint InfraredColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return InfraredRampTable[index];
        }

        /// <summary>
        /// Initializes the pseudo-color look up table for infrared pixels
        /// </summary>

        private static uint[] InitializeInfraredRampLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                var value = (float)i / TableSize;
                // Adjust to increase color change between lower values in infrared images

                var alpha = (float)Math.Pow(1 - value, 12);
                lut[i] = ColorRampInterpolation(alpha);
            }

            return lut;
        }



        /// <summary>
        /// Initializes pseudo-color look up table for depth pixels
        /// </summary>
        private static uint[] InitializePseudoColorLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                lut[i] = ColorRampInterpolation((float)i / TableSize);
            }

            return lut;
        }



        /// <summary>
        /// Maps a float value to a pseudo-color pixel
        /// </summary>
        private static uint ColorRampInterpolation(float value)
        {
            // Map value to surrounding indexes on the color ramp
            int rampSteps = ColorRamp.Length - 1;
            float scaled = value * rampSteps;
            int integer = (int)scaled;
            int index =
                integer < 0 ? 0 :
                integer >= rampSteps - 1 ? rampSteps - 1 :
                integer;

            Color prev = ColorRamp[index];
            Color next = ColorRamp[index + 1];

            // Set color based on ratio of closeness between the surrounding colors
            uint alpha = (uint)((scaled - integer) * 255);
            uint beta = 255 - alpha;
            return
                ((prev.A * beta + next.A * alpha) / 255) << 24 | // Alpha
                ((prev.R * beta + next.R * alpha) / 255) << 16 | // Red
                ((prev.G * beta + next.G * alpha) / 255) << 8 |  // Green
                ((prev.B * beta + next.B * alpha) / 255);        // Blue
        }


        /// <summary>
        /// Maps a value in [0, 1] to a pseudo RGBA color.
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]

        private static uint PseudoColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return PseudoColorTable[index];
        }

        #endregion

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit depth value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
        /// <param name="depthScale">Physical distance that corresponds to one unit in the input scanline.</param>
        /// <param name="minReliableDepth">Shortest distance at which the sensor can provide reliable measurements.</param>
        /// <param name="maxReliableDepth">Furthest distance at which the sensor can provide reliable measurements.</param>

        public static unsafe void PseudoColorForDepth(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes, float depthScale, float minReliableDepth, float maxReliableDepth)
        {
            // Visualize space in front of your desktop.
            float minInMeters = minReliableDepth * depthScale;
            float maxInMeters = maxReliableDepth * depthScale;
            float one_min = 1.0f / minInMeters;
            float range = 1.0f / maxInMeters - one_min;

            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                var depth = inputRow[x] * depthScale;

                if (depth == 0)
                {
                    // Map invalid depth values to transparent pixels.
                    // This happens when depth information cannot be calculated, e.g. when objects are too close.
                    outputRow[x] = 0;
                }
                else
                {
                    var alpha = (1.0f / depth - one_min) / range;
                    outputRow[x] = PseudoColor(alpha * alpha);
                }
            }
        }



        /// <summary>
        /// Maps each pixel in a scanline from a 8 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor8BitInfrared(
            int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            byte* inputRow = inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)Byte.MaxValue);
            }
        }

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor16BitInfrared(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)UInt16.MaxValue);
            }
        }
    }


    // Displays the provided softwareBitmap in a XAML image control.
    public void PresentSoftwareBitmap(SoftwareBitmap softwareBitmap)
    {
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }
}

MultiSourceMediaFrameReader を使用して複数のソースから時間相関フレームを取得する

Windows 10 Version 1607 以降では、MultiSourceMediaFrameReader を使って複数のソースから時間相関フレームを取得できます。 この API により、DepthCorrelatedCoordinateMapper クラスを使う場合など、複数のソースから時間的に近接するフレームを必要とする処理が簡単になります。 ただし、この新しいメソッドを使う場合の制限の 1 つとして、フレーム到着イベントは、最も低速なキャプチャ ソースに合わせて生成されるようになります。 高速なソースからの追加のフレームは取りこぼされます。 また、システムでは、さまざまなソースからさまざまな速度でフレームが到着するものと想定するため、ソースがフレームの生成を完全に停止したかどうかを自動的に認識することはできません。 このセクションのコード例では、イベントを使って独自のタイムアウト ロジックを作成する方法を示します。アプリで定義した制限時間内に相関フレームが到着しなかった場合、タイムアウトが発生します。

MultiSourceMediaFrameReader を使う手順は、この記事で既に説明した MediaFrameReader を使う手順と同様です。 この例では、カラー ソースと深度ソースを使います。 メディア フレーム ソース ID を格納する文字列変数をいくつか宣言します。これらの ID は、各ソースからのフレームを選択するために使われます。 次に、サンプルのタイムアウト ロジックを実装するために使う ManualResetEventSlimCancellationTokenSourceEventHandler を宣言します。

private MultiSourceMediaFrameReader _multiFrameReader = null;
private string _colorSourceId = null;
private string _depthSourceId = null;


private readonly ManualResetEventSlim _frameReceived = new ManualResetEventSlim(false);
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
public event EventHandler CorrelationFailed;

この記事で既に説明した手法を使って、このサンプル シナリオに必要なカラー ソースと深度ソースを含む MediaFrameSourceGroup を照会します。 目的のフレーム ソース グループを選択したら、各フレーム ソースの MediaFrameSourceInfo を取得します。

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];

MediaCapture オブジェクトを作成し、選択したフレーム ソース グループを初期化設定に渡して初期化します。

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};

await mediaCapture.InitializeAsync(settings);

MediaCapture オブジェクトを初期化したら、カラー カメラと深度カメラの MediaFrameSource オブジェクトを取得します。 各ソースの ID を格納して、対応するソースから到着したフレームを選択できるようにします。

MediaFrameSource colorSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Color);

MediaFrameSource depthSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Depth);

if (colorSource == null || depthSource == null)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture doesn't have the Color and Depth streams");
    return;
}

_colorSourceId = colorSource.Info.Id;
_depthSourceId = depthSource.Info.Id;

MultiSourceMediaFrameReader を作成して初期化します。そのためには、CreateMultiSourceFrameReaderAsync を呼び出して、リーダーで使用するフレーム ソースの配列を渡します。 FrameArrived イベントに対するイベント ハンドラーを登録します。 この例では、フレームを Image コントロールにレンダリングするために、この記事で既に説明した FrameRenderer ヘルパー クラスのインスタンスを作成します。 StartAsync を呼び出して、フレーム リーダーを開始します。

この例で既に宣言した CorellationFailed イベントに対するイベント ハンドラーを登録します。 使用中のメディア フレーム ソースのいずれかがフレームの生成を停止すると、このイベントが通知されます。 最後に、Task.Run を呼び出して、タイムアウト ヘルパー メソッドである NotifyAboutCorrelationFailure を別のスレッドで実行します。 このメソッドの実装は後で示します。

_multiFrameReader = await mediaCapture.CreateMultiSourceFrameReaderAsync(
    new[] { colorSource, depthSource });

_multiFrameReader.FrameArrived += MultiFrameReader_FrameArrived;

_frameRenderer = new FrameRenderer(imageElement);

MultiSourceMediaFrameReaderStartStatus startStatus =
    await _multiFrameReader.StartAsync();

if (startStatus != MultiSourceMediaFrameReaderStartStatus.Success)
{
    throw new InvalidOperationException(
        "Unable to start reader: " + startStatus);
}

this.CorrelationFailed += MainPage_CorrelationFailed;
Task.Run(() => NotifyAboutCorrelationFailure(_tokenSource.Token));

FrameArrived イベントは、MultiSourceMediaFrameReader で管理されているすべてのメディア フレーム ソースで新しいフレームが利用可能になったときに発生します。 つまりこのイベントは、最も低速なメディア ソースに合わせて発生することになります。 低速なソースでフレームが 1 つ生成される間に別のソースで複数のフレームが生成された場合、高速なフレームからの追加のフレームは取りこぼされます。

TryAcquireLatestFrame を呼び出して、イベントに関連付けられている MultiSourceMediaFrameReference を取得します。 TryGetFrameReferenceBySourceId を呼び出して、各メディア フレーム ソースに関連付けられている MediaFrameReference を取得します。引数には、フレーム リーダーの初期化時に格納した ID 文字列を渡します。

ManualResetEventSlim オブジェクトの Set メソッドを呼び出して、フレームが到着したことを通知します。 このイベントは、別のスレッドで実行中の NotifyCorrelationFailure メソッドでチェックされます。

最後に、時間相関メディア フレームに対して任意の処理を実行します。 この例では、深度ソースからのフレームを描画するだけです。

private void MultiFrameReader_FrameArrived(MultiSourceMediaFrameReader sender, MultiSourceMediaFrameArrivedEventArgs args)
{
    using (MultiSourceMediaFrameReference muxedFrame =
        sender.TryAcquireLatestFrame())
    using (MediaFrameReference colorFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_colorSourceId))
    using (MediaFrameReference depthFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_depthSourceId))
    {
        // Notify the listener thread that the frame has been received.
        _frameReceived.Set();
        _frameRenderer.ProcessFrame(depthFrame);
    }
}

NotifyCorrelationFailure ヘルパー メソッドは、フレーム リーダーの開始後に別のスレッドで実行ました。 このメソッドでは、フレーム受信イベントが通知されたかどうかをチェックします。 既に説明したとおり、このイベントは、相関フレームのセットが到着するたびに FrameArrived ハンドラーで設定されます。 アプリで定義した時間内 (適切な値は 5 秒程度) にイベントが通知されなかった場合、CancellationToken を使ってタスクが取り消されたのでなければ、いずれかのメディア フレーム ソースでフレームの読み取りが停止した可能性があります。 この場合、通常はフレーム リーダーをシャットダウンすることになります。そこで、アプリ定義の CorrelationFailed イベントを発生させます。 このイベントのハンドラーでフレーム リーダーを停止し、この記事で既に説明したように、関連付けられているリソースをクリーンアップします。

private void NotifyAboutCorrelationFailure(CancellationToken token)
{
    // If in 5 seconds the token is not cancelled and frame event is not signaled,
    // correlation is most likely failed.
    if (WaitHandle.WaitAny(new[] { token.WaitHandle, _frameReceived.WaitHandle }, 5000)
            == WaitHandle.WaitTimeout)
    {
        CorrelationFailed?.Invoke(this, EventArgs.Empty);
    }
}
private async void MainPage_CorrelationFailed(object sender, EventArgs e)
{
    await _multiFrameReader.StopAsync();
    _multiFrameReader.FrameArrived -= MultiFrameReader_FrameArrived;
    mediaCapture.Dispose();
    mediaCapture = null;
}

バッファー処理されたフレーム取得モードを使用して、取得したフレームのシーケンスを保持する

Windows 10、バージョン 1709 以降では、MediaFrameReader またはMultiSourceMediaFrameReaderAcquisitionMode プロパティを Buffered に設定することで、フレーム ソースからアプリに渡されたフレームのシーケンスを保持できます。

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

既定の取得モードである Realtime では、アプリがまだ前のフレームの FrameArrived イベントを処置している間に複数のフレームがソースから取得された場合、システムは取得された最新のフレームをアプリに送信し、バッファーで待機しているその他のフレームを破棄します。 これにより、アプリには利用可能な最新のフレームが常に提供されます。 これは、通常、リアルタイム コンピューター ビジョン アプリケーションで最も有用なモードです。

Buffered 取得モードでは、システムがすべてのフレームをバッファーに保持し、FrameArrived イベントを通じて、フレームを受け取った順にアプリに提供します。 注: このモードでは、システムのフレーム用バッファーがいっぱいになった場合、アプリが前のフレームの FrameArrived イベントを完了して、バッファー領域を解放するまで、システムは新しいフレームの取得を停止します。

MediaSource を使用して、MediaPlayerElement にフレームを表示する

Windows、バージョン 1709 以降では、MediaFrameReader から取得したフレームを XAML ページの MediaPlayerElement コントロールに直接表示できます。 これを行うには、MediaSource.CreateFromMediaFrameSource を使用して、MediaPlayerElement に関連付けられた MediaPlayer によって直接使用できる MediaSource オブジェクトを作成します。 MediaPlayerMediaPlayerElement の操作について詳しくは、「MediaPlayer を使ったオーディオとビデオの再生」をご覧ください。

次のコード例では、前面カメラからのフレームと背面カメラからのフレームを XAML ページに同時に表示する簡単な実装を示します。

まず、2 つの MediaPlayerElement コントロールを XAML ページに追加します。

<MediaPlayerElement x:Name="mediaPlayerElement1" Width="320" Height="240"/>
<MediaPlayerElement x:Name="mediaPlayerElement2" Width="320" Height="240"/>

次に、この記事で前のセクションで示した手法を使用して、前面パネルと背面パネル上のカラー カメラに対し、MediaFrameSourceInfo オブジェクトが含まれている MediaFrameSourceGroup を選択します。 なお、MediaPlayer は、カラー以外の形式 (深度データや赤外線データ) のフレームを自動的にカラー データには変換しません。 他の種類のセンサーを使用すると、予期しない結果が生じる場合があります。

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front
            && info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Back
            && info.SourceKind == MediaFrameSourceKind.Color)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with front and back-facing camera found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo frontSourceInfo = selectedGroup.SourceInfos[0];
MediaFrameSourceInfo backSourceInfo = selectedGroup.SourceInfos[1];

選択した MediaFrameSourceGroup を使うように MediaCapture オブジェクトを初期化します。

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

最後に、MediaSource.CreateFromMediaFrameSource を呼び出し、各フレーム ソースの MediaSource を作成します。これには関連付けられた MediaFrameSourceInfo オブジェクトの Id プロパティを使用して、MediaCapture オブジェクトの FrameSources コレクションでいずれか 1 つのフレーム ソースを選択します。 新しい MediaPlayer オブジェクトを初期化し、SetMediaPlayer を呼び出して、初期化したオブジェクトを MediaPlayerElement に割り当てます。 次に、Source プロパティを、新しく作成した MediaSource オブジェクトに設定します。

var frameMediaSource1 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[frontSourceInfo.Id]);
mediaPlayerElement1.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement1.MediaPlayer.Source = frameMediaSource1;
mediaPlayerElement1.AutoPlay = true;

var frameMediaSource2 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[backSourceInfo.Id]);
mediaPlayerElement2.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement2.MediaPlayer.Source = frameMediaSource2;
mediaPlayerElement2.AutoPlay = true;

ビデオ プロファイルを使用してフレーム ソースを選択する

MediaCaptureVideoProfile オブジェクトによって表されるカメラ プロファイルは、特定のキャプチャ デバイスに搭載されている機能のセットを表します。これらの機能には、フレーム レートや解像度のほか、HDR キャプチャなどの高度な機能が含まれます。 キャプチャ デバイスは、複数のプロファイルをサポートできるため、開発者は目的のキャプチャ シナリオに最適なプロファイルを選択できます。 Windows 10、バージョン 1803 以降では、MediaCaptureVideoProfile を使用して、特定の機能を備えたメディア フレーム ソースを選択した上で、MediaCapture オブジェクトを初期化できます。 次の例のメソッドは、HDR と広色域 (WCG) をサポートするビデオ プロファイルを探して、MediaCaptureInitializationSettings オブジェクトを返します。このオブジェクトを MediaCapture の初期化に使用することで、選択したデバイスとプロファイルを使用できるようになります。

まず、MediaFrameSourceGroup.FindAllAsync を呼び出して、現在のデバイス上で利用可能なすべてのメディア フレーム ソース グループの一覧を取得します。 ループ処理によって各ソース グループで MediaCapture.FindKnownVideoProfiles を呼び出し、現在のソース グループについて、指定したプロファイル (この例では HDR/WCG 写真) をサポートしているすべてのビデオ プロファイルの一覧を取得します。 条件に適合するプロファイルが見つかった場合、新しい MediaCaptureInitializationSettings オブジェクトが作成され、VideoProfile が選択したプロファイルに設定されると共に、VideoDeviceId が現在のメディア フレーム ソース グループの Id プロパティに設定されます。

public async Task<MediaCaptureInitializationSettings> FindHdrWithWcgPhotoProfile()
{
    IReadOnlyList<MediaFrameSourceGroup> sourceGroups = await MediaFrameSourceGroup.FindAllAsync();
    MediaCaptureInitializationSettings settings = null;

    foreach (MediaFrameSourceGroup sourceGroup in sourceGroups)
    {
        // Find a device that support AdvancedColorPhoto
        IReadOnlyList<MediaCaptureVideoProfile> profileList = MediaCapture.FindKnownVideoProfiles(
                                      sourceGroup.Id,
                                      KnownVideoProfile.HdrWithWcgPhoto);

        if (profileList.Count > 0)
        {
            settings = new MediaCaptureInitializationSettings();
            settings.VideoProfile = profileList[0];
            settings.VideoDeviceId = sourceGroup.Id;
            break;
        }
    }
    return settings;
}

private void StartDeviceWatcherButton_Click(object sender, RoutedEventArgs e)
{
    var remoteCameraHelper = new RemoteCameraPairingHelper(this.Dispatcher);
}

カメラ プロファイルの使用方法について詳しくは、「カメラ プロファイル」をご覧ください。