Traiter des images multimédias avec MediaFrameReader

Cet article vous explique comment utiliser un MediaFrameReader avec MediaCapture pour obtenir des images multimédias à partir d’une ou plusieurs sources disponibles, notamment des caméras couleur, de profondeur et infrarouges, des appareils audio ou même des sources d’images personnalisées telles que celles qui produisent des images de suivi du squelette. Cette fonctionnalité est conçue pour être utilisée par les applications qui effectuent le traitement en temps réel des images multimédias, telles que les applications de caméra prenant en charge la profondeur et de réalité augmentée.

Si vous souhaitez simplement capturer du contenu vidéo ou des photos, comme c’est possible avec une application standard de photographie, vous avez probablement tout intérêt à valoriser l’une des autres techniques de capture prises en charge par MediaCapture. Pour consulter une liste des techniques de capture multimédia disponibles et des articles vous expliquant comment les utiliser, consultez la page Appareil photo.

Notes

Les fonctionnalités décrites dans cet article sont disponibles uniquement à partir de Windows 10, version 1607.

Notes

Il existe un exemple d’application Windows universelle qui illustre l’utilisation de MediaFrameReader pour afficher des images de différentes sources, notamment d’appareils photos couleur, de profondeur et infrarouges. Pour plus d’informations voir Profils d’appareil photo.

Notes

Un nouvel ensemble d’API pour l’utilisation de MediaFrameReader avec des données audio a été introduit dans Windows 10 version 1803. Pour plus d’informations, consultez Traiter des images audio avec MediaFrameReader.

Configuration de votre projet

Comme avec toute application utilisant MediaCapture, vous devez déclarer que votre application utilise la fonctionnalité webcam avant de tenter d’accéder à un appareil photo. Si votre application capture à partir d’un périphérique audio, vous devez également déclarer la fonctionnalité microphone.

Ajouter des fonctionnalités au manifeste de l’application

  1. Dans Microsoft Visual Studio, dans l’Explorateur de solutions, ouvrez le concepteur pour le manifeste de l’application en double-cliquant sur l’élément package.appxmanifest.
  2. Sélectionnez l’onglet Fonctionnalités.
  3. Activez les cases à cocher Webcam et Microphone.
  4. Pour accéder à la bibliothèque d’images et à la vidéothèque, cochez les cases correspondant à Bibliothèque d’images et Vidéothèque.

L’exemple de code de cet article utilise des API des espaces de noms suivants, en plus de celles fournies par le modèle de projet par défaut.

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;

Sélectionnez des sources d’images et des groupes de sources d’images

De nombreuses applications qui traitent des images multimédias doivent récupérer ces éléments de plusieurs sources simultanément, comme des appareils photos couleur et de profondeur d’un appareil. L’objet MediaFrameSourceGroup représente un ensemble de sources de trames multimédias qui peuvent être utilisées simultanément. Appelez la méthode statique MediaFrameSourceGroup.FindAllAsync afin de récupérer une liste de l’ensemble des groupes de sources d’images pris en charge par l’appareil actuel.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

Vous pouvez également créer un DeviceWatcher à l’aide de DeviceInformation.CreateWatcher et de la valeur renvoyée par MediaFrameSourceGroup.GetDeviceSelector pour recevoir des notifications lorsque les groupes sources d’images disponibles sur l’appareil changent, par exemple lorsqu’une caméra externe est branchée. Pour plus d’informations, consultez la page Énumérer les appareils.

Un MediaFrameSourceGroup possède une collection d’objets MediaFrameSourceInfo qui décrivent les sources d’images incluses dans le groupe. Une fois les groupes de sources d’images disponibles sur cet appareil récupérés, vous pouvez sélectionner le groupe exposant les sources d’images qui vous intéressent.

L’exemple suivant vous présente le moyen le plus simple de sélectionner un groupe de sources d’images. Ce code effectue simplement une boucle sur tous les groupes disponibles, puis effectue des boucles sur chaque élément de la collection SourceInfos . Chacune des instances MediaFrameSourceInfo est examinée, afin de vérifier qu’elles prennent en charge les fonctionnalités recherchées. Dans ce cas, la propriété MediaStreamType est examinée pour vérifier qu’elle contient la valeur VideoPreview, synonyme de production d’un flux d’aperçu vidéo par l’appareil, et la propriété SourceKind est examinée à la recherche de la valeur Color, indiquant que la source fournit des images couleur.

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

Cette méthode d’identification du groupe des sources d’images et des sources d’images fonctionne efficacement avec des cas simples, mais si vous souhaitez sélectionner des sources d’images en fonction de critères plus complexes, le processus peut rapidement devenir très lourd. Une autre méthode consiste à effectuer cette sélection à l’aide de la syntaxe Linq et d’objets anonymes. L’exemple suivant utilise la méthode d’extension Select afin de transformer les objets MediaFrameSourceGroup de la liste frameSourceGroups en objets anonymes avec deux champs : sourceGroup, représentant le groupe en soi et colorSourceInfo, qui est associé à la source des images couleur du groupe. Le champ colorSourceInfo est défini sur le résultat de FirstOrDefault, qui sélectionne le premier objet pour lequel le prédicat fourni a la valeur TRUE. Dans ce cas, le prédicat a la valeur TRUE si le type de flux est VideoPreview, le type de source est Color et si l’appareil photo se trouve sur le panneau avant de l’appareil.

À partir de la liste des objets anonymes renvoyés de la requête décrite ci-dessus, la méthode d’extension Where, la méthode d’extension est utilisée pour sélectionner uniquement les objets dont le champ colorSourceInfo n’a pas la valeur NULL. Enfin, FirstOrDefault est appelé pour sélectionner le premier élément de la liste.

Vous pouvez désormais utiliser les champs de l’objet sélectionné afin d’obtenir les références à l’instance MediaFrameSourceGroup et à l’objet MediaFrameSourceInfo sélectionnés représentant l’appareil photo couleur. Ces éléments seront utilisés ultérieurement pour initialiser l’objet MediaCapture et créer une instance MediaFrameReader pour la source sélectionnée. Enfin, vous avez intérêt à procéder à un test afin de déterminer si le groupe de sources a la valeur NULL, ce qui signifie que l’appareil actuel ne présente pas vos sources de capture demandées.

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

L’exemple suivant utilise une technique similaire à ce qui est décrit plus haut pour sélectionner un groupe de sources comportant des appareils photos couleur, de profondeur et infrarouges.

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

Notes

À compter de Windows 10 version 1803, vous pouvez utiliser la classe MediaCaptureVideoProfile pour sélectionner une source de trame multimédia avec un ensemble de fonctionnalités souhaitées. Pour plus d’informations, consultez la section Utiliser des profils vidéo pour sélectionner une source de trame plus loin dans cet article.

Initialiser l’objet MediaCapture afin d’utiliser le groupe de sources d’images sélectionné

L’étape suivante consiste à initialiser l’objet MediaCapture afin d’utiliser le groupe de sources d’images sélectionné à l’étape précédente.

L’objet MediaCapture étant généralement utilisé à partir de multiples emplacements de votre application, vous devez déclarer une variable de membre de classe pour le contenir.

MediaCapture mediaCapture;

Créez une instance de l’objet MediaCapture en appelant le constructeur. Ensuite, créez un objet MediaCaptureInitializationSettings qui sera utilisé pour initialiser l’objet MediaCapture . Dans cet exemple, les paramètres suivants sont utilisés :

  • SourceGroup - Ce paramètre indique au système le groupe de sources utilisé pour récupérer les images. N’oubliez pas que le groupe de sources définir un ensemble de sources d’images multimédias pouvant être utilisées simultanément.
  • SharingMode : indique au système si vous avez besoin d’un contrôle exclusif sur les appareils sources de capture. Si vous définissez ce paramètre sur ExclusiveControl, cela signifie que vous pouvez modifier les paramètres de l’appareil de capture, tels que le format des images qu’il produit, mais cela signifie que si une autre application dispose déjà d’un contrôle exclusif, votre application échoue lorsqu’elle tente d’initialiser l’appareil de capture multimédia. Si vous définissez cette valeur sur SharedReadOnly, vous pouvez recevoir des images des sources d’images même si elles sont utilisées par une autre application, mais vous ne pouvez pas modifier les paramètres des appareils.
  • MemoryPreference : si vous spécifiez le processeur, le système utilise la mémoire du processeur, ce qui garantit que lorsque les images arrivent, elles sont disponibles en tant qu’objets SoftwareBitmap . Si vous spécifiez Auto, le système choisit dynamiquement l’emplacement de mémoire optimal pour stocker les images. Si le système choisit d’utiliser la mémoire GPU, les trames multimédias arrivent en tant qu’objet IDirect3DSurface et non en tant que SoftwareBitmap.
  • StreamingCaptureMode : définissez ce paramètre sur Vidéo pour indiquer que l’audio n’a pas besoin d’être diffusé en continu.

Appelez InitializeAsync afin d’initialiser MediaCapture avec vos paramètres souhaités. Assurez-vous d’effectuer votre appel au sein d’un bloc try, ceci pour vous protéger en cas de mise en échec de l’initialisation.

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

Définir le format préféré de la source d’images

Pour définir le format préféré d’une source d’images, vous devez obtenir un objet MediaFrameSource représentant la source. Pour obtenir cet objet, accédez au dictionnaire Frames de l’objet MediaCapture initialisé, en spécifiant l’identificateur de la source d’images que vous souhaitez utiliser. C’est pourquoi nous avons enregistré l’objet MediaFrameSourceInfo lors de la sélection d’un groupe source d’images.

La propriété MediaFrameSource.SupportedFormats contient une liste d’objets MediaFrameFormat décrivant les formats pris en charge pour la source de frame. Utilisez la méthode d’extension Linq Where pour sélectionner un format basé sur les propriétés souhaitées. Dans cet exemple, le format sélectionné présente une largeur de 1 080 pixels et peut fournir des images au format RVB 32 bits. La méthode d’extension FirstOrDefault sélectionne la première entrée de la liste. Si le format sélectionné est NULL, le format demandé n’est pas pris en charge par la source des images. Si le format est pris en charge, vous pouvez demander que la source l’utilise en appelant 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);

Créer un lecteur d’images pour la source d’images

Pour recevoir les images pour une source d’images multimédias, utilisez une instance MediaFrameReader.

MediaFrameReader mediaFrameReader;

Instanciez le lecteur d’images en appelant CreateFrameReaderAsync sur votre objet MediaCapture initialisé. Le premier argument à appliquer à cette méthode est la source d’images à partir de laquelle vous souhaitez recevoir des images. Vous pouvez créer un lecteur séparé d’images pour chaque source d’images que vous souhaitez utiliser. Le second argument indique au système le format de sortie dans lequel vous souhaitez que les images arrivent. Cela peut vous éviter d’avoir à faire vos propres conversions sur les images en entrée. Notez que si vous spécifiez un format qui n’est pas pris en charge par la source d’images, une exception sera transmise. Aussi, assurez-vous que cette valeur se trouve dans la collection SupportedFormats.

Après avoir créé le lecteur d’images, enregistrez un gestionnaire pour l’événement FrameArrived qui est déclenché quand une nouvelle image est disponible à partir de la source.

Demandez au système de commencer à lire les images à partir de la source en appelant StartAsync.

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

Gérer l’événement déclenché à l’arrivée des images

L’événement MediaFrameReader.FrameArrived est déclenché chaque fois qu’une nouvelle image est disponible. Vous pouvez choisir de traiter toutes les images qui arrivent ou seulement celles dont vous avez besoin. Étant donné que le lecteur d’images déclenche l’événement sur son propre thread, il vous faudra éventuellement implémenter une logique de synchronisation afin de garantir que vous n’essayez pas d’accéder aux mêmes données à partir de plusieurs threads. Cette section vous indique comment synchroniser les images couleur des dessins sur un contrôle d’images dans une page XAML. Ce scénario traite de la contrainte de synchronisation supplémentaire qui implique que l’ensemble des mises à jour des contrôles XAML soient effectuées sur le même thread d’interface utilisateur.

La première étape de l’affichage des images au format XAML consiste à créer un contrôle Image.

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

Dans votre page code-behind, déclarez une variable de membre de classe de type SoftwareBitmap, qui sera utilisée en tant que mémoire tampon d’arrière-plan sur laquelle seront copiées l’ensemble des images entrantes. Notez que les données d’images en soi ne sont pas copiées, ce sont les références d’objet qui le sont. Par ailleurs, déclarez une valeur booléenne détectant l’exécution de votre opération d’interface utilisateur.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

Les images arrivant en tant qu’objets SoftwareBitmap, vous devez créer un objet SoftwareBitmapSource qui vous permet d’utiliser une instance SoftwareBitmap en tant que source pour un élément Control XAML. Vous devez définir la source d’images au sein de votre code avant de démarrer le lecteur d’images.

imageElement.Source = new SoftwareBitmapSource();

Il est désormais temps d’implémenter le gestionnaire d’événements FrameArrived. Quand le gestionnaire est appelé, le paramètre sender contient une référence à l’objet MediaFrameReader qui déclenche l’événement. Appelez TryAcquireLatestFrame sur cet objet pour tenter d’obtenir la dernière image. Comme son nom l’indique, TryAcquireLatestFrame peut ne pas réussir à renvoyer une image. Par conséquent, quand vous accédez aux propriétés VideoMediaFrame puis SoftwareBitmap, assurez-vous de détecter la définition de la valeur NULL. Dans cet exemple, l’opérateur conditionnel NULL ? est utilisé pour accéder à l’instance SoftwareBitmap. Ensuite, la valeur NULL est recherchée sur l’objet récupéré.

Le contrôle Image peut afficher des images uniquement au format BRGA8, avec aucune valeur alpha ou avec des valeurs alpha prémultipliées. Si l’image arrivante n’est pas dans ce format, la méthode statique Convert est utilisée pour convertir l’image bitmap logicielle au format approprié.

Ensuite, la méthode Interlocked.Exchange est utilisée pour remplacer la référence de l’image bitmap en entrée par l’image bitmap de la mémoire tampon d’arrière-plan. Cette méthode échange des références au cours d’une opération atomique thread-safe. Après le remplacement, l’ancienne image de mémoire tampon d’arrière-plan, désormais dans la variable softwareBitmap, est supprimée à des fins de nettoyage de ses ressources.

Ensuite, l’instance CoreDispatcher associée à l’élément Image est utilisée pour créer une tâche qui s’exécutera sur le thread d’interface utilisateur en appelant RunAsync. Étant donné que les tâches asynchrones seront exécutées au sein de la tâche, l’expression lambda transmise à RunAsync est déclarée avec le mot-clé async.

Au sein de la tâche, la variable _taskRunning est examinée afin de garantir qu’une seule instance de la tâche s’exécute à la fois. SI la tâche n’est pas en cours d’exécution, l’élément _taskRunning est défini sur True, ceci pour prévenir toute nouvelle réexécution. Dans une boucle while, Interlocked.Exchange est appelé pour l’exécution d’une copie d’une mémoire tampon d’arrière-plan sur une instance SoftwareBitmap temporaire, jusqu’à ce que l’image de la mémoire tampon d’arrière-plan soit définie sur NULL. À chaque fois que l’image bitmap temporaire est remplie, la propriété Source du contrôle Image est castée en instance SoftwareBitmapSource, puis SetBitmapAsync est appelée pour définir la source de l’image.

Enfin, la variable _taskRunning est redéfinie sur False, afin que la tâche puisse à nouveau s’exécuter lors du prochain appel du gestionnaire.

Notes

Si vous accédez aux objets SoftwareBitmap ou Direct3DSurface fournis par la propriété VideoMediaFrame d’un MediaFrameReference, le système crée une référence forte à ces objets. Autrement dit, ils ne sont pas supprimés lorsque vous appelez Dispose sur le conteneur MediaFrameReference. Vous devez appeler la méthode Dispose de SoftwareBitmap ou de Direct3DSurface explicitement et directement pour les objets à supprimer immédiatement. Sinon, le récupérateur de mémoire va libérer de la mémoire pour ces objets. Mais vous ne pouvez pas savoir quand cela se produit, et si le nombre de surfaces ou d’images bitmap allouées dépasse la quantité maximale autorisée par le système, le flux de nouvelles images s’arrête. Vous pouvez copier des images récupérées, à l’aide de la méthode SoftwareBitmap.Copy par exemple, puis libérer les images d’origine pour surmonter cette limitation. En outre, si vous créez le MediaFrameReader à l’aide de la surcharge 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), les images retournées sont des copies des données d’image d’origine et ne provoquent donc pas l’arrêt de l’acquisition de trame lorsqu’elles sont conservées.

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

Nettoyer les ressources

Lorsque vous avez fini de lire les images, assurez-vous d’arrêter le lecteur d’images multimédias en appelant StopAsync, en annulant l’enregistrement du gestionnaire FrameArrived et en supprimant l’objet MediaCapture.

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

Pour plus d’informations sur le nettoyage des objets de capture multimédia lorsque votre application est suspendue, consultez Afficher l’aperçu de l’appareil photo.

La classe d’assistance FrameRenderer

Le profil d’appareil photo Windows universel fournit une classe d’assistance qui facilite l’affichage d’images de sources couleur, infrarouges et de profondeur dans votre application. Généralement, vous ne vous contentez pas d’afficher les données infrarouge et de profondeur à l’écran, mais cette classe d’assistance est un outil utile permettant d’illustrer la fonctionnalité du lecteur d’images et de déboguer votre propre implémentation du lecteur d’images.

La classe d’assistance FrameRenderer implémente les méthodes suivantes.

  • Constructeur FrameRenderer - Le constructeur initialise la classe d’assistance afin d’utiliser l’élément XAML Image transmis pour l’affichage des images multimédias.
  • ProcessFrame - Cette méthode affiche une image multimédia, représentée par une instance MediaFrameReference, dans l’élément Image transmis dans le constructeur. Vous devez généralement appeler cette méthode à partir de votre gestionnaire d’événements FrameArrived , en passant le frame retourné par TryAcquireLatestFrame.
  • ConvertToDisplayableImage - Cette méthode vérifie le format de l’image multimédia et si nécessaire la convertit en format affichable. Pour les images couleur, elle garantit que le format couleur est BGRA8 et que le mode alpha bitmap est prémultiplié. Pour les images de profondeur et infrarouges, chaque ligne de balayage est traitée pour convertir les valeurs de profondeur et infrarouges en gradient pseudocolor, à l’aide de la classe PseudoColorHelper qui est également incluse dans l’exemple et répertoriée ci-dessous.

Notes

Pour procéder à une manipulation de pixels sur les images SoftwareBitmap, vous devez accéder à une mémoire tampon native. Pour ce faire, vous devez utiliser l’interface COM IMemoryBufferByteAccess COM incluse dans l’ensemble du code ci-dessous, et vous devez mettre à jour vos propriétés de projet afin de permettre la compilation du code non sécurisé. Pour plus d’informations, consultez la page Créer, modifier et enregistrer des images 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;
                });
        }
    }
}

Utiliser MultiSourceMediaFrameReader pour obtenir des images corellées à partir de plusieurs sources

À compter de Windows 10 version 1607, vous pouvez utiliser MultiSourceMediaFrameReader pour recevoir des images corellées dans le temps de plusieurs sources. Cette API facilite le traitement qui nécessite des images provenant de plusieurs sources qui ont été prises à proximité temporelle proche, comme l’utilisation de la classe DepthCorrelatedCoordinateMapper . L’une des limitations de l’utilisation de cette nouvelle méthode est que les événements arrivés aux images ne sont déclenchés qu’au rythme de la source de capture la plus lente. Les images supplémentaires provenant de sources plus rapides seront supprimées. En outre, étant donné que le système s’attend à ce que les images arrivent de différentes sources à des taux différents, il ne reconnaît pas automatiquement si une source a cessé de générer des images. L’exemple de code de cette section montre comment utiliser un événement pour créer votre propre logique de délai d’attente qui est appelée si les trames corrélées n’arrivent pas dans une limite de temps définie par l’application.

Les étapes d’utilisation de MultiSourceMediaFrameReader sont similaires aux étapes d’utilisation de MediaFrameReader décrites précédemment dans cet article. Cet exemple utilise une source de couleur et une source de profondeur. Déclarez certaines variables de chaîne pour stocker les ID de source de trame multimédia qui seront utilisés pour sélectionner des images de chaque source. Ensuite, déclarez un ManualResetEventSlim, un CancellationTokenSource et un EventHandler qui seront utilisés pour implémenter la logique de délai d’expiration pour l’exemple.

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;

À l’aide des techniques décrites précédemment dans cet article, interrogez un MediaFrameSourceGroup qui inclut les sources de couleur et de profondeur requises pour cet exemple de scénario. Après avoir sélectionné le groupe de sources d’images souhaité, obtenez le MediaFrameSourceInfo pour chaque source de trame.

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

Créez et initialisez un objet MediaCapture en passant le groupe source de trames sélectionné dans les paramètres d’initialisation.

mediaCapture = new MediaCapture();

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

await mediaCapture.InitializeAsync(settings);

Après avoir initialisé l’objet MediaCapture , récupérez les objets MediaFrameSource pour les caméras de couleur et de profondeur. Stockez l’ID de chaque source afin de pouvoir sélectionner le cadre d’arrivée de la source correspondante.

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;

Créez et initialisez le MultiSourceMediaFrameReader en appelant CreateMultiSourceFrameReaderAsync et en transmettant un tableau de sources d’images que le lecteur utilisera. Inscrivez un gestionnaire d’événements pour l’événement FrameArrived . Cet exemple crée une instance la classe d’assistance FrameRenderer, décrite précédemment dans cet article, pour afficher des images dans un contrôle Image. Démarrez le lecteur d’images en appelant StartAsync.

Inscrivez un gestionnaire d’événements pour l’événement CorellationFailed déclaré précédemment dans l’exemple. Nous signalerons cet événement si l’une des sources de trames multimédias utilisées arrête de produire des images. Enfin, appelez Task.Run pour appeler la méthode d’assistance au délai d’expiration, NotifyAboutCorrelationFailure, sur un thread distinct. L’implémentation de cette méthode est présentée plus loin dans cet article.

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

L’événement FrameArrived est déclenché chaque fois qu’une nouvelle image est disponible à partir de toutes les sources d’images multimédias gérées par le MultiSourceMediaFrameReader. Cela signifie que l’événement sera déclenché à la cadence de la source multimédia la plus lente. Si une source produit plusieurs images pendant qu’une source plus lente produit une image, les images supplémentaires de la source rapide sont supprimées.

Obtenez le MultiSourceMediaFrameReference associé à l’événement en appelant TryAcquireLatestFrame. Obtenez le MediaFrameReference associé à chaque source de trame multimédia en appelant TryGetFrameReferenceBySourceId, en transmettant les chaînes d’ID stockées lors de l’initialisation du lecteur d’images.

Appelez la méthode Set de l’objet ManualResetEventSlim pour signaler que des images sont arrivées. Nous allons case activée cet événement dans la méthode NotifyCorrelationFailure qui s’exécute dans un thread distinct.

Enfin, effectuez tout traitement sur les images multimédias corrélées dans le temps. Cet exemple montre comment afficher simplement le cadre à partir de la source de profondeur.

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

La méthode d’assistance NotifyCorrelationFailure a été exécutée sur un thread distinct après le démarrage du lecteur d’images. Dans cette méthode, case activée pour voir si l’événement frame reçu a été signalé. N’oubliez pas que dans le gestionnaire FrameArrived, nous définissons cet événement chaque fois qu’un ensemble d’images corrélées arrive. Si l’événement n’a pas été signalé pendant une période définie par l’application (5 secondes est une valeur raisonnable) et que la tâche n’a pas été annulée à l’aide de CancellationToken, il est probable qu’une des sources de trames multimédias ait cessé de lire les images. Dans ce cas, vous souhaitez généralement arrêter le lecteur d’images. Par conséquent, déclenchez l’événement CorrelationFailed défini par l’application. Dans le gestionnaire de cet événement, vous pouvez arrêter le lecteur d’images et propre les ressources associées, comme indiqué précédemment dans cet article.

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

Utiliser le mode d’acquisition d’images mises en mémoire tampon pour préserver la séquence d’images acquises

À compter de Windows 10 version 1709, vous pouvez définir la propriété AcquisitionMode d’un MediaFrameReader ou d’un MultiSourceMediaFrameReader sur Buffered pour conserver la séquence d’images passées dans votre application à partir de la source de trame.

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

En mode d’acquisition par défaut, Realtime, si plusieurs images sont acquises à partir de la source alors que votre application gère toujours l’événement FrameArrived pour une image précédente, le système envoie à votre application la dernière image acquise et supprime les images supplémentaires en attente dans la mémoire tampon. Cela fournit à votre application la trame disponible la plus récente à tout moment. Il s’agit généralement du mode le plus utile pour les applications de vision par ordinateur en temps réel.

En mode d’acquisition mise en mémoire tampon , le système conserve toutes les images dans la mémoire tampon et les fournit à votre application via l’événement FrameArrived dans la commande reçue. Notez que dans ce mode, lorsque la mémoire tampon du système pour les images est remplie, le système cesse d’acquérir de nouvelles images jusqu’à ce que votre application termine l’événement FrameArrived pour les images précédentes, ce qui libère plus d’espace dans la mémoire tampon.

Utiliser MediaSource pour afficher des images dans un élément MediaPlayerElement

À compter de Windows, version 1709, vous pouvez afficher les images acquises à partir d’un MediaFrameReader directement dans un contrôle MediaPlayerElement dans votre page XAML. Pour ce faire, utilisez MediaSource.CreateFromMediaFrameSource pour créer un objet MediaSource qui peut être utilisé directement par un MediaPlayer associé à un MediaPlayerElement. Pour plus d’informations sur l’utilisation de MediaPlayer et De MediaPlayerElement, consultez Lire de l’audio et de la vidéo avec MediaPlayer.

Les exemples de code suivants vous montrent une implémentation simple qui affiche les images d’une caméra frontale et arrière simultanément dans une page XAML.

Tout d’abord, ajoutez deux contrôles MediaPlayerElement à votre page XAML.

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

Ensuite, à l’aide des techniques présentées dans les sections précédentes de cet article, sélectionnez un MediaFrameSourceGroup qui contient des objets MediaFrameSourceInfo pour les caméras couleur sur le panneau avant et le panneau arrière. Notez que MediaPlayer ne convertit pas automatiquement les images de formats non couleur, tels qu’une profondeur ou des données infrarouges, en données de couleur. L’utilisation d’autres types de capteurs peut produire des résultats inattendus.

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

Initialisez l’objet MediaCapture pour utiliser le MediaFrameSourceGroup sélectionné.

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

Enfin, appelez MediaSource.CreateFromMediaFrameSource pour créer un MediaSource pour chaque source d’images à l’aide de la propriété Id de l’objet MediaFrameSourceInfo associé pour sélectionner l’une des sources de frame dans la collection FrameSources de l’objet MediaCapture. Initialisez un nouvel objet MediaPlayer et affectez-le à un objet MediaPlayerElement en appelant SetMediaPlayer. Ensuite, définissez la propriété Source sur l’objet MediaSource nouvellement créé.

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;

Utiliser des profils vidéo pour sélectionner une source de trame

Un profil de caméra, représenté par un objet MediaCaptureVideoProfile , représente un ensemble de fonctionnalités qu’un appareil de capture particulier fournit, telles que les fréquences d’images, les résolutions ou les fonctionnalités avancées telles que la capture HDR. Un appareil de capture peut prendre en charge plusieurs profils, ce qui vous permet de sélectionner celui qui est optimisé pour votre scénario de capture. À compter de Windows 10 version 1803, vous pouvez utiliser MediaCaptureVideoProfile pour sélectionner une source de trame multimédia avec des fonctionnalités particulières avant d’initialiser l’objet MediaCapture. L’exemple de méthode suivant recherche un profil vidéo qui prend en charge HDR avec Wide Color Gamut (WCG) et retourne un objet MediaCaptureInitializationSettings qui peut être utilisé pour initialiser le MediaCapture pour utiliser l’appareil et le profil sélectionnés.

Tout d’abord, appelez MediaFrameSourceGroup.FindAllAsync pour obtenir la liste de tous les groupes de sources de trames multimédias disponibles sur l’appareil actuel. Effectuez une boucle dans chaque groupe source et appelez MediaCapture.FindKnownVideoProfiles pour obtenir la liste de tous les profils vidéo pour le groupe source actuel qui prend en charge le profil spécifié, dans ce cas HDR avec photo WCG. Si un profil qui répond aux critères est trouvé, créez un objet MediaCaptureInitializationSettings et définissezVideoProfile sur le profil de sélection et VideoDeviceId sur la propriété Id du groupe source d’images multimédias actuel.

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

Pour plus d’informations sur l’utilisation des profils d’appareil photo, consultez Profils d’appareil photo.