使用 MediaFrameReader 处理媒体帧

本文介绍如何将 MediaFrameReaderMediaCapture 结合使用,以获取一个或多个可用源提供的媒体帧,这些可用源包括彩色、深度、红外线相机,音频设备,甚至是自定义的帧源(例如生成骨架跟踪帧的帧源)。 此功能旨在由执行实时处理媒体帧的应用使用,例如增强现实和感知深度的相机应用。

如果你对仅捕获视频或照片感兴趣(例如典型的摄影应用),则可能希望使用 MediaCapture 支持的其他捕获技术。 有关可用的媒体捕获技术列表和演示如何使用这些技术的文章,请参阅相机

注意

本文中讨论的功能仅从 Windows 10 版本 1607 开始提供。

注意

文中提供了一个通用 Windows 应用示例,介绍使用 MediaFrameReader 显示不同帧源(包括彩色、深度和红外相机)的帧。 有关详细信息,请参阅相机帧示例

注意

Windows 10 版本 1803 中采用了一组新的 API,借助其可将 MediaFrameReader 与音频数据结合使用。 有关详细信息,请参阅使用 MediaFrameReader 处理音频帧

设置项目

对于使用 MediaCapture 的任何应用,在尝试访问任何相机设备前都必须声明应用使用 webcam 功能。 如果应用从音频设备捕获音频,还应声明 microphone 设备功能。

将功能添加到应用清单

  1. 在 Microsoft Visual Studio 的“解决方案资源管理器”中,通过双击“package.appxmanifest”项,打开应用程序清单的设计器。
  2. 选择“功能”选项卡。
  3. 选中“摄像头”框和“麦克风”框。
  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.CreateWatcher 和从 MediaFrameSourceGroup.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 对象转换为具有两个字段的匿名对象:sourceGroup 表示组本身,colorSourceInfo 表示组中的颜色帧源。 colorSourceInfo 字段设为 FirstOrDefault 的结果,后者用于选择所提供的谓词解析为 true 的第一个对象。 在本例中,如果流类型为 VideoPreview、源种类为 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 mediaCapture;

通过调用构造函数创建 MediaCapture 对象的实例。 接下来,创建用于初始化 MediaCapture 对象的 MediaCaptureInitializationSettings 对象。 本示例使用以下设置:

  • SourceGroup - 告知系统用于获取帧的源组。 请记住,源组定义可以同时使用的一组媒体帧源。
  • SharingMode - 告知系统是否需要捕获源设备的独占控制权。 如果将它设置为 ExclusiveControl,意味着可以更改捕获设备的设置(例如它生成的帧格式),但也意味着如果其他应用已经拥有独占控制权,你的应用在尝试初始化媒体捕获设备时会失败。 如果将它设置为 SharedReadOnly,则可以检索帧源的帧,即使其他应用已经在使用这些帧也是如此,但无法更改设备设置。
  • MemoryPreference - 如果指定 CPU,系统将使用可以保证到达的帧能够用作 SoftwareBitmap 对象的 CPU 内存。 如果指定 Auto,系统将动态选择存储帧的最佳内存位置。 如果系统选择使用 GPU 内存,媒体帧将作为 IDirect3DSurface 对象(而非 SoftwareBitmap 对象)到达。
  • 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,实例化帧阅读器。 此方法的第一个参数是你希望从中接收帧的帧源。 可以为每个要使用的帧源创建单独的帧阅读器。 第二个参数告知系统你希望帧到达时使用的输出格式。 这可以省去在帧到达时需要自行转换到帧的工作。 请注意,如果所指定的格式不受帧源支持,将引发异常,因此请确保该值在 SupportedFormats 集合中。

创建帧阅读器后,请为源提供新帧时引发的 FrameArrived 事件注册处理程序。

通过调用 StartAsync 指示系统开始读取源的帧。

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

处理帧到达事件

新帧可用时即引发 MediaFrameReader.FrameArrived 事件。 可以选择处理到达的每个帧,或者仅在需要时使用它们。 因为帧阅读器在自己的线程上引发事件,所以可能需要实现某些同步逻辑,以确保你不会尝试从多个线程访问相同数据。 本节介绍如何在 XAML 页面中将图形颜色帧同步到图像控件。 这套方案解决需要在 UI 线程上执行 XAML 控件的所有更新的其他同步约束。

若要在 XAML 中显示帧,第一步是创建 Image 控件。

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

在代码隐藏页面中,声明 SoftwareBitmap 类型的类成员变量,该变量用作所有传入图像都会复制到的后台缓冲区。 请注意,图像数据本身不会复制,复制的仅是对象引用。 此外,要声明一个布尔值以跟踪 UI 操作当前是否正在运行。

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

由于帧将作为 SoftwareBitmap 对象到达,因此需要创建 SoftwareBitmapSource 对象,该对象支持将 SoftwareBitmap 用作 XAML Control 的源。 在启动帧阅读器前,应在代码的某个位置设置图像源。

imageElement.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 线程上运行的任务。 由于异步任务将在任务中执行,因此传递到 RunAsync 的 lambda 表达式使用 async 关键字声明。

在任务中,查看 _taskRunning 变量以确保一次只运行任务的一个实例。 如果尚未运行该任务,将 _taskRunning 设置为 true,以防止任务再次运行。 在 while 循环中,调用 Interlocked.Exchange 以将后台缓冲区的内容复制到临时 SoftwareBitmap,直到后台缓冲区图像为 null。 每次在填充临时位图时,ImageSource 属性转换为 SoftwareBitmapSource,然后调用 SetBitmapAsync 以设置图像源。

最后,_taskRunning 变量重新设置为 false,以便下次调用处理程序时任务会重新运行。

注意

如果访问 SoftwareBitmapDirect3DSurface 对象(由 MediaFrameReferenceVideoMediaFrame 属性提供),系统将创建对这些对象的强引用,这意味着,当在包含的 MediaFrameReference 上调用 Dispose 时,它们将不会释放。 必须直接为要立即释放的对象显式调用 SoftwareBitmapDirect3DSurfaceDispose 方法。 否则,垃圾回收器将最终为这些对象释放内存,但无法知道这将何时出现,并且如果分配的位图或曲面的数量超过系统所允许的最大量,将停止新帧的流程。 可以拷贝检索的帧(如使用 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,并且位图 alpha 模式已预乘。 对于深度或红外帧,每个扫描行都经过处理,以使用也包含在示例中并且在下面列出的 PsuedoColorHelper 类将深度或红外值转换为伪色渐变。

注意

为了在 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 版本 1607 开始,你可以使用 MultiSourceMediaFrameReader 从多个源接收与时间关联的帧。 此 API 便于更加轻松地执行在接近时间获取的多个源的帧的处理,例如使用 DepthCorrelatedCoordinateMapper 类。 使用此新方法的一个限制是,只有以最慢的捕获源速率,才能引发帧到达事件。 来自更快的源的额外帧将被丢弃。 此外,由于系统预计来自不同源的帧以不同的速率到达,因此它不会自动识别一个源是否已完全停止生成帧。 此部分的示例代码显示如何使用事件自行创建在关联的帧在应用定义的时间限制内未到达时调用的超时逻辑。

使用 MultiSourceMediaFrameReader 的步骤与本文前面所述的使用 MediaFrameReader 的步骤类似。 此示例中将使用色源和深度源。 声明一些字符串变量以存储将被用来从每个源选择帧的媒体帧源 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;

通过调用 CreateMultiSourceFrameReaderAsync 和传递阅读器将使用的一组帧源创建并初始化 MultiSourceMediaFrameReader。 为 FrameArrived 事件注册事件处理程序。 此示例创建本文前面所述的 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));

每当由 MultiSourceMediaFrameReader 管理的所有媒体帧源都提供可用的新帧时,引发 FrameArrived 事件。 这意味着事件将按照最慢的媒体源的节奏引发。 如果一个源在较慢的源生成一个帧的时间内生成多个帧,则来自快源的额外帧将被丢弃。

通过调用 TryAcquireLatestFrame 获取与事件关联的 MultiSourceMediaFrameReference。 通过调用 TryGetFrameReferenceBySourceId 获取与每个媒体帧源相关联的 MediaFrameReference,同时在初始化帧阅读器时在存储的 ID 字符串中传递。

调用 ManualResetEventSlim 对象的设置 方法以发出帧已到达的信号。 我们将在单独的线程中运行的 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 开始,可以将 MediaFrameReaderMultiSourceMediaFrameReaderAcquisitionMode 属性设置为 Buffered 来保留从帧源传入应用的帧的序列。

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

在默认采集模式 Realtime 下,当应用仍在处理前一帧的 FrameArrived 事件时,如果从源采集了多个帧,系统将向应用发送最新采集的帧并删除在缓冲区中等待的其他帧。 这样可以始终向应用提供最新可用的帧。 对于实时计算机视觉应用程序,这通常是最有用的模式。

Buffered 采集模式下,系统将所有帧保留在缓冲区中,并按照帧的接收顺序,通过 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"/>

然后,使用前文中所介绍的技术为前置和后置彩色相机选择包含 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];

初始化 MediaCapture 对象以使用所选的 MediaFrameSourceGroup

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

最后,通过使用关联的 MediaFrameSourceInfo 对象的 Id 属性,在 MediaCapture 对象的 FrameSources 集合中选择一个帧源,来调用 MediaSource.CreateFromMediaFrameSource,为每个帧源创建一个 MediaSource。 通过调用 SetMediaPlayer 初始化新的 MediaPlayer 对象并将其分配给 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) 的视频配置文件,然后返回可用于初始化 MediaCaptureMediaCaptureInitializationSettings 对象,以使用所选的设备和配置文件。

首先,通过调用 MediaFrameSourceGroup.FindAllAsync 获取当前设备上可用的所有媒体帧源组的列表。 循环访问每个源组,并通过调用 MediaCapture.FindKnownVideoProfiles 获取当前源组中支持特定配置文件(本例中为带 WCG 照片的 HDR)的所有视频配置文件的列表。 如果找到符合条件的配置文件,则新建一个 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);
}

有关使用相机配置文件的详细信息,请参阅相机配置文件