Freigeben über


Verarbeiten von Medienframes mit „MediaFrameReader“

In diesem Artikel wird gezeigt, wie Sie einen MediaFrameReader mit MediaCapture verwenden, um Medienframes aus einer oder mehreren verfügbaren Quellen abzurufen, einschließlich Farb-, Tiefen- und Infrarotkameras, Audiogeräte oder sogar benutzerdefinierte Framequellen wie solche, die Skelett-Tracking-Frames erzeugen. Dieses Feature wurde für die Verwendung von Apps entworfen, die Medienframes in Echtzeit verarbeiten, wie beispielsweise Augmented-Reality- und Tiefenkamera-Apps.

Wenn Sie nur Video oder Fotos aufnehmen möchten, z. B. eine typische Foto-App, sollten Sie wahrscheinlich eine der anderen Aufnahmetechniken verwenden, die von MediaCapture unterstützt werden. Eine Liste der verfügbaren Medienaufnahmetechniken und Artikel zur Verwendung finden Sie unter "Kamera".

Hinweis

Die in diesem Artikel beschriebenen Features sind nur ab Windows 10, Version 1607, verfügbar.

Hinweis

Es gibt ein Beispiel für universelle Windows-Apps, das die Verwendung von MediaFrameReader zum Anzeigen von Frames aus verschiedenen Framequellen veranschaulicht, einschließlich Farb-, Tiefen- und Infrarotkameras. Weitere Informationen finden Sie im Beispiel für Kameraframes.

Hinweis

In Windows 10, Version 1803, wurde ein neuer Satz von APIs zur Verwendung von MediaFrameReader mit Audiodaten eingeführt. Weitere Informationen finden Sie unter Verarbeiten von Audioframes mit MediaFrameReader.

Einrichten Ihres Projekts

Wie bei jeder App, die MediaCapture verwendet, müssen Sie deklarieren, dass Ihre App die Webcamfunktion verwendet, bevor Sie versuchen, auf ein Kameragerät zuzugreifen. Wenn Ihre App von einem Audiogerät erfasst wird, sollten Sie auch die Mikrofongerätefunktion deklarieren.

Hinzufügen von Funktionen zum App-Manifest

  1. Öffnen Sie in Microsoft Visual Studio im Projektmappen-Explorer, den Designer für das Anwendungsmanifest, indem Sie auf das Element package.appxmanifest doppelklicken.
  2. Wählen Sie die Registerkarte Funktionen aus.
  3. Aktivieren Sie das Kästchen für Webcam und das Kästchen für Mikrofon.
  4. Für den Zugriff auf die Bilder- und Videobibliothek markieren Sie die Kästchen für Bilderbibliothek und das Kästchen für Videobibliothek.

Im Beispielcode in diesem Artikel werden neben den in der Standardprojektvorlage enthaltenen APIs aus den folgenden Namespaces verwendet.

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;

Auswählen von Framequellen und Framequellgruppen

Viele Apps, die Medienframes verarbeiten, müssen Frames aus mehreren Quellen gleichzeitig abrufen, z. B. die Farb- und Tiefenkameras eines Geräts. Das MediaFrameSourceGroup -Objekt stellt eine Reihe von Medienframequellen dar, die gleichzeitig verwendet werden können. Rufen Sie die statische Methode MediaFrameSourceGroup.FindAllAsync auf, um eine Liste aller Gruppen von Framequellen abzurufen, die vom aktuellen Gerät unterstützt werden.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

Sie können auch ein DeviceWatcher mit DeviceInformation.CreateWatcher und dem von MediaFrameSourceGroup.GetDeviceSelector zurückgegebenen Wert erstellen, um Benachrichtigungen zu erhalten, wenn sich die verfügbaren Framequellgruppen auf dem Gerät ändern, z. B. wenn eine externe Kamera angeschlossen ist. Weitere Informationen finden Sie unter "Aufzählen von Geräten".

Eine MediaFrameSourceGroup verfügt über eine Sammlung von MediaFrameSourceInfo-Objekten , die die in der Gruppe enthaltenen Framequellen beschreiben. Nachdem Sie die auf dem Gerät verfügbaren Framequellgruppen abgerufen haben, können Sie die Gruppe auswählen, die die framequellen verfügbar macht, an denen Sie interessiert sind.

Das folgende Beispiel zeigt die einfachste Möglichkeit zum Auswählen einer Framequellgruppe. Dieser Code durchläuft einfach alle verfügbaren Gruppen und durchläuft dann jedes Element in der SourceInfos-Auflistung. Jede MediaFrameSourceInfo wird überprüft, um festzustellen, ob sie die von uns gesuchten Features unterstützt. In diesem Fall wird die MediaStreamType-Eigenschaft auf den Wert VideoPreview überprüft, d. h. das Gerät stellt einen Videovorschaudatenstrom bereit, und die SourceKind-Eigenschaft wird auf den Wert Color überprüft, der angibt, dass die Quelle Farbframes bereitstellt.

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

Diese Methode zum Identifizieren der gewünschten Framequellgruppe und Framequellen funktioniert für einfache Fälle, aber wenn Sie Framequellen basierend auf komplexeren Kriterien auswählen möchten, kann es schnell umständlich werden. Eine weitere Methode besteht darin, linq-Syntax und anonyme Objekte zu verwenden, um die Auswahl zu treffen. Im folgenden Beispiel wird die Select-Erweiterungsmethode verwendet, um die MediaFrameSourceGroup-Objekte in der FrameSourceGroups-Liste in ein anonymes Objekt mit zwei Feldern zu transformieren: sourceGroup, das die Gruppe selbst darstellt, und colorSourceInfo, die die Farbframequelle in der Gruppe darstellt. Das ColorSourceInfo-Feld wird auf das Ergebnis von FirstOrDefault festgelegt, das das erste Objekt auswählt, für das das bereitgestellte Prädikat auf "true" aufgelöst wird. In diesem Fall ist das Prädikat "true", wenn der Datenstromtyp "VideoPreview" ist, die Quellart "Color" und wenn sich die Kamera auf der Vorderseite des Geräts befindet.

Aus der Liste der anonymen Objekte, die aus der oben beschriebenen Abfrage zurückgegeben werden, wird die Where-Erweiterungsmethode verwendet, um nur die Objekte auszuwählen, bei denen das ColorSourceInfo-Feld nicht NULL ist. Schließlich wird FirstOrDefault aufgerufen, um das erste Element in der Liste auszuwählen.

Jetzt können Sie die Felder des ausgewählten Objekts verwenden, um Verweise auf die ausgewählte MediaFrameSourceGroup und das MediaFrameSourceInfo -Objekt abzurufen, das die Farbkamera darstellt. Diese werden später verwendet, um das MediaCapture-Objekt zu initialisieren und einen MediaFrameReader für die ausgewählte Quelle zu erstellen. Schließlich sollten Sie testen, ob die Quellgruppe null ist, was bedeutet, dass das aktuelle Gerät nicht über die angeforderten Aufnahmequellen verfügt.

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

Im folgenden Beispiel wird eine ähnliche Technik verwendet, wie oben beschrieben, um eine Quellgruppe auszuwählen, die Farb-, Tiefen- und Infrarotkameras enthält.

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

Hinweis

Ab Windows 10, Version 1803, können Sie die MediaCaptureVideoProfile-Klasse verwenden, um eine Medienframequelle mit einer Reihe von gewünschten Funktionen auszuwählen. Weitere Informationen finden Sie im Abschnitt "Verwenden von Videoprofilen" zum Auswählen einer Framequelle weiter unten in diesem Artikel.

Initialisieren des MediaCapture-Objekts für die Verwendung der ausgewählten Framequellgruppe

Der nächste Schritt besteht darin, das MediaCapture-Objekt zu initialisieren, um die Framequellgruppe zu verwenden, die Sie im vorherigen Schritt ausgewählt haben.

Das MediaCapture-Objekt wird in der Regel von mehreren Speicherorten in Ihrer App verwendet, daher sollten Sie eine Klassenmemmemnungsvariable deklarieren, die sie enthalten soll.

MediaCapture mediaCapture;

Erstellen Sie eine Instanz des MediaCapture-Objekts , indem Sie den Konstruktor aufrufen. Erstellen Sie als Nächstes ein MediaCaptureInitializationSettings-Objekt , das zum Initialisieren des MediaCapture-Objekts verwendet wird. In diesem Beispiel werden die folgenden Einstellungen verwendet:

  • SourceGroup – Dies teilt dem System mit, welche Quellgruppe Sie zum Abrufen von Frames verwenden werden. Denken Sie daran, dass die Quellgruppe einen Satz von Medienframequellen definiert, die gleichzeitig verwendet werden können.
  • SharingMode – Dies teilt dem System mit, ob Sie exklusive Kontrolle über die Aufnahmequellengeräte benötigen. Wenn Sie dies auf ExclusiveControl festlegen, bedeutet dies, dass Sie die Einstellungen des Aufnahmegeräts ändern können, z. B. das Format der von ihr erzeugten Frames. Dies bedeutet jedoch, dass ihre App fehlschlägt, wenn eine andere App bereits über exklusive Kontrolle verfügt, wenn sie versucht, das Medienaufnahmegerät zu initialisieren. Wenn Sie dies auf SharedReadOnly festlegen, können Sie Frames aus den Framequellen empfangen, auch wenn sie von einer anderen App verwendet werden, aber Sie können die Einstellungen für die Geräte nicht ändern.
  • MemoryPreference – Wenn Sie CPU angeben, verwendet das System CPU-Speicher, der garantiert, dass sie beim Eintreffen von Frames als SoftwareBitmap-Objekte verfügbar sind. Wenn Sie Auto angeben, wählt das System dynamisch den optimalen Speicherort zum Speichern von Frames aus. Wenn das System den GPU-Speicher verwendet, werden die Medienframes als IDirect3DSurface-Objekt und nicht als SoftwareBitmap eintreffen.
  • StreamingCaptureMode – Legen Sie dies auf Video fest, um anzugeben, dass Audio nicht gestreamt werden muss.

Rufen Sie InitializeAsync auf, um mediaCapturemit den gewünschten Einstellungen zu initialisieren. Achten Sie darauf, dies in einem Try-Block aufzurufen, falls die Initialisierung fehlschlägt.

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

Festlegen des bevorzugten Formats für die Framequelle

Um das bevorzugte Format für eine Framequelle festzulegen, müssen Sie ein MediaFrameSource-Objekt abrufen, das die Quelle darstellt. Sie erhalten dieses Objekt, indem Sie auf das Frames-Wörterbuch des initialisierten MediaCapture-Objekts zugreifen und den Bezeichner der framequelle angeben, die Sie verwenden möchten. Deshalb haben wir das MediaFrameSourceInfo-Objekt gespeichert, als wir eine Framequellgruppe ausgewählt haben.

Die MediaFrameSource.SupportedFormats-Eigenschaft enthält eine Liste von MediaFrameFormat-Objekten, die die unterstützten Formate für die Framequelle beschreiben. Verwenden Sie die Where Linq-Erweiterungsmethode, um ein Format basierend auf den gewünschten Eigenschaften auszuwählen. In diesem Beispiel wird ein Format ausgewählt, das eine Breite von 1080 Pixel aufweist und Frames im 32-Bit-RGB-Format bereitstellen kann. Die FirstOrDefault-Erweiterungsmethode wählt den ersten Eintrag in der Liste aus. Wenn das ausgewählte Format NULL ist, wird das angeforderte Format von der Framequelle nicht unterstützt. Wenn das Format unterstützt wird, können Sie anfordern, dass die Quelle dieses Format verwendet, indem Sie SetFormatAsync aufrufen.

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

Erstellen eines Framelesers für die Framequelle

Verwenden Sie einen MediaFrameReader, um Frames für eine Medienframequelle zu empfangen.

MediaFrameReader mediaFrameReader;

Instanziieren Sie den Frameleser, indem Sie CreateFrameReaderAsync für Das initialisierte MediaCapture-Objekt aufrufen. Das erste Argument für diese Methode ist die Framequelle, aus der Sie Frames empfangen möchten. Sie können für jede Framequelle, die Sie verwenden möchten, einen separaten Frameleser erstellen. Das zweite Argument teilt dem System das Ausgabeformat mit, in dem Frames eintreffen sollen. Dadurch können Sie nicht ihre eigenen Konvertierungen in Frames vornehmen müssen, sobald sie ankommen. Beachten Sie, dass beim Angeben eines Formats, das von der Framequelle nicht unterstützt wird, eine Ausnahme ausgelöst wird. Stellen Sie daher sicher, dass dieser Wert in der SupportedFormats-Auflistung enthalten ist.

Registrieren Sie nach dem Erstellen des Framereaders einen Handler für das FrameArrived-Ereignis , das ausgelöst wird, wenn ein neuer Frame aus der Quelle verfügbar ist.

Teilen Sie dem System mit, frames aus der Quelle zu lesen, indem Sie StartAsync aufrufen.

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

Behandeln des eingetroffenen Frame-Ereignisses

Das MediaFrameReader.FrameArrived-Ereignis wird ausgelöst, wenn ein neuer Frame verfügbar ist. Sie können auswählen, dass jeder Frame verarbeitet wird, der eingeht oder nur Frames verwendet, wenn Sie sie benötigen. Da der Frameleser das Ereignis in einem eigenen Thread auslöst, müssen Sie möglicherweise eine Synchronisierungslogik implementieren, um sicherzustellen, dass Sie nicht versuchen, auf dieselben Daten aus mehreren Threads zuzugreifen. In diesem Abschnitt wird gezeigt, wie Sie Zeichnungsfarbrahmen mit einem Bildsteuerelement auf einer XAML-Seite synchronisieren. In diesem Szenario wird die zusätzliche Synchronisierungseinschränkung behandelt, für die alle Aktualisierungen von XAML-Steuerelementen im UI-Thread ausgeführt werden müssen.

Der erste Schritt beim Anzeigen von Frames in XAML besteht darin, ein Bildsteuerelement zu erstellen.

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

Deklarieren Sie auf der CodeBehind-Seite eine Klassenmemembesendvariable vom Typ "SoftwareBitmap ", die als Hintergrundpuffer verwendet wird, in den alle eingehenden Bilder kopiert werden. Beachten Sie, dass die Bilddaten selbst nicht kopiert werden, nur die Objektverweise. Deklarieren Sie außerdem einen booleschen Wert, um nachzuverfolgen, ob der UI-Vorgang zurzeit ausgeführt wird.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

Da die Frames als SoftwareBitmap-Objekte eingehen, müssen Sie ein SoftwareBitmapSource-Objekt erstellen, mit dem Sie eine SoftwareBitmap als Quelle für ein XAML-Steuerelement verwenden können. Sie sollten die Bildquelle an einer beliebigen Stelle im Code festlegen, bevor Sie den Frameleser starten.

imageElement.Source = new SoftwareBitmapSource();

Jetzt ist es an der Zeit, den FrameArrived-Ereignishandler zu implementieren. Wenn der Handler aufgerufen wird, enthält der Senderparameter einen Verweis auf das MediaFrameReader -Objekt, das das Ereignis ausgelöst hat. Rufen Sie TryAcquireLatestFrame für dieses Objekt auf, um zu versuchen, den neuesten Frame abzurufen. Wie der Name schon sagt, ist TryAcquireLatestFrame möglicherweise nicht erfolgreich, um einen Frame zurückzugeben. Wenn Sie also auf die Eigenschaften "VideoMediaFrame" und "SoftwareBitmap" zugreifen, müssen Sie unbedingt auf NULL testen. In diesem Beispiel ist der bedingte Operator null ? wird für den Zugriff auf die SoftwareBitmap verwendet, und dann wird das abgerufene Objekt auf NULL überprüft.

Das Bildsteuerelement kann nur Bilder im BRGA8-Format mit vormultiplizierten oder ohne Alpha anzeigen. Wenn sich der eingehende Frame nicht in diesem Format befindet, wird die statische Methode Convert verwendet, um die Softwarebitmap in das richtige Format zu konvertieren.

Als Nächstes wird die Interlocked.Exchange-Methode verwendet, um den Verweis auf die eingehende Bitmap mit der Backbuffer-Bitmap zu tauschen. Mit dieser Methode werden diese Verweise in einem atombasierten Vorgang ausgetauscht, der threadsicher ist. Nach dem Swapping wird das alte Backbuffer-Image, jetzt in der softwareBitmap-Variablen , gelöscht, um die Ressourcen zu bereinigen.

Als Nächstes wird der dem Image-Element zugeordnete CoreDispatcher verwendet, um eine Aufgabe zu erstellen, die im UI-Thread ausgeführt wird, indem RunAsync aufgerufen wird. Da die asynchronen Aufgaben innerhalb der Aufgabe ausgeführt werden, wird der an RunAsync übergebene Lambda-Ausdruck mit dem asynchronen Schlüsselwort deklariert.

Innerhalb der Aufgabe wird die _taskRunning Variable überprüft, um sicherzustellen, dass jeweils nur eine Instanz der Aufgabe ausgeführt wird. Wenn die Aufgabe noch nicht ausgeführt wird, wird _taskRunning auf "true" festgelegt, um zu verhindern, dass die Aufgabe erneut ausgeführt wird. In einer While-Schleife wird Interlocked.Exchange aufgerufen, um aus dem Backbuffer in eine temporäre SoftwareBitmap zu kopieren, bis das Backbuffer-Image null ist. Bei jedem Auffüllen der temporären Bitmap wird die Source-Eigenschaft des Bilds in eine SoftwareBitmapSource umgestellt, und dann wird SetBitmapAsync aufgerufen, um die Quelle des Bilds festzulegen.

Schließlich wird die _taskRunning Variable auf "false" zurückgesetzt, sodass die Aufgabe erneut ausgeführt werden kann, wenn der Handler das nächste Mal aufgerufen wird.

Hinweis

Wenn Sie auf die SoftwareBitmap- oder Direct3DSurface-Objekte zugreifen, die von der VideoMediaFrame-Eigenschaft eines MediaFrameReference bereitgestellt werden, erstellt das System einen starken Verweis auf diese Objekte, was bedeutet, dass sie beim Aufrufen von Dispose für das enthaltende MediaFrameReference nicht verworfen werden. Sie müssen explizit die Dispose-Methode der SoftwareBitmap oder Direct3DSurface direkt aufrufen, damit die Objekte sofort gelöscht werden. Andernfalls gibt der Garbage Collector schließlich den Speicher für diese Objekte frei, aber Sie können nicht wissen, wann dies geschieht, und wenn die Anzahl der zugewiesenen Bitmaps oder Oberflächen den vom System zulässigen maximalen Wert überschreitet, wird der Fluss neuer Frames beendet. Sie können abgerufene Frames kopieren, z. B. mit der SoftwareBitmap.Copy-Methode , und dann die ursprünglichen Frames freigeben, um diese Einschränkung zu überwinden. Wenn Sie den MediaFrameReader auch mithilfe der Überladung CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) oder CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype) erstellen, sind die zurückgegebenen Frames Kopien der ursprünglichen Framedaten und führen daher nicht dazu, dass frame-Kauf angehalten wird, wenn sie sie sind beibehalten.

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

Bereinigen von Ressourcen

Wenn Sie mit dem Lesen von Frames fertig sind, müssen Sie den Medienframeleser beenden, indem Sie StopAsync aufrufen, die Registrierung des FrameArrived-Handlers aufheben und das MediaCapture-Objekt entfernen.

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

Weitere Informationen zum Bereinigen von Medienaufnahmeobjekten beim Anhalten der Anwendung finden Sie unter "Anzeigen der Kameravorschau".

Die FrameRenderer-Hilfsklasse

Das Beispiel für universelle Windows-Kameraframes bietet eine Hilfsklasse, mit der die Frames aus Farb-, Infrarot- und Tiefenquellen in Ihrer App leicht angezeigt werden können. Normalerweise möchten Sie etwas mehr mit Tiefen- und Infrarotdaten tun, als sie nur auf dem Bildschirm anzuzeigen, aber diese Hilfsklasse ist ein hilfreiches Tool zum Demonstrieren der Framelesefunktion und zum Debuggen Ihrer eigenen Framereader-Implementierung.

Die FrameRenderer-Hilfsklasse implementiert die folgenden Methoden.

  • FrameRenderer-Konstruktor – Der Konstruktor initialisiert die Hilfsklasse, um das XAML Image-Element zu verwenden, das Sie zum Anzeigen von Medienframes übergeben.
  • ProcessFrame – Mit dieser Methode wird ein Medienframe angezeigt, dargestellt durch ein MediaFrameReference-Element im Image-Element, das Sie an den Konstruktor übergeben haben. In der Regel sollten Sie diese Methode aus dem FrameArrived-Ereignishandler aufrufen und den von TryAcquireLatestFrame zurückgegebenen Frame übergeben.
  • ConvertToDisplayableImage – Mit diesen Methoden wird das Format des Medienframes überprüft und bei Bedarf in ein anzeigefähiges Format konvertiert. Bei Farbbildern bedeutet dies, dass das Farbformat BGRA8 ist und dass der Bitmap-Alphamodus prämultipliziert ist. Bei Tiefen- oder Infrarotframes wird jede Scanlinie verarbeitet, um die Tiefen- oder Infrarotwerte in einen Psuedocolor-Farbverlauf zu konvertieren, wobei die PsuedoColorHelper-Klasse verwendet wird, die ebenfalls in der Stichprobe enthalten ist und unten aufgeführt ist.

Hinweis

Um die Pixelbearbeitung für SoftwareBitmap-Bilder durchzuführen, müssen Sie auf einen systemeigenen Speicherpuffer zugreifen. Dazu müssen Sie die IMemoryBufferByteAccess-COM-Schnittstelle verwenden, die in der folgenden Codeauflistung enthalten ist, und Sie müssen die Projekteigenschaften aktualisieren, um die Kompilierung unsicherer Code zu ermöglichen. Weitere Informationen finden Sie unter Erstellen, Bearbeiten und Speichern von Bitmapbildern.

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

Verwenden von MultiSourceMediaFrameReader zum Abrufen von Zeitkernframes aus mehreren Quellen

Ab Windows 10, Version 1607, können Sie MultiSourceMediaFrameReader verwenden, um zeitkernige Frames aus mehreren Quellen zu empfangen. Diese API erleichtert die Verarbeitung, die Frames aus mehreren Quellen erfordert, die in enger zeitlicher Nähe verwendet wurden, z. B. die Verwendung der DepthCorrelatedCoordinateMapper-Klasse. Eine Einschränkung der Verwendung dieser neuen Methode besteht darin, dass Frame-eintreffende Ereignisse nur mit der Geschwindigkeit der langsamsten Aufnahmequelle ausgelöst werden. Zusätzliche Frames aus schnelleren Quellen werden gelöscht. Da das System davon ausgeht, dass Frames mit unterschiedlichen Quellen mit unterschiedlichen Raten eingehen, erkennt es nicht automatisch, ob eine Quelle das Generieren von Frames vollständig beendet hat. Im Beispielcode in diesem Abschnitt wird gezeigt, wie Sie mithilfe eines Ereignisses eine eigene Timeoutlogik erstellen, die aufgerufen wird, wenn korrelierte Frames nicht innerhalb eines app-definierten Zeitlimits eingehen.

Die Schritte zur Verwendung von MultiSourceMediaFrameReader ähneln den Schritten für die Verwendung von MediaFrameReader, die zuvor in diesem Artikel beschrieben wurden. In diesem Beispiel wird eine Farbquelle und eine Tiefenquelle verwendet. Deklarieren Sie einige Zeichenfolgenvariablen, um die Medienframequellen-IDs zu speichern, die zum Auswählen von Frames aus jeder Quelle verwendet werden. Deklarieren Sie als Nächstes ein ManualResetEventSlim, ein CancellationTokenSource und einen EventHandler, der zum Implementieren der Timeoutlogik für das Beispiel verwendet wird.

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;

Mit den in diesem Artikel beschriebenen Techniken können Sie eine MediaFrameSourceGroup abfragen, die die für dieses Beispielszenario erforderlichen Farb- und Tiefenquellen enthält. Rufen Sie nach dem Auswählen der gewünschten Framequellgruppe die MediaFrameSourceInfo für jede Framequelle ab.

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

Erstellen und initialisieren Sie ein MediaCapture-Objekt , und übergeben Sie die ausgewählte Framequellgruppe in den Initialisierungseinstellungen.

mediaCapture = new MediaCapture();

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

await mediaCapture.InitializeAsync(settings);

Rufen Sie nach der Initialisierung des MediaCapture-Objekts MediaFrameSource-Objekte für die Farb- und Tiefenkameras ab. Speichern Sie die ID für jede Quelle, damit Sie den eingehenden Frame für die entsprechende Quelle auswählen können.

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;

Erstellen und initialisieren Sie den MultiSourceMediaFrameReader, indem Sie CreateMultiSourceFrameReaderAsync aufrufen und ein Array von Framequellen übergeben, die der Leser verwendet. Registrieren Sie einen Ereignishandler für das FrameArrived-Ereignis. In diesem Beispiel wird eine Instanz der FrameRenderer-Hilfsklasse erstellt, die zuvor in diesem Artikel beschrieben wurde, um Frames in einem Bildsteuerelement zu rendern. Starten Sie den Frameleser, indem Sie StartAsync aufrufen.

Registrieren Sie einen Ereignishandler für das zuvor im Beispiel deklarierte CorellationFailed-Ereignis . Dieses Ereignis wird signalisiert, wenn eine der verwendeten Medienframequellen die Erstellung von Frames beendet. Rufen Sie schließlich Task.Run auf, um die Timeout-Hilfsmethode NotifyAboutCorrelationFailure in einem separaten Thread aufzurufen. Die Implementierung dieser Methode wird weiter unten in diesem Artikel gezeigt.

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

Das FrameArrived-Ereignis wird ausgelöst, wenn ein neuer Frame aus allen Medienframequellen verfügbar ist, die von MultiSourceMediaFrameReader verwaltet werden. Dies bedeutet, dass das Ereignis im Takt der langsamsten Medienquelle ausgelöst wird. Wenn eine Quelle mehrere Frames in der Zeit erzeugt, in der eine langsamere Quelle einen Frame erzeugt, werden die zusätzlichen Frames aus der schnellen Quelle gelöscht.

Rufen Sie das dem Ereignis zugeordnete MultiSourceMediaFrameReference ab, indem Sie TryAcquireLatestFrame aufrufen. Rufen Sie das MediaFrameReference-Element ab, das jeder Medienframequelle zugeordnet ist, indem Sie TryGetFrameReferenceBySourceId aufrufen und die BEIM Initialisieren des Framelesers gespeicherten ID-Zeichenfolgen übergeben.

Rufen Sie die Set-Methode des ManualResetEventSlim-Objekts auf, um zu signalisieren, dass Frames eingetroffen sind. Wir überprüfen dieses Ereignis in der NotifyCorrelationFailure-Methode , die in einem separaten Thread ausgeführt wird.

Führen Sie schließlich alle Verarbeitungen für zeitkorrelierte Medienframes durch. In diesem Beispiel wird einfach der Frame aus der Tiefenquelle angezeigt.

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

Die NotifyCorrelationFailure-Hilfsmethode wurde auf einem separaten Thread ausgeführt, nachdem der Frameleser gestartet wurde. Überprüfen Sie in dieser Methode, ob das frame received-Ereignis signalisiert wurde. Denken Sie daran, dass wir dieses Ereignis im FrameArrived-Handler immer dann festlegen, wenn eine Reihe korrelierter Frames eintreffen. Wenn das Ereignis für einen bestimmten app-definierten Zeitraum nicht signalisiert wurde – 5 Sekunden ist ein angemessener Wert – und die Aufgabe wurde nicht mit dem CancellationToken abgebrochen, dann ist es wahrscheinlich, dass eine der Medienframequellen das Lesen von Frames beendet hat. In diesem Fall möchten Sie in der Regel den Frameleser herunterfahren, sodass das appdefinierte CorrelationFailed-Ereignis ausgelöst wird. Im Handler für dieses Ereignis können Sie den Framereader beenden und die zugeordneten Ressourcen bereinigen, wie zuvor in diesem Artikel gezeigt.

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

Verwenden des Gepufferten Frameerfassungsmodus zum Beibehalten der Sequenz der abgerufenen Frames

Ab Windows 10, Version 1709, können Sie die AcquisitionMode-Eigenschaft eines MediaFrameReader - oder MultiSourceMediaFrameReader-Objekts auf "Buffered " festlegen, um die Sequenz von Frames beizubehalten, die von der Framequelle an Ihre App übergeben werden.

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

Wenn mehrere Frames aus der Quelle abgerufen werden, während Ihre App das FrameArrived-Ereignis für einen vorherigen Frame weiterhin verarbeitet, sendet das System Ihre App im Standarderfassungsmodus den zuletzt erworbenen Frame und gibt zusätzliche Frames ab, die auf den Puffer warten. Dadurch erhält Ihre App jederzeit den aktuellsten verfügbaren Frame. Dies ist in der Regel der nützlichste Modus für Echtzeit-Computer-Vision-Anwendungen.

Im Puffererfassungsmodus behält das System alle Frames im Puffer bei und stellt sie ihrer App über das FrameArrived-Ereignis in der empfangenen Reihenfolge bereit. Beachten Sie, dass in diesem Modus, wenn der Puffer des Systems für Frames gefüllt ist, das System den Erwerb neuer Frames beendet, bis Ihre App das FrameArrived-Ereignis für vorherige Frames abgeschlossen hat und mehr Speicherplatz im Puffer freigibt.

Verwenden von MediaSource zum Anzeigen von Frames in einem MediaPlayerElement

Ab Windows, Version 1709, können Sie Frames anzeigen, die von einem MediaFrameReader erworben wurden, direkt in einem MediaPlayerElement-Steuerelement auf Ihrer XAML-Seite. Dies wird mithilfe der MediaSource.CreateFromMediaFrameSource zum Erstellen eines MediaSource-Objekts erreicht, das direkt von einem MediaPlayer verwendet werden kann, der einem MediaPlayerElement zugeordnet ist. Ausführliche Informationen zum Arbeiten mit MediaPlayer und MediaPlayerElement finden Sie unter Wiedergeben von Audio und Video mit MediaPlayer.

Die folgenden Codebeispiele zeigen Eine einfache Implementierung, die die Frames von einer nach vorne gerichteten und nach hinten gerichteten Kamera gleichzeitig auf einer XAML-Seite anzeigt.

Fügen Sie ihrer XAML-Seite zunächst zwei MediaPlayerElement-Steuerelemente hinzu.

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

Wählen Sie als Nächstes mithilfe der in den vorherigen Abschnitten in diesem Artikel gezeigten Techniken eine MediaFrameSourceGroup aus, die MediaFrameSourceInfo-Objekte für Farbkameras auf der Vorderseite und im Hintergrundbereich enthält. Beachten Sie, dass der MediaPlayer Frames aus Nicht-Farbformaten, z. B. tiefen- oder Infrarotdaten, nicht automatisch in Farbdaten konvertiert. Die Verwendung anderer Sensortypen kann zu unerwarteten Ergebnissen führen.

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

Initialisieren Sie das MediaCapture-Objekt , um die ausgewählte MediaFrameSourceGroup zu verwenden.

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

Rufen Sie schließlich MediaSource.CreateFromMediaFrameSource auf, um eine MediaSource für jede Framequelle zu erstellen, indem Sie die ID-Eigenschaft des zugeordneten MediaFrameSourceInfo-Objekts verwenden, um eine der Framequellen in der FrameSources-Auflistung des MediaCapture-Objekts auszuwählen. Initialisieren Sie ein neues MediaPlayer-Objekt, und weisen Sie es einem MediaPlayerElement zu, indem Sie SetMediaPlayer aufrufen. Legen Sie dann die Source-Eigenschaft auf das neu erstellte MediaSource-Objekt fest.

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;

Verwenden von Videoprofilen zum Auswählen einer Framequelle

Ein Kameraprofil, dargestellt durch ein MediaCaptureVideoProfile -Objekt, stellt eine Reihe von Funktionen dar, die ein bestimmtes Aufnahmegerät bereitstellt, z. B. Bildfrequenzen, Auflösungen oder erweiterte Features wie HDR-Aufnahme. Ein Aufnahmegerät unterstützt möglicherweise mehrere Profile, sodass Sie das für Ihr Aufnahmeszenario optimierte auswählen können. Ab Windows 10, Version 1803, können Sie MediaCaptureVideoProfile verwenden, um eine Medienframequelle mit bestimmten Funktionen auszuwählen, bevor Sie das MediaCapture-Objekt initialisieren. Die folgende Beispielmethode sucht nach einem Videoprofil, das HDR mit Wide Color Gamut (WCG) unterstützt, und gibt ein MediaCaptureInitializationSettings -Objekt zurück, mit dem mediaCapture initialisiert werden kann, um das ausgewählte Gerät und Profil zu verwenden.

Rufen Sie zunächst MediaFrameSourceGroup.FindAllAsync auf, um eine Liste aller Medienframequellgruppen abzurufen, die auf dem aktuellen Gerät verfügbar sind. Führen Sie eine Schleife durch jede Quellgruppe aus, und rufen Sie MediaCapture.FindKnownVideoProfiles auf, um eine Liste aller Videoprofile für die aktuelle Quellgruppe zu erhalten, die das angegebene Profil unterstützen, in diesem Fall HDR mit Fotos mit breiter Farbskala (Wide Color Gamut, WCG). Wenn ein Profil gefunden wird, das die Kriterien erfüllt, erstellen Sie ein neues MediaCaptureInitializationSettings-Objekt, und legen Sie VideoProfile auf das ausgewählte Profil und VideoDeviceId auf die Id-Eigenschaft der aktuellen Medienframequellgruppe fest.

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

Weitere Informationen zur Verwendung von Kameraprofilen finden Sie unter Kameraprofile.