Procesar fotogramas multimedia con MediaFrameReader

En este artículo se muestra cómo usar mediaFrameReader con MediaCapture para obtener fotogramas multimedia de uno o varios orígenes disponibles, como cámaras de color, profundidad e infrarrojos, dispositivos de audio o incluso orígenes de fotogramas personalizados, como aquellos que producen fotogramas de seguimiento esqueléticos. Esta característica se diseñó para que la usen las aplicaciones que realizan procesamiento en tiempo real de fotogramas multimedia, como las aplicaciones de realidad aumentada y las de cámara con reconocimiento de profundidad.

Si, simplemente, estás interesado en capturar vídeo o fotos, como una aplicación típica de fotografía, es probable que quieras usar una de las otras técnicas de captura que admite MediaCapture. Para obtener una lista de las técnicas de captura de elementos multimedia disponibles y artículos donde se muestra cómo usarlos, consulta Cámara.

Nota:

Las características descritas en este artículo solo están disponibles a partir de Windows 10, versión 1607.

Nota:

Hay una muestra de aplicación universal de Windows que muestra el uso de MediaFrameReader para mostrar fotogramas de distintos orígenes de fotogramas, lo que incluye cámaras a color, de profundidad y de infrarrojos. Para obtener más información, consulta Camera frames sample (Muestra de fotogramas de cámara).

Nota:

Se introdujo un nuevo conjunto de API para usar MediaFrameReader con datos de audio en Windows 10, versión 1803. Para obtener más información, vea Procesar fotogramas de audio con MediaFrameReader.

Configurar tu proyecto

Al igual que con cualquier aplicación que use MediaCapture, debes declarar que tu aplicación usa la funcionalidad cámara web antes de intentar acceder a cualquier dispositivo de cámara. Si la aplicación captura desde un dispositivo de audio, también debes declarar la funcionalidad micrófono del dispositivo.

Agregar funcionalidades al manifiesto de la aplicación

  1. En Microsoft Visual Studio, en el Explorador de soluciones, abre el diseñador para el manifiesto de la aplicación haciendo doble clic en el elemento package.appxmanifest.
  2. Seleccione la pestaña Funcionalidades.
  3. Active la casilla webcam y la casilla micrófono.
  4. Para obtener acceso a la biblioteca de imágenes y vídeos, marca las casillas de Biblioteca de imágenes y de Biblioteca de vídeos.

En el código de ejemplo de este artículo se usan las API de los siguientes espacios de nombres, además de las que se incluyen con la plantilla de proyecto predeterminada.

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;

Seleccionar orígenes de fotogramas y grupos de orígenes de fotogramas

Muchas aplicaciones que procesan los fotogramas multimedia necesitan obtener fotogramas de distintos orígenes al mismo tiempo, como cámaras de profundidad y de color de un dispositivo. El objeto MediaFrameSourceGroup representa un conjunto de orígenes de fotogramas multimedia que se pueden usar simultáneamente. Llama al método estático MediaFrameSourceGroup.FindAllAsync para obtener una lista de todos los grupos de orígenes de fotogramas que admite el dispositivo actual.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

También puedes crear un DeviceWatcher mediante DeviceInformation.CreateWatcher y el valor devuelto de MediaFrameSourceGroup.GetDeviceSelector para recibir notificaciones cuando cambien los grupos de orígenes de fotogramas disponibles en el dispositivo, como cuando se conecta una cámara externa. Para obtener más información, consulta Enumerar dispositivos.

Una clase MediaFrameSourceGroup tiene una colección de objetos MediaFrameSourceInfo que describen los orígenes de fotogramas que se incluyen en el grupo. Después de recuperar los grupos de origen de fotogramas disponibles en el dispositivo, puedes seleccionar el grupo que expone los orígenes de fotogramas que te interesan.

El siguiente ejemplo muestra la forma más sencilla de seleccionar un grupo de orígenes de fotogramas. Este código, simplemente, recorre todos los grupos disponibles y, a continuación, recorre cada elemento de la colección SourceInfos. Cada objeto MediaFrameSourceInfo se comprueba para ver si admite las características que nos interesan. En este caso, se comprueba el valor VideoPreview de la propiedad MediaStreamType, lo que significa que el dispositivo proporciona una secuencia de vista previa de vídeo, y se comprueba el valor Color de la propiedad SourceKind, que indica que el origen proporciona fotogramas de 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;
    }
}

Este método para identificar el grupo de orígenes de fotogramas y los orígenes de fotogramas deseados funciona con casos sencillos, pero si quieres seleccionar orígenes de fotogramas por criterios más complejos, puede volverse más complicado. Otro método es usar la sintaxis de Linq y objetos anónimos para realizar la selección. En el siguiente ejemplo se usa el método de extensión Select para transformar los objetos MediaFrameSourceGroup de la lista frameSourceGroups en un objeto anónimo con dos campos: sourceGroup, que representa el grupo en sí mismo, y colorSourceInfo, que representa el origen del fotograma de color en el grupo. El campo colorSourceInfo se establece en el resultado del elemento FirstOrDefault, que selecciona el primer objeto para el que se resuelve el predicado proporcionado en true. En este caso, el predicado es true si el tipo de secuencia es VideoPreview, el tipo de origen es Color y la cámara está en el panel frontal del dispositivo.

De la lista de objetos anónimos que devuelve la consulta que se describió anteriormente, el método de extensión Where se usa para seleccionar solo los objetos en los que el campo colorSourceInfo no es nulo. Por último, se llama al objeto FirstOrDefault para seleccionar el primer elemento de la lista.

Ahora puedes usar los campos del objeto seleccionado para obtener referencias de los objetos MediaFrameSourceGroup y MediaFrameSourceInfo que representan la cámara a color. Se usarán más tarde para inicializar el objeto MediaCapture y crear una clase MediaFrameReader para el origen seleccionado. Por último, debes comprobar si el grupo de orígenes es nulo, lo que significa que el dispositivo actual no tiene los orígenes de captura solicitados.

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

En el siguiente ejemplo se usa una técnica similar, como la que se describió anteriormente, para seleccionar un grupo de orígenes que contiene cámaras a color, de profundidad y de infrarrojos.

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

Nota:

A partir de Windows 10, versión 1803, puedes usar la clase MediaCaptureVideoProfile para seleccionar un origen de fotogramas multimedia con un conjunto de funcionalidades deseadas. Para obtener más información, consulte la sección Uso de perfiles de vídeo para seleccionar un origen de fotogramas más adelante en este artículo.

Inicializar el objeto MediaCapture para usar el grupo de orígenes de fotogramas seleccionado

El siguiente paso es inicializar el objeto MediaCapture para usar el grupo de orígenes de fotogramas seleccionado en el paso anterior.

El objeto MediaCapture se usa normalmente desde varias ubicaciones dentro de la aplicación, por lo que debes declarar una variable de miembro de clase para contenerlo.

MediaCapture mediaCapture;

Para crear una instancia del objeto MediaCapture, llama al constructor. A continuación, cree un objeto MediaCaptureInitializationSettings que se usará para inicializar el objeto MediaCapture . En este ejemplo, se usan los siguientes valores:

  • SourceGroup: indica al sistema qué grupo de orígenes usarás para obtener fotogramas. Recuerda que el grupo de orígenes define un conjunto de orígenes de fotogramas multimedia que se pueden usar simultáneamente.
  • SharingMode: indica al sistema si necesitas un control exclusivo de los dispositivos de origen de captura. Si estableces esto en ExclusiveControl, significa que puedes cambiar la configuración del dispositivo de captura, como el formato de los fotogramas que genera, pero esto significa que si otra aplicación ya tiene control exclusivo, se producirá un error en la aplicación cuando intente inicializar el dispositivo de captura multimedia. Si lo estableces en SharedReadOnly, puede recibir fotogramas de los orígenes de fotogramas incluso si otra aplicación los usa, pero no puedes cambiar la configuración de los dispositivos.
  • MemoryPreference: si especificas CPU, el sistema usará la memoria de la CPU que garantiza que, cuando lleguen fotogramas, estarán disponibles como objetos SoftwareBitmap. Si especificas Auto, el sistema elegirá dinámicamente la ubicación de memoria óptima para almacenar los fotogramas. Si el sistema decide usar la memoria de GPU, los fotogramas multimedia llegarán como un objeto IDirect3DSurface y no como softwareBitmap.
  • StreamingCaptureMode: establécelo en Video para indicar que no es necesario transmitir el audio.

Llama al método InitializeAsync para inicializar la clase MediaCapture con la configuración que quieras. Asegúrate de llamar a este método dentro de un bloque try por si se produce un error de inicialización.

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

Establecer el formato preferido en el origen de fotogramas

Para establecer el formato preferido en un origen de fotogramas, debes obtener un objeto MediaFrameSource que represente el origen. Para obtener el objeto, accede al diccionario de la propiedad Frames del objeto MediaCapture y especifica el identificador del origen de fotogramas que quieres usar. Por este motivo guardamos el objeto MediaFrameSourceInfo mientras seleccionábamos un grupo de orígenes de fotogramas.

La propiedad MediaFrameSource.SupportedFormats contiene una lista de objetos MediaFrameFormat que describen los formatos admitidos para el origen de fotogramas. Usa el método de extensión de Linq Where para seleccionar un formato basado en las propiedades que quieras. En este ejemplo, se selecciona un formato que tiene un ancho de 1080 píxeles y puede suministrar fotogramas en un formato RGB de 32 bits. El método de extensión FirstOrDefault selecciona la primera entrada de la lista. Si el formato seleccionado es nulo, el origen de fotogramas no admitirá el formato solicitado. Si se admite el formato, puedes solicitar que el origen use este formato llamando al objeto 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);

Crear un lector de fotogramas para el origen de fotogramas

Para recibir fotogramas de un origen de fotogramas multimedia, usa una clase MediaFrameReader.

MediaFrameReader mediaFrameReader;

Para crear una instancia del lector de fotogramas, llama al método CreateFrameReaderAsync en el objeto MediaCapture inicializado. El primer argumento de este método es el origen de fotogramas desde el que quieres recibir fotogramas. Puedes crear un lector de fotogramas distinto para cada origen de fotogramas que quieras usar. El segundo argumento indica al sistema el formato de salida en el que quieres que lleguen los fotogramas. Esto puede ahorrarte tener que realizar tus propias conversiones a fotogramas a medida que llegan. Ten en cuenta que, si especificas un formato que no es compatible con el origen de fotogramas, se generará una excepción, así que asegúrate de que el valor esté en la colección SupportedFormats.

Después de crear el lector de fotogramas, registra un controlador para el evento FrameArrived que se genera siempre que un nuevo fotograma está disponible desde el origen.

Indica al sistema que comience a leer fotogramas desde el origen con una llamada al método StartAsync.

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

Controlar el evento de llegada de fotogramas

El evento MediaFrameReader.FrameArrived se genera siempre que un nuevo fotograma esté disponible. Puedes elegir procesar cada fotograma que llegue o usar solo fotogramas cuando los necesites. Ya que el lector de fotogramas genera el evento en su propio subproceso, es posible que tengas que implementar una lógica de sincronización para asegurarte de que no intentas acceder a los mismos datos desde varios subprocesos. En esta sección se muestra cómo sincronizar dibujando fotogramas de color en un control de imagen de una página XAML. Este escenario trata la restricción de sincronización adicional que necesita que todas las actualizaciones de los controles XAML se realicen en el subproceso de la interfaz de usuario.

El primer paso para mostrar fotogramas en XAML es crear un control de imagen.

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

En la página de código subyacente, declara una variable de miembro de clase de tipo SoftwareBitmap que se usará como un búfer de reserva al que se copiarán todas las imágenes entrantes. Ten en cuenta que no se copian los datos de imagen, solo las referencias de objeto. Además, declara un valor booleano para comprobar si la operación de interfaz de usuario todavía se está ejecutando.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

Ya que los fotogramas llegarán como objetos SoftwareBitmap, debes crear un objeto SoftwareBitmapSource que te permita usar un objeto SoftwareBitmap como el origen de un Control XAML. Debes establecer el origen de imagen en algún lugar del código antes de iniciar el lector de fotogramas.

imageElement.Source = new SoftwareBitmapSource();

Ahora es el momento de implementar el controlador de eventos FrameArrived. Cuando se llama al controlador, el parámetro sender contiene una referencia al objeto MediaFrameReader que generó el evento. Llama al método TryAcquireLatestFrame en este objeto para intentar obtener el fotograma más reciente. Como su nombre indica, es posible que el método TryAcquireLatestFrame no consiga devolver correctamente un fotograma. Por lo tanto, cuando accedes a las propiedades VideoMediaFrame y SoftwareBitmap, asegúrate de probar que no son nulas. En este ejemplo, el operador condicional nulo ? se usa para acceder a la propiedad SoftwareBitmap y, a continuación, se comprueba que el objeto recuperado no sea nulo.

El control Image solo puede mostrar imágenes en formato BRGA8 con valores alfa premultiplicados o sin valores alfa. Si el fotograma de llegada no está en ese formato, el método estático Convert se usa para convertir el mapa de bits de software al formato correcto.

A continuación, se usa el método Interlocked.Exchange para intercambiar la referencia del mapa de bits de llegada con el mapa de bits del búfer de reserva. Este método intercambia estas referencias en una operación atómica segura para subprocesos. Después del intercambio, se desecha la imagen anterior del búfer de reserva, ubicada ahora en la variable softwareBitmap, para limpiar los recursos.

A continuación, se usa la clase CoreDispatcher asociada al elemento Image para crear una tarea que se ejecutará en el subproceso de la interfaz de usuario mediante una llamada al método RunAsync. Ya que las tareas asincrónicas se realizarán dentro de la tarea, la expresión lambda que se pasa al método RunAsync se declara con la palabra clave async.

Dentro de la tarea, se comprueba la variable _taskRunning para asegurarse de que solo se ejecuta una instancia de la tarea a la vez. Si ya no se está ejecutando la tarea, la variable _taskRunning se establece en true para impedir que se ejecute de nuevo la tarea. En un bucle while, se llama al método Interlocked.Exchange para copiar desde el búfer de reserva a una propiedad temporal SoftwareBitmap hasta que la imagen del búfer de reserva sea nula. Cada vez que se rellena el mapa de bits temporal, la propiedad Source de la clase Image se convierte en una propiedad SoftwareBitmapSource y luego se llama al método SetBitmapAsync para establecer el origen de la imagen.

Por último, la variable _taskRunning se vuelve a establecer en false para que la tarea se pueda ejecutar de nuevo la próxima vez que se llame al controlador.

Nota:

Si accedes a los objetos SoftwareBitmap o Direct3DSurface proporcionados por la propiedad VideoMediaFrame de una clase MediaFrameReference, el sistema crea una referencia fuerte a estos objetos, lo que significa que no se eliminarán cuando se llamae a Dispose en la clase MediaFrameReference contenedora. Se debe llamar explícitamente al método Dispose de SoftwareBitmap o Direct3DSurface directamente para los objetos que deben eliminarse inmediatamente. De lo contrario, el recolector de elementos no usados al final liberará la memoria de estos objetos, pero no se puede saber cuando ocurrirá, y si el número de superficies o mapas de bits asignados supera la cantidad máxima permitida por el sistema, el nuevo flujo de fotogramas se detendrá. Puede copiar fotogramas recuperados, mediante el método SoftwareBitmap.Copy , por ejemplo, y luego liberar los marcos originales para superar esta limitación. Además, si crea mediaFrameReader mediante la sobrecarga CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) o CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype), los fotogramas devueltos son copias de los datos de fotogramas originales y, por lo tanto, no hacen que la adquisición de fotogramas se detenga cuando se conserven.

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

Limpieza de recursos

Cuando hayas terminado de leer fotogramas, asegúrate de detener el lector de fotogramas multimedia con una llamada al método StopAsync. Después, elimina el controlador FrameArrived y desecha el objeto MediaCapture.

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

Para obtener más información sobre cómo limpiar los objetos de captura multimedia cuando se suspende la aplicación, consulta Acceso fácil a la vista previa de cámara.

La clase auxiliar FrameRenderer

Camera frames sample (Muestra de fotogramas de cámara) universal de Windows proporciona una clase auxiliar que facilita mostrar los fotogramas de orígenes a color, de infrarrojos y en profundidad en la aplicación. Normalmente, querrás hacer más cosas con los datos en profundidad y de infrarrojos que solo mostrarlos en la pantalla, pero esta clase auxiliar es una herramienta útil para demostrar la característica de lector de fotogramas y para depurar tu propia implementación del lector de fotogramas.

La clase auxiliar FrameRenderer implementa los métodos siguientes.

  • Constructor FrameRenderer: el constructor inicializa la clase auxiliar para usar el elemento Image de XAML que pasas para mostrar los fotogramas multimedia.
  • ProcessFrame: este método muestra un fotograma multimedia, que se representa con una clase MediaFrameReference, en el elemento Image que pasaste al constructor. Normalmente, debes llamar a este método desde el controlador de eventos FrameArrived y pasar el fotograma que devuelve el método TryAcquireLatestFrame.
  • ConvertToDisplayableImage: este método comprueba el formato del fotograma multimedia y, si fuera necesario, lo convierte a un formato que se pueda mostrar. Para las imágenes a color, esto significa asegurarse de que el formato de color sea BGRA8 y que el modo alfa del mapa de bits sea premultiplicado. Para fotogramas de infrarrojos o en profundidad, se procesa cada línea de digitalización para convertir los valores de infrarrojos o en profundidad a un degradado psuedocolor, con la clase PsuedoColorHelper que también está incluida en la muestra y se indica a continuación.

Nota

Para manipular píxeles en imágenes SoftwareBitmap, debes acceder a un búfer de memoria nativo. Para ello, debes usar la interfaz COM IMemoryBufferByteAccess incluida en la siguiente lista de códigos y actualizar las propiedades del proyecto para permitir la compilación del código no seguro. Para obtener más información, consulta Creación de imágenes.

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

Uso de MultiSourceMediaFrameReader para obtener fotogramas con núcleo temporal de varios orígenes

A partir de Windows 10, versión 1607, puedes usar MultiSourceMediaFrameReader para recibir fotogramas con núcleo temporal de varios orígenes. Esta API facilita el procesamiento que requiere fotogramas de varios orígenes que se tomaron en proximidad temporal cercana, como el uso de la clase DepthCorrelatedCoordinateMapper . Una limitación del uso de este nuevo método es que los eventos llegados a fotogramas solo se generan a la velocidad del origen de captura más lento. Se quitarán fotogramas adicionales de orígenes más rápidos. Además, dado que el sistema espera que los fotogramas lleguen de diferentes orígenes a diferentes velocidades, no reconoce automáticamente si un origen ha dejado de generar fotogramas por completo. El código de ejemplo de esta sección muestra cómo usar un evento para crear su propia lógica de tiempo de espera que se invoque si los fotogramas correlacionados no llegan dentro de un límite de tiempo definido por la aplicación.

Los pasos para usar MultiSourceMediaFrameReader son similares a los pasos para usar MediaFrameReader descrito anteriormente en este artículo. En este ejemplo se usará un origen de color y un origen de profundidad. Declare algunas variables de cadena para almacenar los identificadores de origen de fotogramas multimedia que se usarán para seleccionar fotogramas de cada origen. A continuación, declare un ManualResetEventSlim, CancellationTokenSource y un EventHandler que se usará para implementar la lógica de tiempo de espera del ejemplo.

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;

Con las técnicas descritas anteriormente en este artículo, consulte una clase MediaFrameSourceGroup que incluya los orígenes de color y profundidad necesarios para este escenario de ejemplo. Después de seleccionar el grupo de origen de fotogramas deseado, obtenga MediaFrameSourceInfo para cada origen de fotogramas.

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

Cree e inicialice un objeto MediaCapture , pasando el grupo de origen de fotogramas seleccionado en la configuración de inicialización.

mediaCapture = new MediaCapture();

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

await mediaCapture.InitializeAsync(settings);

Después de inicializar el objeto MediaCapture , recupere los objetos MediaFrameSource para las cámaras de color y profundidad. Almacene el identificador de cada origen para que pueda seleccionar el marco de llegada para el origen correspondiente.

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;

Cree e inicialice MultiSourceMediaFrameReader llamando a CreateMultiSourceFrameReaderAsync y pasando una matriz de orígenes de fotogramas que usará el lector. Registre un controlador de eventos para el evento FrameArrived . En este ejemplo se crea una instancia de la clase auxiliar FrameRenderer , descrita anteriormente en este artículo, para representar fotogramas en un control Image . Inicie el lector de fotogramas llamando a StartAsync.

Registre un controlador de eventos para el evento CorellationFailed declarado anteriormente en el ejemplo. Señalaremos este evento si uno de los orígenes de fotogramas multimedia que se usan deja de producir fotogramas. Por último, llame a Task.Run para llamar al método auxiliar de tiempo de espera , NotifyAboutCorrelationFailure, en un subproceso independiente. La implementación de este método se muestra más adelante en este artículo.

_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));

El evento FrameArrived se genera cada vez que hay un nuevo fotograma disponible en todos los orígenes de fotogramas multimedia administrados por MultiSourceMediaFrameReader. Esto significa que el evento se generará en la cadencia del origen de medios más lento. Si un origen genera varios fotogramas en el tiempo en que un origen más lento genera un fotograma, se quitarán los fotogramas adicionales del origen rápido.

Obtenga la clase MultiSourceMediaFrameReference asociada al evento mediante una llamada a TryAcquireLatestFrame. Obtenga la clase MediaFrameReference asociada a cada origen de fotogramas multimedia mediante una llamada a TryGetFrameReferenceBySourceId, pasando las cadenas de identificador almacenadas cuando se inicializó el lector de fotogramas.

Llame al método Set del objeto ManualResetEventSlim para indicar que han llegado fotogramas. Comprobaremos este evento en el método NotifyCorrelationFailure que se ejecuta en un subproceso independiente.

Por último, realice cualquier procesamiento en los fotogramas multimedia correlacionados con el tiempo. En este ejemplo simplemente se muestra el marco del origen de profundidad.

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

El método auxiliar NotifyCorrelationFailure se ejecutó en un subproceso independiente después de iniciar el lector de fotogramas. En este método, compruebe si se ha señalado el evento de fotograma recibido. Recuerde que, en el controlador FrameArrived , establecemos este evento cada vez que llega un conjunto de fotogramas correlacionados. Si el evento no se ha señalado durante algún período de tiempo definido por la aplicación ( 5 segundos es un valor razonable) y la tarea no se canceló mediante CancellationToken, es probable que uno de los orígenes de fotogramas multimedia haya dejado de leer fotogramas. En este caso, normalmente quiere apagar el lector de fotogramas, por lo que genera el evento CorrelationFailed definido por la aplicación. En el controlador de este evento, puede detener el lector de fotogramas y limpiar los recursos asociados, como se muestra anteriormente en este artículo.

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

Usar el modo de adquisición de fotogramas almacenados en búfer para conservar la secuencia de fotogramas adquiridos

A partir de Windows 10, versión 1709, puedes establecer la propiedad AcquisitionMode de mediaFrameReader o MultiSourceMediaFrameReader en Buffered para conservar la secuencia de fotogramas pasados a la aplicación desde el origen de fotogramas.

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

En el modo de adquisición predeterminado, Realtime, si se adquieren varios fotogramas del origen mientras la aplicación sigue controlando el evento FrameArrived para un fotograma anterior, el sistema enviará a la aplicación el fotograma adquirido más recientemente y quitará fotogramas adicionales en espera en el búfer. Esto proporciona a la aplicación el fotograma disponible más reciente en todo momento. Normalmente, este es el modo más útil para las aplicaciones computer vision en tiempo real.

En el modo de adquisición almacenado en búfer , el sistema mantendrá todos los fotogramas en el búfer y los proporcionará a la aplicación a través del evento FrameArrived en el orden recibido. Ten en cuenta que en este modo, cuando se rellena el búfer del sistema para fotogramas, el sistema dejará de adquirir nuevos fotogramas hasta que la aplicación complete el evento FrameArrived para fotogramas anteriores, liberando más espacio en el búfer.

Usar MediaSource para mostrar fotogramas en mediaPlayerElement

A partir de Windows, versión 1709, puedes mostrar fotogramas adquiridos desde mediaFrameReader directamente en un control MediaPlayerElement en tu página XAML. Esto se logra mediante el uso de MediaSource.CreateFromMediaFrameSource para crear un objeto MediaSource que puede usar directamente un objeto MediaPlayer asociado a un MediaPlayerElement. Para obtener información detallada sobre cómo trabajar con MediaPlayer y MediaPlayerElement, consulta Reproducir audio y vídeo con MediaPlayer.

En los ejemplos de código siguientes se muestra una implementación sencilla que muestra los fotogramas de una cámara frontal y trasera simultáneamente en una página XAML.

En primer lugar, agrega dos controles MediaPlayerElement a tu página XAML.

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

A continuación, con las técnicas que se muestran en las secciones anteriores de este artículo, seleccione un mediaFrameSourceGroup que contenga objetos MediaFrameSourceInfo para cámaras de color en el panel frontal y el panel posterior. Ten en cuenta que MediaPlayer no convierte automáticamente fotogramas de formatos que no sean de color, como datos de profundidad o infrarrojos, en datos de color. El uso de otros tipos de sensores puede producir resultados inesperados.

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

Inicialice el objeto MediaCapture para usar el objeto MediaFrameSourceGroup seleccionado.

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

Por último, llame a MediaSource.CreateFromMediaFrameSource para crear un objeto MediaSource para cada origen de fotogramas mediante la propiedad Id del objeto MediaFrameSourceInfo asociado para seleccionar uno de los orígenes de fotogramas de la colección FrameSources del objeto MediaCapture. Inicialice un nuevo objeto MediaPlayer y asígnelo a un objeto MediaPlayerElement llamando a SetMediaPlayer. A continuación, establezca la propiedad Source en el objeto MediaSource recién creado.

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;

Usar perfiles de vídeo para seleccionar un origen de fotogramas

Un perfil de cámara, representado por un objeto MediaCaptureVideoProfile , representa un conjunto de funcionalidades que proporciona un dispositivo de captura determinado, como velocidades de fotogramas, resoluciones o características avanzadas como la captura HDR. Un dispositivo de captura puede admitir varios perfiles, lo que le permite seleccionar el que está optimizado para su escenario de captura. A partir de Windows 10, versión 1803, puedes usar MediaCaptureVideoProfile para seleccionar un origen de fotograma multimedia con funcionalidades concretas antes de inicializar el objeto MediaCapture . El siguiente método de ejemplo busca un perfil de vídeo que admita HDR con la gama de colores anchos (WCG) y devuelve un objeto MediaCaptureInitializationSettings que se puede usar para inicializar MediaCapture para usar el dispositivo y el perfil seleccionados.

En primer lugar, llame a MediaFrameSourceGroup.FindAllAsync para obtener una lista de todos los grupos de orígenes de fotogramas multimedia disponibles en el dispositivo actual. Recorra cada grupo de origen y llame a MediaCapture.FindKnownVideoProfiles para obtener una lista de todos los perfiles de vídeo del grupo de origen actual que admiten el perfil especificado, en este caso HDR con foto WCG. Si se encuentra un perfil que cumple los criterios, cree un nuevo objeto MediaCaptureInitializationSettings y establezca VideoProfile en el perfil de selección y VideoDeviceId en la propiedad Id del grupo de origen de fotogramas multimedia actual.

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

Para obtener más información sobre el uso de perfiles de cámara, consulte Perfiles de cámara.