Compartilhar via


Processar quadros de mídia com o MediaFrameReader

Este artigo mostra como usar um MediaFrameReader com MediaCapture para obter quadros de mídia de uma ou mais fontes disponíveis, incluindo câmeras de cor, profundidade e infravermelho, dispositivos de áudio ou até mesmo fontes de quadro personalizadas, como aquelas que produzem quadros de rastreamento esqueléticos. Esse recurso foi criado para ser usado por aplicativos que executam processamento em tempo real de quadros de mídia, como aplicativos de câmera com reconhecimento de profundidade e realidade aumentada.

Se você estiver interessado em simplesmente capturar vídeos ou fotos, como um aplicativo de fotografia típico, provavelmente desejará usar uma das outras técnicas de captura suportadas pelo MediaCapture. Para obter uma lista de técnicas de captura de mídia disponíveis e artigos mostrando como usá-las, consulte Câmera.

Observação

Os recursos discutidos neste artigo só estão disponíveis a partir do Windows 10, versão 1607.

Observação

Há um exemplo de aplicativo Universal do Windows que demonstra o uso de MediaFrameReader para exibir quadros de diferentes fontes de quadros, incluindo cores, profundidade e câmeras infravermelhas. Para obter mais informações, consulte Exemplo de quadros de câmera.

Observação

Um novo conjunto de APIs para usar MediaFrameReader com dados de áudio foi introduzido no Windows 10, versão 1803. Para saber mais, confira Processar quadros de áudio com o MediaFrameReader.

Configurar seu projeto

Assim como acontece com qualquer aplicativo que usa MediaCapture, você deve declarar que seu aplicativo usa o recurso de webcam antes de tentar acessar qualquer dispositivo de câmera. Se o aplicativo for capturar de um dispositivo de áudio, você também deverá declarar a funcionalidade do dispositivo de microfone .

Adicionar funcionalidades ao manifesto do aplicativo

  1. No Microsoft Visual Studio, no Gerenciador de Soluções, abra o designer do manifesto do aplicativo clicando duas vezes no item package.appxmanifest.
  2. Selecione a guia Funcionalidades.
  3. Marque a caixa da Webcam e a caixa do Microfone.
  4. Para acessar a biblioteca de Imagens e Vídeos, marque as caixas Biblioteca de Imagens e a caixa Biblioteca de Vídeos.

O código de exemplo neste artigo usa APIs dos namespaces a seguir, além daqueles incluídos pelo modelo de projeto padrão.

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;

Selecionar fontes de quadros e grupos de fontes de quadros

Muitos aplicativos que processam quadros de mídia precisam obter quadros de várias fontes ao mesmo tempo, como câmeras coloridas e de profundidade de um dispositivo. O objeto MediaFrameSourceGroup representa um conjunto de fontes de quadro de mídia que podem ser usadas simultaneamente. Chame o método estático MediaFrameSourceGroup.FindAllAsync para obter uma lista de todos os grupos de fontes de quadro compatíveis com o dispositivo atual.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

Você também pode criar um DeviceWatcher usando DeviceInformation.CreateWatcher e o valor retornado de MediaFrameSourceGroup.GetDeviceSelector para receber notificações quando os grupos de origem de quadros disponíveis no dispositivo forem alterados, como quando uma câmera externa estiver conectada. Para obter mais informações, consulte Enumerar dispositivos.

Um MediaFrameSourceGroup tem uma coleção de objetos MediaFrameSourceInfo que descrevem as fontes de quadro incluídas no grupo. Depois de recuperar os grupos de origem de quadros disponíveis no dispositivo, você pode selecionar o grupo que expõe as fontes de quadros nas quais está interessado.

O exemplo a seguir mostra a maneira mais simples de selecionar um grupo de origem de quadros. Esse código simplesmente executa um loop em todos os grupos disponíveis e, em seguida, executa um loop em cada item na coleção SourceInfos. Cada MediaFrameSourceInfo é verificado para ver se ele suporta os recursos que estamos procurando. Nesse caso, a propriedade MediaStreamType é verificada para o valor VideoPreview, o que significa que o dispositivo fornece um fluxo de visualização de vídeo, e a propriedade SourceKind é verificada para o valor Color, indicando que a origem fornece quadros coloridos.

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

Esse método de identificar o grupo de fontes de quadros desejado e as fontes de quadros funciona para casos simples, mas se você quiser selecionar fontes de quadros com base em critérios mais complexos, ele pode rapidamente se tornar complicado. Outro método é usar a sintaxe Linq e objetos anônimos para fazer a seleção. O exemplo a seguir usa o método de extensão Select para transformar os objetos MediaFrameSourceGroup na lista frameSourceGroups em um objeto anônimo com dois campos: sourceGroup, que representa o próprio grupo, e colorSourceInfo, que representa a origem do quadro de cor no grupo. O campo colorSourceInfo é definido como o resultado de FirstOrDefault, que seleciona o primeiro objeto para o qual o predicado fornecido é resolvido como true. Nesse caso, o predicado será true se o tipo de fluxo for VideoPreview, o tipo de origem for Color e se a câmera estiver no painel frontal do dispositivo.

Na lista de objetos anônimos retornados da consulta descrita acima, o método de extensão Where é usado para selecionar apenas os objetos em que o campo colorSourceInfo não é nulo. Por fim, FirstOrDefault é chamado para selecionar o primeiro item da lista.

Agora você pode usar os campos do objeto selecionado para obter referências ao MediaFrameSourceGroup selecionado e ao objeto MediaFrameSourceInfo que representa a câmera colorida. Eles serão usados posteriormente para inicializar o objeto MediaCapture e criar um MediaFrameReader para a origem selecionada. Por fim, você deve testar para ver se o grupo de origem é nulo, o que significa que o dispositivo atual não tem as fontes de captura solicitadas.

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

O exemplo a seguir usa uma técnica semelhante à descrita acima para selecionar um grupo de origem que contém câmeras de cor, profundidade e infravermelho.

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

Observação

A partir do Windows 10, versão 1803, você pode usar a classe MediaCaptureVideoProfile para selecionar uma fonte de quadro de mídia com um conjunto de recursos desejados. Para obter mais informações, consulte a seção Usar perfis de vídeo para selecionar uma origem de quadro mais adiante neste artigo.

Inicialize o objeto MediaCapture para usar o grupo de origem de quadros selecionado

A próxima etapa é inicializar o objeto MediaCapture para usar o grupo de origem de quadros selecionado na etapa anterior.

O objeto MediaCapture normalmente é usado em vários locais dentro do seu aplicativo, portanto, você deve declarar uma variável de membro de classe para mantê-lo.

MediaCapture mediaCapture;

Crie uma instância do objeto MediaCapture chamando o construtor. Em seguida, crie um objeto MediaCaptureInitializationSettings que será usado para inicializar o objeto MediaCapture . Neste exemplo, as seguintes configurações são usadas:

  • SourceGroup - Informa ao sistema qual grupo de origem você usará para obter quadros. Lembre-se de que o grupo de origem define um conjunto de fontes de quadro de mídia que podem ser usadas simultaneamente.
  • SharingMode - Isso informa ao sistema se você precisa de controle exclusivo sobre os dispositivos de origem de captura. Se você definir isso como ExclusiveControl, isso significa que você pode alterar as configurações do dispositivo de captura, como o formato dos quadros que ele produz, mas isso significa que, se outro aplicativo já tiver controle exclusivo, seu aplicativo falhará ao tentar inicializar o dispositivo de captura de mídia. Se você definir isso como SharedReadOnly, poderá receber quadros das fontes de quadros, mesmo que eles estejam em uso por outro aplicativo, mas não poderá alterar as configurações dos dispositivos.
  • MemoryPreference - Se você especificar CPU, o sistema usará a memória da CPU, o que garante que, quando os quadros chegarem, eles estarão disponíveis como objetos SoftwareBitmap. Se você especificar Automático, o sistema escolherá dinamicamente o local de memória ideal para armazenar quadros. Se o sistema optar por usar a memória GPU, os quadros de mídia chegarão como um objeto IDirect3DSurface e não como um SoftwareBitmap.
  • StreamingCaptureMode – Defina isso como Vídeo para indicar que o áudio não precisa ser transmitido.

Chame InitializeAsync para inicializar o MediaCapture com as configurações desejadas. Certifique-se de chamar isso dentro de um bloco try caso a inicialização falhe.

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

Definir o formato preferencial para a origem do quadro

Para definir o formato preferencial para uma origem de quadro, você precisa obter um objeto MediaFrameSource que represente a origem. Você obtém esse objeto acessando o dicionário Frames do objeto MediaCapture inicializado, especificando o identificador da fonte de quadro que deseja usar. É por isso que salvamos o objeto MediaFrameSourceInfo quando estávamos selecionando um grupo de origem de quadros.

A propriedade MediaFrameSource.SupportedFormats contém uma lista de objetos MediaFrameFormat que descrevem os formatos com suporte para a origem do quadro. Use o método de extensão Where Linq para selecionar um formato com base nas propriedades desejadas. Neste exemplo, é selecionado um formato que tem uma largura de 1080 pixels e pode fornecer quadros no formato RGB de 32 bits. O método de extensão FirstOrDefault seleciona a primeira entrada na lista. Se o formato selecionado for nulo, o formato solicitado não será suportado pela origem do quadro. Se houver suporte para o formato, você poderá solicitar que a origem use esse formato chamando 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);

Criar um leitor de quadros para a origem do quadro

Para receber quadros para uma fonte de quadro de mídia, use um MediaFrameReader.

MediaFrameReader mediaFrameReader;

Instancie o leitor de quadros chamando CreateFrameReaderAsync em seu objeto MediaCapture inicializado. O primeiro argumento para esse método é a origem do quadro da qual você deseja receber quadros. Você pode criar um leitor de quadros separado para cada fonte de quadros que deseja usar. O segundo argumento informa ao sistema o formato de saída no qual você deseja que os quadros cheguem. Isso pode evitar que você tenha que fazer suas próprias conversões em quadros à medida que eles chegam. Observe que, se você especificar um formato que não é compatível com a origem do quadro, uma exceção será lançada, portanto, certifique-se de que esse valor esteja na coleção SupportedFormats.

Depois de criar o leitor de quadros, registre um manipulador para o evento FrameArrived que é gerado sempre que um novo quadro está disponível na origem.

Diga ao sistema para começar a ler quadros da origem chamando StartAsync.

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

Manipular o evento de chegada do quadro

O evento MediaFrameReader.FrameArrived é gerado sempre que um novo quadro está disponível. Você pode optar por processar todos os quadros que chegam ou usar quadros apenas quando precisar deles. Como o leitor de quadros gera o evento em seu próprio thread, talvez seja necessário implementar alguma lógica de sincronização para garantir que você não esteja tentando acessar os mesmos dados de vários threads. Esta seção mostra como sincronizar quadros de cores de desenho com um controle de imagem em uma página XAML. Esse cenário aborda a restrição de sincronização adicional que exige que todas as atualizações dos controles XAML sejam executadas no thread da interface do usuário.

A primeira etapa na exibição de quadros em XAML é criar um controle Image.

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

Na página code-behind, declare uma variável de membro de classe do tipo SoftwareBitmap que será usada como um buffer traseiro para o qual todas as imagens de entrada serão copiadas. Observe que os dados da imagem em si não são copiados, apenas as referências de objeto. Além disso, declare um booleano para rastrear se nossa operação de interface do usuário está em execução no momento.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

Como os quadros chegarão como objetos SoftwareBitmap, você precisa criar um objeto SoftwareBitmapSource que permita usar um SoftwareBitmap como a origem de um controle XAML. Você deve definir a origem da imagem em algum lugar do código antes de iniciar o leitor de quadros.

imageElement.Source = new SoftwareBitmapSource();

Agora é hora de implementar o manipulador de eventos FrameArrived . Quando o manipulador é chamado, o parâmetro sender contém uma referência ao objeto MediaFrameReader que gerou o evento. Chame TryAcquireLatestFrame neste objeto para tentar obter o quadro mais recente. Como o nome indica, TryAcquireLatestFrame pode não ter êxito em retornar um quadro. Portanto, ao acessar as propriedades VideoMediaFrame e, em seguida, SoftwareBitmap, certifique-se de testar se há nulo. Neste exemplo, o operador condicional nulo ? é usado para acessar o SoftwareBitmap e, em seguida, o objeto recuperado é verificado quanto a nulo.

O controle Image só pode exibir imagens no formato BRGA8 com pré-multiplicado ou sem alfa. Se o quadro de chegada não estiver nesse formato, o método estático Convert será usado para converter o bitmap do software para o formato correto.

Em seguida, o método Interlocked.Exchange é usado para trocar a referência de bitmap de chegada pelo bitmap de backbuffer. Esse método troca essas referências em uma operação atômica que é thread-safe. Após a troca, a antiga imagem de backbuffer, agora na variável softwareBitmap , é descartada para limpar seus recursos.

Em seguida, o CoreDispatcher associado ao elemento Image é usado para criar uma tarefa que será executada no thread da interface do usuário chamando RunAsync. Como as tarefas assíncronas serão executadas dentro da tarefa, a expressão lambda passada para RunAsync é declarada com a palavra-chave async .

Dentro da tarefa, a variável _taskRunning é verificada para garantir que apenas uma instância da tarefa esteja em execução por vez. Se a tarefa ainda não estiver em execução, _taskRunning será definido como true para impedir que a tarefa seja executada novamente. Em um loop while, Interlocked.Exchange é chamado para copiar do backbuffer para um SoftwareBitmap temporário até que a imagem do backbuffer seja nula. Para cada vez que o bitmap temporário é preenchido, a propriedade Source da Image é convertida em um SoftwareBitmapSource e, em seguida, SetBitmapAsync é chamado para definir a origem da imagem.

Por fim, a variável _taskRunning é definida de volta como false para que a tarefa possa ser executada novamente na próxima vez que o manipulador for chamado.

Observação

Se você acessar os objetos SoftwareBitmap ou Direct3DSurface fornecidos pela propriedade VideoMediaFrame de um MediaFrameReference, o sistema criará uma referência forte a esses objetos, o que significa que eles não serão descartados quando você chamar Dispose no MediaFrameReference que o contém. Você deve chamar explicitamente o método Dispose do SoftwareBitmap ou Direct3DSurface diretamente para que os objetos sejam descartados imediatamente. Caso contrário, o coletor de lixo acabará liberando a memória para esses objetos, mas você não poderá saber quando isso ocorrerá e, se o número de bitmaps ou superfícies alocados exceder a quantidade máxima permitida pelo sistema, o fluxo de novos quadros será interrompido. Você pode copiar quadros recuperados, usando o método SoftwareBitmap.Copy , por exemplo, e liberar os quadros originais para superar essa limitação. Além disso, se você criar o MediaFrameReader usando a sobrecarga CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) ou CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype), os quadros retornados serão cópias dos dados do quadro original e, portanto, não farão com que a aquisição de quadros seja interrompida quando forem Retidos.

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

Recursos de limpeza

Quando terminar de ler quadros, certifique-se de interromper o leitor de quadros de mídia chamando StopAsync, cancelando o registro do manipulador FrameArrived e descartando o objeto MediaCapture.

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

Para obter mais informações sobre como limpar objetos de captura de mídia quando seu aplicativo está suspenso, consulte Exibir a visualização da câmera.

A classe auxiliar FrameRenderer

O exemplo de quadros de Câmera Universal do Windows fornece uma classe auxiliar que facilita a exibição dos quadros de fontes de cores, infravermelho e profundidade em seu aplicativo. Normalmente, você desejará fazer algo mais com dados de profundidade e infravermelho do que apenas exibi-los na tela, mas essa classe auxiliar é uma ferramenta útil para demonstrar o recurso de leitor de quadros e para depurar sua própria implementação de leitor de quadros.

A classe auxiliar FrameRenderer implementa os métodos a seguir.

  • Construtor FrameRenderer – o construtor inicializa a classe auxiliar para usar o elemento XAML Image que você passa para exibir quadros de mídia.
  • ProcessFrame - Esse método exibe um quadro de mídia, representado por um MediaFrameReference, no elemento Image que você passou para o construtor. Normalmente, você deve chamar esse método do manipulador de eventos FrameArrived, passando o quadro retornado por TryAcquireLatestFrame.
  • ConvertToDisplayableImage - Este método verifica o formato do quadro de mídia e, se necessário, converte-o em um formato exibível. Para imagens coloridas, isso significa garantir que o formato de cor seja BGRA8 e que o modo alfa de bitmap seja pré-multiplicado. Para quadros de profundidade ou infravermelho, cada linha de varredura é processada para converter os valores de profundidade ou infravermelho em um gradiente psuedocolor, usando a classe PsuedoColorHelper que também está incluída no exemplo e listada abaixo.

Observação

Para fazer a manipulação de pixels em imagens SoftwareBitmap , você deve acessar um buffer de memória nativo. Para fazer isso, você deve usar a interface COM IMemoryBufferByteAccess incluída na listagem de código abaixo e deve atualizar as propriedades do projeto para permitir a compilação de código não seguro. Para obter mais informações, consulte Criar, editar e salvar imagens de bitmap.

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

Use MultiSourceMediaFrameReader para obter quadros com correlação de tempo de várias fontes

A partir do Windows 10, versão 1607, você pode usar MultiSourceMediaFrameReader para receber quadros com correlação de tempo de várias fontes. Essa API facilita o processamento que requer quadros de várias fontes que foram obtidos em estreita proximidade temporal, como o uso da classe DepthCorrelatedCoordinateMapper. Uma limitação do uso desse novo método é que os eventos de quadro chegados são gerados apenas na taxa da fonte de captura mais lenta. Quadros extras de fontes mais rápidas serão descartados. Além disso, como o sistema espera que os quadros cheguem de fontes diferentes em taxas diferentes, ele não reconhece automaticamente se uma fonte parou de gerar quadros completamente. O código de exemplo nesta seção mostra como usar um evento para criar sua própria lógica de tempo limite que será invocada se os quadros correlacionados não chegarem dentro de um limite de tempo definido pelo aplicativo.

As etapas para usar MultiSourceMediaFrameReader são semelhantes às etapas para usar MediaFrameReader descritas anteriormente neste artigo. Este exemplo usará uma fonte de cor e uma fonte de profundidade. Declare algumas variáveis de cadeia de caracteres para armazenar as IDs de origem do quadro de mídia que serão usadas para selecionar quadros de cada fonte. Em seguida, declare um ManualResetEventSlim, um CancellationTokenSource e um EventHandler que serão usados para implementar a lógica de tempo limite para o exemplo.

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;

Usando as técnicas descritas anteriormente neste artigo, consulte um MediaFrameSourceGroup que inclua as fontes de cor e profundidade necessárias para este cenário de exemplo. Depois de selecionar o grupo de origem de quadro desejado, obtenha o MediaFrameSourceInfo para cada fonte de quadro.

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

Crie e inicialize um objeto MediaCapture , passando o grupo de origem de quadros selecionado nas configurações de inicialização.

mediaCapture = new MediaCapture();

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

await mediaCapture.InitializeAsync(settings);

Depois de inicializar o objeto MediaCapture , recupere os objetos MediaFrameSource para as câmeras de cor e profundidade. Armazene a ID de cada fonte para que você possa selecionar o quadro de chegada para a fonte correspondente.

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;

Crie e inicialize o MultiSourceMediaFrameReader chamando CreateMultiSourceFrameReaderAsync e passando uma matriz de fontes de quadro que o leitor usará. Registre um manipulador de eventos para o evento FrameArrived. Este exemplo cria uma instância da classe auxiliar FrameRenderer , descrita anteriormente neste artigo, para renderizar quadros em um controle Image . Inicie o leitor de quadros chamando StartAsync.

Registre um manipulador de eventos para o evento CorellationFailed declarado anteriormente no exemplo. Sinalizaremos esse evento se uma das fontes de quadro de mídia usadas parar de produzir quadros. Por fim, chame Task.Run para chamar o método auxiliar de tempo limite, NotifyAboutCorrelationFailure, em um thread separado. A implementação desse método é mostrada posteriormente neste artigo.

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

O evento FrameArrived é gerado sempre que um novo quadro está disponível em todas as fontes de quadro de mídia gerenciadas pelo MultiSourceMediaFrameReader. Isso significa que o evento será gerado na cadência da fonte de mídia mais lenta. Se uma fonte produzir vários quadros no tempo em que uma fonte mais lenta produzir um quadro, os quadros extras da fonte rápida serão descartados.

Obtenha o MultiSourceMediaFrameReference associado ao evento chamando TryAcquireLatestFrame. Obtenha o MediaFrameReference associado a cada fonte de quadro de mídia chamando TryGetFrameReferenceBySourceId, passando as cadeias de caracteres de ID armazenadas quando o leitor de quadro foi inicializado.

Chame o método Set do objeto ManualResetEventSlim para sinalizar que os quadros chegaram. Verificaremos esse evento no método NotifyCorrelationFailure que está sendo executado em um thread separado.

Por fim, execute qualquer processamento nos quadros de mídia correlacionados ao tempo. Este exemplo simplesmente exibe o quadro da fonte de profundidade.

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

O método auxiliar NotifyCorrelationFailure foi executado em um thread separado depois que o leitor de quadros foi iniciado. Neste método, verifique se o evento de quadro recebido foi sinalizado. Lembre-se de que, no manipulador FrameArrived , definimos esse evento sempre que um conjunto de quadros correlacionados chega. Se o evento não tiver sido sinalizado por algum período de tempo definido pelo aplicativo – 5 segundos é um valor razoável – e a tarefa não tiver sido cancelada usando o CancellationToken, é provável que uma das fontes de quadro de mídia tenha parado de ler quadros. Nesse caso, você normalmente deseja desligar o leitor de quadros, portanto, gere o evento CorrelationFailed definido pelo aplicativo. No manipulador desse evento, você pode parar o leitor de quadros e limpar seus recursos associados, conforme mostrado anteriormente neste artigo.

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

Use o modo de aquisição de quadros em buffer para preservar a sequência de quadros adquiridos

A partir do Windows 10, versão 1709, você pode definir a propriedade AcquisitionMode de um MediaFrameReader ou MultiSourceMediaFrameReader como Buffered para preservar a sequência de quadros passados para seu aplicativo da origem do quadro.

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

No modo de aquisição padrão, em tempo real, se vários quadros forem adquiridos da origem enquanto o aplicativo ainda estiver manipulando o evento FrameArrived de um quadro anterior, o sistema enviará ao aplicativo o quadro adquirido mais recentemente e descartará quadros adicionais aguardando no buffer. Isso fornece ao seu aplicativo o quadro disponível mais recente em todos os momentos. Normalmente, esse é o modo mais útil para aplicativos de visão computacional em tempo real.

No modo de aquisição em buffer, o sistema manterá todos os quadros no buffer e os fornecerá ao seu aplicativo por meio do evento FrameArrived na ordem recebida. Observe que, nesse modo, quando o buffer do sistema para quadros estiver preenchido, o sistema interromperá a aquisição de novos quadros até que seu aplicativo conclua o evento FrameArrived para quadros anteriores, liberando mais espaço no buffer.

Usar MediaSource para exibir quadros em um MediaPlayerElement

A partir do Windows, versão 1709, você pode exibir quadros adquiridos de um MediaFrameReader diretamente em um controle MediaPlayerElement em sua página XAML. Isso é obtido usando o MediaSource.CreateFromMediaFrameSource para criar o objeto MediaSource que pode ser usado diretamente por um MediaPlayer associado a um MediaPlayerElement. Para obter informações detalhadas sobre como trabalhar com MediaPlayer e MediaPlayerElement, consulte Reproduzir áudio e vídeo com MediaPlayer.

Os exemplos de código a seguir mostram uma implementação simples que exibe os quadros de uma câmera frontal e traseira simultaneamente em uma página XAML.

Primeiro, adicione dois controles MediaPlayerElement à sua página XAML.

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

Em seguida, usando as técnicas mostradas nas seções anteriores deste artigo, selecione um MediaFrameSourceGroup que contenha objetos MediaFrameSourceInfo para câmeras coloridas no painel frontal e no painel traseiro. Observe que o MediaPlayer não converte automaticamente quadros de formatos não coloridos, como dados de profundidade ou infravermelho, em dados coloridos. O uso de outros tipos de sensores pode produzir 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];

Inicialize o objeto MediaCapture para usar o MediaFrameSourceGroup selecionado.

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 fim, chame MediaSource.CreateFromMediaFrameSource para criar um MediaSource para cada fonte de quadro usando a propriedade Id do objeto MediaFrameSourceInfo associado para selecionar uma das fontes de quadro na coleção FrameSources do objeto MediaCapture. Inicialize um novo objeto MediaPlayer e atribua-o a um MediaPlayerElement chamando SetMediaPlayer. Em seguida, defina a propriedade Source como o objeto MediaSource recém-criado.

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 perfis de vídeo para selecionar uma fonte de quadro

Um perfil de câmera, representado por um objeto MediaCaptureVideoProfile , representa um conjunto de recursos que um dispositivo de captura específico fornece, como taxas de quadros, resoluções ou recursos avançados, como captura HDR. Um dispositivo de captura pode dar suporte a vários perfis, permitindo que você selecione aquele que é otimizado para seu cenário de captura. A partir do Windows 10, versão 1803, você pode usar MediaCaptureVideoProfile para selecionar uma fonte de quadro de mídia com recursos específicos antes de inicializar o objeto MediaCapture . O método de exemplo a seguir procura um perfil de vídeo que dá suporte a HDR com WCG (Wide Color Gamut) e retorna um objeto MediaCaptureInitializationSettings que pode ser usado para inicializar o MediaCapture para usar o dispositivo e o perfil selecionados.

Primeiro, chame MediaFrameSourceGroup.FindAllAsync para obter uma lista de todos os grupos de origem de quadros de mídia disponíveis no dispositivo atual. Execute um loop em cada grupo de origem e chame MediaCapture.FindKnownVideoProfiles para obter uma lista de todos os perfis de vídeo para o grupo de origem atual que dá suporte ao perfil especificado, nesse caso, HDR com foto WCG. Se um perfil que atende aos critérios for encontrado, crie um novo objeto MediaCaptureInitializationSettings e defina o VideoProfile como o perfil selecionado e o VideoDeviceId para a ´propriedade ID do grupo de origem do quadro de mídia atual.

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 obter mais informações sobre como usar perfis de câmera, consulte Perfis de câmera.