共用方式為


使用 MediaFrameReader 處理媒體畫面

本文說明如何使用 MediaFrameReader 搭配 MediaCapture,從一個或多個可用來源取得媒體畫面,包括色彩、深度和紅外線相機、音訊裝置,甚至還有產生骨架追蹤畫面的自定義畫面來源。 這項功能的設計供執行媒體畫面即時處理的應用程式使用,例如擴增實境和深度感知相機應用程式。

如果您只想要擷取視訊或相片,例如典型的攝影應用程式,您可能想要使用 MediaCapture 支援的其中一種其他擷取技術。 如需可用媒體擷取技術和文章的清單,其中顯示如何使用它們,請參閱 相機

備註

本文所討論的功能僅適用於從 Windows 10 版本 1607 開始。

備註

有一個通用 Windows 應用程式範例示範如何使用 MediaFrameReader 來顯示來自不同畫面來源的畫面,包括色彩、深度和紅外相機。 如需詳細資訊,請參閱 相機畫面範例

備註

Windows 10 版本 1803 引進了一組新的 API,用於與音訊資料一起使用 MediaFrameReader。 如需詳細資訊,請參閱 使用 MediaFrameReader 處理音訊畫面

選取畫面來源和畫面來源群組

處理媒體畫面的許多應用程式需要一次從多個來源取得畫面,例如裝置的色彩和深度相機。 MediaFrameSourceGroup 物件代表一組可以同時使用的媒體畫面來源。 呼叫靜態方法 MediaFrameSourceGroup.FindAllAsync ,以取得目前裝置所支援之所有畫面來源群組的清單。

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

您也可以使用 deviceInformation.CreateWatcher 建立 DeviceWatch er,以及從 mediaFrameSourceGroup.GetDeviceSele ctor 傳回的值,以在裝置上可用的畫面來源群組變更時接收通知,例如當外部相機插入時。 如需更多資訊,請參閱 裝置列表

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 物件轉換為具有兩個欄位的匿名物件:sourceGroup,用於表示群組本身,以及 colorSourceInfo,用於表示群組中的色彩畫面來源。 colorSourceInfo 欄位會設定為 firstOrDefault的結果,這會選取所提供述詞解析為 true 的第一個物件。 在此情況下,如果串流類型 VideoPreview,則述詞為 true,來源種類會 Color,如果相機位於裝置的前面板上,則為 true。

從上述查詢傳回的匿名物件清單中, 使用 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 物件的 MediaCaptureInitializationSettings 物件。 在此範例中,會使用下列設定:

  • SourceGroup - 這會告知系統您要用來取得畫面的來源群組。 請記住,來源群組會定義一組可同時使用的媒體畫面來源。
  • SharingMode - 這會告知系統您是否需要對擷取來源裝置進行獨佔控制。 如果您將此設定為 ExclusiveControl,表示您可以變更擷取裝置的設定,例如它所產生的畫面格格式,但這表示如果另一個應用程式已經有獨佔控件,當應用程式嘗試初始化媒體擷取裝置時,您的應用程式將會失敗。 如果您將此設定為 SharedReadOnly,即使畫面來源正被另一個應用程式使用,您仍可以從中接收畫面格,但無法變更這些裝置的設定。
  • MemoryPreference - 如果您指定 CPU,系統會使用 CPU 記憶體,保證在影格到達時,這些影格會以 SoftwareBitmap 物件的形式存在。 如果您指定 [自動],系統會動態選擇最佳的記憶體位置來儲存畫面。 如果系統選擇使用 GPU 記憶體,媒體畫面就會以 IDirect3DSurface 物件的形式抵達,而不是 SoftwareBitmap
  • StreamingCaptureMode - 將此設定為 Video ,表示不需要串流處理音訊。

呼叫 InitializeAsync,以使用您想要的設定初始化 MediaCapture。 如果初始化可能失敗,請務必在 `try` 區塊中呼叫這個。

m_mediaCapture = new MediaCapture();

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

設定框架來源的慣用格式

若要設定框架來源的慣用格式,您需要取得代表來源的 MediaFrameSource 物件。 您可以透過存取初始化的 MediaCapture 物件的 Frames 字典,並指定要使用的畫面來源標識符,來取得此物件。 這就是為什麼我們在選取畫面來源群組時儲存 MediaFrameSourceInfo 物件的原因。

MediaFrameSource.SupportedFormats 屬性包含一份 MediaFrameFormat 物件清單,描述畫面來源支援的格式。 在此範例中,選取格式的寬度為 1080 像素,而且可以提供 32 位 RGB 格式的畫面。 FirstOrDefault 擴充方法會選取清單中的第一個項目。 如果選取的格式為 Null,則框架來源不支援所要求的格式。 如果支援格式,您可以呼叫 SetFormatAsync 來要求來源使用此格式。

var colorFrameSource = m_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 m_mediaFrameReader;

在您初始化的 MediaCapture 物件上,通過呼叫 CreateFrameReaderAsync 來實例化框架讀取器。 此方法的第一個自變數是您要接收畫面的畫面來源。 您可以為每個您想要使用的畫面來源建立個別的畫面讀取器。 第二個自變數會告知系統您想要畫面到達的輸出格式。 這可讓您不必在影格到達時自行進行轉換。 請注意,如果您指定框架來源不支援的格式,則會擲回例外狀況,因此請確定此值位於 SupportedFormats 集合中。

建立幀讀取器之後,註冊 FrameArrived 事件的處理程式,每當來源有新的幀可用時,就會引發此事件。

使用呼叫 startAsync來指示系統從來源開始讀取框架。

m_mediaFrameReader = await m_mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
m_mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await m_mediaFrameReader.StartAsync();

處理畫面抵達事件

每當有新的畫面可用時,就會引發 MediaFrameReader.FrameArrived 事件。 您可以選擇處理抵達的每個畫面,或只在需要畫面時使用畫面。 因為框架讀取器會在自己的線程上引發事件,因此您可能需要實作一些同步處理邏輯,以確保您不會嘗試從多個線程存取相同的數據。 本節說明如何將繪圖色彩畫面同步處理至 XAML 頁面中的影像控件。 此案例可解決需要在UI線程上執行 XAML 控件之所有更新的額外同步處理條件約束。

在 XAML 中顯示畫面格的第一個步驟是建立影像控件。

<Image x:Name="iFrameReaderImageControl" MaxWidth="300" MaxHeight="200"/>

在您的程式後置頁面中,宣告一個類別成員變數,型態為 SoftwareBitmap,該變數將用作所有傳入影像的後臺緩衝區。 請注意,影像數據本身不會被複製,只會複製物件參考。 此外,請宣告布爾值,以追蹤UI作業目前是否正在執行。

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

由於框架會以 SoftwareBitmap 物件的形式抵達,因此您必須建立 SoftwareBitmapSource 物件,這可讓您使用 SoftwareBitmap 做為 XAML 控件的來源。 您應該先在程式代碼的某個位置設定影像來源,再啟動畫面讀取器。

iFrameReaderImageControl.Source = new SoftwareBitmapSource();

現在是時候實作 FrameArrived 事件處理程式了。 呼叫處理程式時, sender 參數會包含引發事件的 MediaFrameReader 對象的參考。 在此物件上呼叫 TryAcquireLatestFrame ,以嘗試取得最新的框架。 顧名思義, TryAcquireLatestFrame 可能無法成功傳回框架。 因此,當您存取 VideoMediaFrame 和 SoftwareBitmap 屬性時,請務必測試 null。 在這個範例中,Null 條件運算子「?」 用來存取 SoftwareBitmap ,然後檢查擷取的物件是否為 null。

Image 控制項只能以 BRGA8 格式顯示預先乘法或沒有 Alpha 的影像。 如果接收到的框架不是該格式,則會使用靜態方法 Convert,將軟體位圖轉換成正確的格式。

接下來,使用 Interlocked.Exchange 方法來交換到達位圖的參照與反緩衝區位圖。 在一個線程安全的原子操作中,這個方法會交換這些參考。 交換之後,現在位於 softwareBitmap 變數中的舊反緩衝區影像會被釋放,以清理其資源。

接下來,與 Image 元素相關聯的 CoreDispatcher 被用來透過呼叫 RunAsync來建立將在 UI 線程上執行的任務。 因為異步任務會在該任務中執行,因此傳給 Lambda 運算式RunAsync 會使用 async 關鍵詞宣告。

在工作中,會檢查 _taskRunning 變數,以確保一次只執行一個工作實例。 如果工作尚未執行, _taskRunning 會設定為 true,以防止工作再次執行。 在 迴圈中,呼叫 Interlocked.Exchange,將後緩衝區複製到暫存 SoftwareBitmap,直到後緩衝區影像為 null。 每次填入暫存位圖時,ImageSource 屬性會轉換成 SoftwareBitmapSource,然後呼叫 SetBitmapAsync 來設定影像的來源。

最後, _taskRunning 變數會設定回 false,以便在下次呼叫處理程式時再次執行工作。

備註

如果您存取 SoftwareBitmapDirect3DSurface 物件,這些物件由 VideoMediaFrame 屬性提供的 MediaFrameReference,系統就會對這些物件建立強式參考,這表示當您在包含這些物件的 MediaFrameReference上呼叫 Dispose 時,將不會釋放它們。 您必須明確地直接呼叫 Dispose 方法來對 SoftwareBitmapDirect3DSurface 物件進行立即處置。 否則,垃圾收集器最終會釋放這些物件的記憶體,但您不知道何時會發生此情況,而且如果配置的位圖或畫面數量超過系統允許的最大數量,新畫面的生成將會停止。 例如,您可以使用 SoftwareBitmap.Copy 方法來複製擷取的畫面,然後釋放原始畫面格以克服這項限制。 此外,如果您使用多載 CreateFrameReaderAsync 建立 MediaFrameReader(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype、Windows.Graphics.Imaging.BitmapSize outputSize)CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype),傳回的畫面會是原始畫面數據的複本,因此不會在保留畫面時造成畫面擷取停止。

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 = iFrameReaderImageControl.DispatcherQueue.TryEnqueue(
            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)iFrameReaderImageControl.Source;
                    await imageSource.SetBitmapAsync(latestBitmap);
                    latestBitmap.Dispose();
                }

                taskRunning = false;
            });
    }

    if (mediaFrameReference != null)
    {
        mediaFrameReference.Dispose();
    }
}

清理資源

當您完成讀取影像框架時,請務必透過呼叫 StopAsync來停止媒體框架讀取器,並取消註冊 FrameArrived 處理程式,最後處置 MediaCapture 物件。

await m_mediaFrameReader.StopAsync();
m_mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
m_mediaCapture.Dispose();
m_mediaCapture = null;

如需在應用程式暫停時清除媒體擷取對象的詳細資訊,請參閱 在 WinUI 3 應用程式中顯示相機預覽

FrameRenderer 輔助類別

本節提供一個完整的程式碼清單,包含一個輔助類別,可以讓您輕鬆地在應用程式中顯示來自色彩、紅外線和深度來源的畫面框架。 一般而言,除了將深度和紅外數據顯示到螢幕之外,您會想要執行更多動作,但此協助程式類別是示範畫面讀取器功能並偵錯您自己的畫面讀取器實作的實用工具。 本節中的程式代碼會根據 相機畫面範例進行調整。

FrameRenderer 協助程式類別會實作下列方法。

  • FrameRenderer 建構函式 - 此建構函式會初始化協助類別,以便使用您傳入的 XAML Image 元素來顯示媒體畫面。
  • ProcessFrame - 此方法會在您傳入建構函式的 Image 元素中顯示一個媒體畫面,該畫面由 MediaFrameReference所表示。 您通常應該從 FrameArrived 事件處理程式呼叫這個方法,並傳入 TryAcquireLatestFrame 所傳回的框架。
  • ConvertToDisplayableImage - 此方法會檢查媒體畫面的格式,並視需要將它轉換成可顯示的格式。 對於彩色影像,這表示確定色彩格式為 BGRA8,且點陣圖 Alpha 模式為預乘。 針對深度或紅外畫面,會處理每個掃描線,以將深度或紅外值轉換成 psuedocolor 漸層,並使用範例中也包含的 PsuedoColorHelper 類別,並列於下列範例中。

備註

若要對 SoftwareBitmap 影像執行圖元作,您必須存取原生記憶體緩衝區。 若要這樣做,您必須使用下列程式代碼清單中所包含的 IMemoryBufferByteAccess COM 介面,而且您必須更新專案屬性,以允許編譯不安全的程式代碼。 如需詳細資訊,請參閱 建立、編輯和儲存位圖影像

[GeneratedComInterface]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe partial interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}
class FrameRenderer
{
    private Image m_imageElement;
    private SoftwareBitmap m_backBuffer;
    private bool _taskRunning = false;

    public FrameRenderer(Image imageElement)
    {
        m_imageElement = imageElement;
        m_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 m_backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref m_backBuffer, softwareBitmap);

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

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = m_imageElement.DispatcherQueue.TryEnqueue(
                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 m_backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)m_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 m_backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref m_backBuffer, softwareBitmap);

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

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = m_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 m_backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)m_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }
}

使用 MultiSourceMediaFrameReader 從多個來源取得時間同步的畫面

從 Windows 10 版本 1607 開始,您可以使用 MultiSourceMediaFrameReader 從多個來源接收時間相互關聯的畫面。 此 API 讓您更輕鬆地執行需要來自多個來源且時間鄰近的畫面幀的處理,例如使用 DepthCorrelatedCoordinateMapper 類別。 使用這個新方法的其中一個限制是,畫面到達的事件只會以最慢的擷取來源速率提高。 來自速度較快來源的額外畫面格將會被丟棄。 此外,由於系統預期畫面格會以不同的速率從不同的來源抵達,因此不會自動辨識來源是否已完全停止產生畫面。 本節中的範例程式代碼示範如何使用 事件來建立您自己的逾時邏輯,如果相互關聯的畫面未到達應用程式定義的時間限制,就會叫用該邏輯。

使用 MultiSourceMediaFrameReader 的步驟與本文先前所述的使用 MediaFrameReader 的步驟類似。 此範例會使用色彩來源和深度來源。 宣告幾個字串變數來儲存用於從每個來源選取畫面的媒體框架來源ID。 接下來,宣告一個 ManualResetEventSlim、一個 CancellationTokenSource,以及一個 EventHandler,這些將用於為範例實作逾時邏輯。

private MultiSourceMediaFrameReader m_multiFrameReader = null;
private string m_colorSourceId = null;
private string m_depthSourceId = null;


private readonly ManualResetEventSlim m_frameReceived = new ManualResetEventSlim(false);
private readonly CancellationTokenSource m_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 物件,並在初始化設定中傳遞選取的畫面來源群組。

m_mediaCapture = new MediaCapture();

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

await m_mediaCapture.InitializeAsync(settings);

初始化 MediaCapture 對象後,請擷取色彩和深度相機的 MediaFrameSource 物件。 儲存每個來源的標識碼,以便選取對應來源的抵達畫面。

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

MediaFrameSource depthSource =
    m_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;
}

m_colorSourceId = colorSource.Info.Id;
m_depthSourceId = depthSource.Info.Id;

呼叫方法 CreateMultiSourceFrameReaderAsync,並傳遞一個包含讀取器將使用的框架來源的數組,以建立和初始化 MultiSourceMediaFrameReader。 註冊 FrameArrived 事件的事件處理程式。 本範例會建立 FrameRenderer 輔助類別的實例,如本文先前所述,將框架渲染到 Image 控件中。 呼叫 StartAsync 來啟動框架讀取器。

為在範例中先前宣告的 CorellationFailed 事件註冊事件處理程式。 如果使用的其中一個媒體畫面來源會停止產生畫面,我們會發出此事件訊號。 最後,在個別線程上呼叫 Task.Run 來呼叫逾時協助方法 NotifyAboutCorrelationFailure。 本文稍後會顯示此方法的實作。

m_multiFrameReader = await m_mediaCapture.CreateMultiSourceFrameReaderAsync(
    new[] { colorSource, depthSource });

m_multiFrameReader.FrameArrived += MultiFrameReader_FrameArrived;

m_frameRenderer = new FrameRenderer(iFrameReaderImageControl);

MultiSourceMediaFrameReaderStartStatus startStatus =
    await m_multiFrameReader.StartAsync();

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

this.CorrelationFailed += MainWindow_CorrelationFailed;
Task.Run(() => NotifyAboutCorrelationFailure(m_tokenSource.Token));

只要 MultiSourceMediaFrameReader管理的所有媒體畫面來源有新的畫面可用,就會觸發 FrameArrived 事件。 這表示事件會以最慢媒體來源的速率引發。 如果某個來源在較慢的來源產生一個畫面時產生多個畫面,則會丟棄來自快速來源的多餘的畫面。

取得與事件相關的 MultiSourceMediaFrameReference,請呼叫 tryAcquireLatestFrame。 呼叫 TryGetFrameReferenceBySourceId,以取得與每個媒體畫面來源相關聯的 MediaFrameReference,並傳入初始化框架讀取器時儲存的標識符字串。

呼叫 ManualResetEventSlim 物件的 Set 方法,以發出畫面已送達的訊號。 我們將在個別線程中執行的 NotifyCorrelationFailure 方法中檢查此事件。

最後,對時間相互關聯的媒體畫面執行任何處理。 此範例只會顯示來自深度來源的框架。

private void MultiFrameReader_FrameArrived(MultiSourceMediaFrameReader sender, MultiSourceMediaFrameArrivedEventArgs args)
{
    using (MultiSourceMediaFrameReference muxedFrame =
        sender.TryAcquireLatestFrame())
    using (MediaFrameReference colorFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(m_colorSourceId))
    using (MediaFrameReference depthFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(m_depthSourceId))
    {
        // Notify the listener thread that the frame has been received.
        m_frameReceived.Set();
        m_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, m_frameReceived.WaitHandle }, 5000)
            == WaitHandle.WaitTimeout)
    {
        CorrelationFailed?.Invoke(this, EventArgs.Empty);
    }
}
private async void MainWindow_CorrelationFailed(object sender, EventArgs e)
{
    await m_multiFrameReader.StopAsync();
    m_multiFrameReader.FrameArrived -= MultiFrameReader_FrameArrived;
    m_mediaCapture.Dispose();
    m_mediaCapture = null;
}

使用緩衝畫面擷取模式來保留已取得畫面的序列

從 Windows 10 版本 1709 開始,您可以 設定 MediaFrameReaderMultiSourceMediaFrameReaderBuffered 的 AcquisitionMode 属性,以保留從畫面來源傳遞至 app 的畫面序列。

m_mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

在預設的擷取模式中,Realtime,如果您的 app 仍在處理上一個畫面格 FrameArrived 事件時,從來源取得多個畫面格,則系統會傳送應用程式最近取得的畫面格,並在緩衝區中卸除等待的其他畫面。 這可讓您的應用程式隨時提供最新的可用畫面。 這通常是即時電腦視覺應用程式最有用的模式。

緩衝 擷取模式中,系統會保存在緩衝區中的所有畫面,並會按收到的順序,透過 FrameArrived 事件提供給您的應用程式。 請注意,在此模式中,當系統的畫面緩衝區滿時,系統會停止取得新的畫面框,直到您的應用程式完成先前畫面框的 FrameArrived 事件為止,從而釋放緩衝區中的更多空間。

使用 MediaSource 在 MediaPlayerElement 中顯示畫面

從 Windows 版本 1709 開始,您可以直接在 XAML 頁面的 MediaPlayerElement 控制項中,顯示從 MediaFrameReader 取得的影格。 這是使用 MediaSource.CreateFromMediaFrameSource 來建立 MediaSource 物件,而該物件可以直接由與 MediaPlayerElement 相關聯的 MediaPlayer 使用。 如需使用 MediaPlayerMediaPlayerElement的詳細資訊,請參閱 使用 MediaPlayer 播放音訊和視訊

下列程式代碼範例示範簡單的實作,該實作會在 XAML 頁面中同時顯示來自正面和反向相機的畫面格。

首先,將兩個 MediaPlayerElement 控件新增至 XAML 頁面。

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

接下來,使用本文前面各節中所示的技術,選取 MediaFrameSourceGroup,其中包含用於前面板和後面板彩色相機的 MediaFrameSourceInfo 物件。 請注意,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];

初始化 MediaCapture 物件,使用所選擇的 MediaFrameSourceGroup

m_mediaCapture = new MediaCapture();

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

最後,呼叫 MediaSource.CreateFromMediaFrameSource,透過使用關聯的 MediaFrameSourceInfo 物件的 Id 屬性,從 MediaCapture 物件的 FrameSources 集合中選取一個框架來源,為每個畫面來源建立 MediaSource。 初始化新的 MediaPlayer 物件,然後呼叫 SetMediaPlayer將它指派給 MediaPlayerElement。 然後將 Source 屬性設定為新建立的 MediaSource 物件。

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

var frameMediaSource2 = MediaSource.CreateFromMediaFrameSource(m_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 物件。 下列範例程式代碼會尋找支援使用Wide Color Gamut (WCG) HDR的視訊配置檔,並傳回 MediaCaptureInitializationSettings 物件,可用來初始化 MediaCapture 以使用選取的裝置和配置檔。

首先,呼叫 MediaFrameSourceGroup.FindAllAsync,以取得目前裝置上所有可用的媒體畫面來源群組清單。 對每個來源群組進行迴圈操作,並呼叫 MediaCapture.FindKnownVideoProfiles,以獲取符合指定配置的目前來源群組的所有視訊配置文件清單,此案例中為支援具有 WCG 相片的 HDR。 如果找到符合準則的配置檔,請建立一個新的 MediaCaptureInitializationSettings 物件,然後將 VideoProfile 設定為所選的配置檔,並將 VideoDeviceId 設定為當前媒體框架來源群組的 Id 屬性。

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;
    }
}

如需使用相機配置檔的詳細資訊,請參閱 相機配置檔