Udostępnij przez


Przetwarzanie ramek multimedialnych za pomocą MediaFrameReader

W tym artykule pokazano, jak używać MediaFrameReader z MediaCapture w celu uzyskania ramek multimedialnych z co najmniej jednego dostępnego źródła, w tym kolorów, głębokości i kamer podczerwieni, urządzeń audio, a nawet niestandardowych źródeł ramek, takich jak te, które generują ramki śledzenia szkieletów. Ta funkcja została zaprojektowana do użycia przez aplikacje, które wykonują przetwarzanie klatek medialnych w czasie rzeczywistym, takie jak aplikacje rzeczywistości rozszerzonej i aparaty obsługujące głębię.

Jeśli interesuje Cię po prostu przechwytywanie wideo lub zdjęć, takich jak typowa aplikacja fotograficzna, prawdopodobnie chcesz użyć jednej z innych technik przechwytywania obsługiwanych przez MediaCapture. Aby uzyskać listę dostępnych technik przechwytywania multimediów i artykułów pokazujących sposób ich używania, zobacz Camera.

Uwaga / Notatka

Funkcje omówione w tym artykule są dostępne tylko w systemie Windows 10 w wersji 1607.

Uwaga / Notatka

Istnieje przykład aplikacji uniwersalnej systemu Windows, który demonstruje używanie elementu MediaFrameReader do wyświetlania ramek z różnych źródeł ramek, w tym kolorów, głębokości i kamer podczerwieni. Aby uzyskać więcej informacji, zobacz przykładowe ramki aparatu.

Uwaga / Notatka

Wprowadzono nowy zestaw interfejsów API do używania MediaFrameReader z danymi audio w systemie Windows 10 w wersji 1803. Aby uzyskać więcej informacji, zobacz Przetwarzanie ramek audio za pomocą MediaFrameReader.

Wybieranie źródeł klatek i grup źródeł klatek

Wiele aplikacji, które przetwarzają ramki multimediów, musi pobierać ramki z wielu źródeł jednocześnie, takich jak kamery kolorów i głębokości urządzenia. Obiekt MediaFrameSourceGroup reprezentuje zestaw źródeł ramek multimedialnych, które mogą być używane jednocześnie. Wywołaj metodę statyczną MediaFrameSourceGroup.FindAllAsync, aby uzyskać listę wszystkich grup źródeł ramek obsługiwanych przez bieżące urządzenie.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

Możesz również utworzyć DeviceWatcher za pomocą DeviceInformation.CreateWatcher i wartości zwróconej z MediaFrameSourceGroup.GetDeviceSelector, aby otrzymywać powiadomienia o zmianach dostępnych grup źródeł ramek na urządzeniu, na przykład wtedy, gdy podłączana jest zewnętrzna kamera. Aby uzyskać więcej informacji, zobacz Wyliczanie urządzeń.

Obiekt MediaFrameSourceGroup zawiera kolekcję obiektów MediaFrameSourceInfo, które opisują źródła ramek zawarte w grupie. Po pobraniu grup źródłowych ramek dostępnych na urządzeniu możesz wybrać grupę, która uwidacznia interesujące Cię źródła ramek.

Poniższy przykład przedstawia najprostszy sposób wybierania grupy źródłowej ramki. Ten kod po prostu iteruje przez wszystkie dostępne grupy, a następnie wykonuje iterację dla każdego elementu w kolekcji SourceInfos. Każda MediaFrameSourceInfo jest sprawdzana, czy obsługuje ona szukane funkcje. W tym przypadku właściwość MediaStreamType jest sprawdzana pod kątem wartości VideoPreview, co oznacza, że urządzenie udostępnia strumień podglądu wideo, a właściwość SourceKind jest sprawdzana pod kątem wartości Color, co wskazuje, że źródło zawiera ramki kolorów.

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

Ta metoda identyfikowania żądanej grupy źródłowej klatek i źródeł klatek działa w prostych przypadkach, ale jeśli chcesz wybrać źródła klatek na podstawie bardziej złożonych kryteriów, może szybko stać się skomplikowane. Inną metodą jest użycie składni Linq i obiektów anonimowych w celu dokonania wyboru. W poniższym przykładzie użyto metody rozszerzenia Select, aby przekształcić obiekty MediaFrameSourceGroup na liście frameSourceGroups w obiekt anonimowy z dwoma polami: sourceGroup, reprezentującą samą grupę i colorSourceInfo, która reprezentuje źródło ramki kolorów w grupie. Pole colorSourceInfo jest ustawione na wynik FirstOrDefault, który wybiera pierwszy obiekt, dla którego podany predykat rozpoznaje wartość true. W takim przypadku predykat ma wartość true, jeśli typ strumienia to VideoPreview, rodzaj źródła to Colororaz jeśli kamera znajduje się na przednim panelu urządzenia.

Z listy anonimowych obiektów zwróconych z powyższego zapytania metoda rozszerzająca Where jest używana do wyboru tylko tych obiektów, w których pole colorSourceInfo nie jest null. Na koniec wywoływana jest metoda FirstOrDefault, aby wybrać pierwszy element z listy.

Teraz możesz użyć pól wybranego obiektu, aby uzyskać odwołania do wybranej MediaFrameSourceGroup i obiektu MediaFrameSourceInfo reprezentującego kamerę kolorową. Zostaną one użyte później do zainicjowania obiektu MediaCapture i utworzenia MediaFrameReader dla wybranego źródła. Na koniec należy sprawdzić, czy grupa źródłowa ma wartość null, co oznacza, że bieżące urządzenie nie ma żądanych źródeł przechwytywania.

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

W poniższym przykładzie użyto podobnej techniki, jak opisano powyżej, aby wybrać grupę źródłową zawierającą kamery kolorów, głębokości i podczerwieni.

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

Uwaga / Notatka

Począwszy od systemu Windows 10 w wersji 1803, można użyć klasy MediaCaptureVideoProfile, aby wybrać źródło ramki multimedialnej z zestawem żądanych możliwości. Aby uzyskać więcej informacji, zobacz sekcję Używanie profilów wideo do wybierania źródła klatki w dalszej części tego artykułu.

Zainicjuj obiekt MediaCapture, aby użyć wybranej grupy źródłowej ramki

Następnym krokiem jest zainicjowanie obiektu MediaCapture w celu użycia grupy źródła ramek wybranej w poprzednim kroku. Utwórz wystąpienie obiektu MediaCapture przez wywołanie konstruktora. Następnie utwórz obiekt MediaCaptureInitializationSettings, który będzie używany do inicjowania obiektu MediaCapture. W tym przykładzie są używane następujące ustawienia:

  • SourceGroup — informuje system, której grupy źródłowej będziesz używać w celu pobierania ramek. Należy pamiętać, że grupa źródłowa definiuje zestaw źródeł ramek multimedialnych, które mogą być używane jednocześnie.
  • SharingMode — informuje system, czy potrzebujesz wyłącznej kontroli nad urządzeniami źródłowymi przechwytywania. Jeśli ustawisz tę opcję na ExclusiveControl, oznacza to, że można zmienić ustawienia urządzenia przechwytywania, takie jak format generowanych ramek, ale oznacza to, że jeśli inna aplikacja ma już wyłączną kontrolę, aplikacja zakończy się niepowodzeniem podczas próby zainicjowania urządzenia przechwytywania multimediów. Jeśli ustawisz tę opcję na wartość SharedReadOnly, możesz odbierać ramki ze źródeł ramek, nawet jeśli są one używane przez inną aplikację, ale nie można zmienić ustawień dla urządzeń.
  • MemoryPreference — jeśli określisz pamięć procesora, system użyje pamięci procesora, co gwarantuje, że po nadejściu ramek będą one dostępne jako obiekty SoftwareBitmap. Jeśli określisz auto, system będzie dynamicznie wybierać optymalną lokalizację pamięci do przechowywania ramek. Jeśli system wybierze użycie pamięci GPU, ramki multimedialne będą dostarczane jako obiekt IDirect3DSurface, a nie jako SoftwareBitmap.
  • StreamingCaptureMode — ustaw tę wartość na wideo, aby wskazać, że dźwięk nie musi być przesyłany strumieniowo.

Wywołaj InitializeAsync, aby zainicjować MediaCapture z preferowanymi ustawieniami. Pamiętaj, aby wywołać tę metodę w ramach spróbuj bloku w przypadku niepowodzenia inicjowania.

m_mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await m_mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

Ustawianie preferowanego formatu dla źródła ramki

Aby ustawić preferowany format dla źródła ramki, należy uzyskać obiekt MediaFrameSource reprezentujący źródło. Ten obiekt można uzyskać, uzyskując dostęp do słownika ramek zainicjowanego obiektu MediaCapture , określając identyfikator źródła ramki, którego chcesz użyć. Dlatego zapisaliśmy obiekt MediaFrameSourceInfo podczas wybierania grupy źródłowej ramki.

Właściwość MediaFrameSource.SupportedFormats zawiera listę obiektów MediaFrameFormat opisujących obsługiwane formaty źródła ramki. W tym przykładzie wybrano format, który ma szerokość 1080 pikseli i może dostarczać ramki w formacie 32-bitowym RGB. Metoda rozszerzenia FirstOrDefault wybiera pierwszy wpis na liście. Jeśli wybrany format ma wartość null, żądany format nie jest obsługiwany przez źródło ramki. Jeśli format jest obsługiwany, możesz zażądać użycia tego formatu przez wywołanie SetFormatAsync.

var colorFrameSource = m_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);

Tworzenie czytnika ramek dla źródła ramki

Aby odbierać ramki dla źródła ramki multimedialnej, użyj MediaFrameReader.

MediaFrameReader m_mediaFrameReader;

Utwórz wystąpienie czytnika ramek, wywołując createFrameReaderAsync na zainicjowanym obiekcie MediaCapture. Pierwszym argumentem tej metody jest źródło ramek, z którego chcesz je odbierać. Można utworzyć oddzielny czytnik ramek dla każdego źródła ramki, którego chcesz użyć. Drugi argument informuje system o formacie danych wyjściowych, w którym chcesz, aby ramki były dostarczane. Pozwala to zaoszczędzić na konieczności wykonywania własnych konwersji na ramki w miarę ich nadejścia. Należy pamiętać, że jeśli określisz format, który nie jest obsługiwany przez źródło ramki, zostanie zgłoszony wyjątek, dlatego upewnij się, że ta wartość znajduje się w kolekcji SupportedFormats.

Po utworzeniu czytnika ramek zarejestruj procedurę obsługi zdarzenia FrameArrived, które jest zgłaszane za każdym razem, gdy nowa ramka jest dostępna ze źródła.

Zleć systemowi rozpoczęcie odczytywania ramek ze źródła, wywołując StartAsync.

m_mediaFrameReader = await m_mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
m_mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await m_mediaFrameReader.StartAsync();

Obsługa zdarzenia dostarczonego ramki

Zdarzenie MediaFrameReader.FrameArrived jest zgłaszane za każdym razem, gdy jest dostępna nowa ramka. Możesz przetwarzać każdą ramkę, która dociera, lub używać ramek tylko wtedy, gdy ich potrzebujesz. Ponieważ czytnik ramek zgłasza zdarzenie we własnym wątku, może być konieczne zaimplementowanie logiki synchronizacji, aby upewnić się, że nie próbujesz uzyskać dostępu do tych samych danych z wielu wątków. W tej sekcji przedstawiono sposób synchronizowania ramek kolorów rysunku z kontrolką obrazu na stronie XAML. Ten scenariusz dotyczy dodatkowego ograniczenia synchronizacji, które wymaga wykonania wszystkich aktualizacji kontrolek XAML w wątku interfejsu użytkownika.

Pierwszym krokiem wyświetlania ramek w języku XAML jest utworzenie kontrolki Obraz.

<Image x:Name="iFrameReaderImageControl" MaxWidth="300" MaxHeight="200"/>

W kodzie za stroną zadeklaruj zmienną składową klasy typu SoftwareBitmap, która będzie używana jako bufor wsteczny, do którego zostaną skopiowane wszystkie obrazy przychodzące. Należy pamiętać, że same dane obrazu nie są kopiowane, a tylko odwołania do obiektów. Ponadto zadeklaruj zmienną logiczną, aby śledzić, czy nasza operacja interfejsu użytkownika jest obecnie uruchomiona.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

Ponieważ ramki pojawią się jako obiekty SoftwareBitmap, należy utworzyć obiekt SoftwareBitmapSource, który umożliwia użycie SoftwareBitmap jako źródłacontrol XAML. Przed rozpoczęciem czytnika klatek należy ustawić źródło obrazu w kodzie.

iFrameReaderImageControl.Source = new SoftwareBitmapSource();

Nadszedł czas na zaimplementowanie programu obsługi zdarzeń FrameArrived. Kiedy obsługiwane jest wywołanie, parametr nadawca zawiera odwołanie do obiektu MediaFrameReader, który zainicjował zdarzenie. Wywołaj TryAcquireLatestFrame na tym obiekcie, aby spróbować pobrać najnowszą ramkę. Jak wskazuje nazwa, TryAcquireLatestFrame może nie udać się uzyskanie najnowszej ramki. Dlatego podczas uzyskiwania dostępu do właściwości VideoMediaFrame i SoftwareBitmap pamiętaj, aby przetestować wartość null. W tym przykładzie operator warunkowy o wartości null ? służy do uzyskiwania dostępu do SoftwareBitmap, a następnie pobrany obiekt jest sprawdzany pod kątem wartości null.

Kontrolka Obraz może wyświetlać obrazy tylko w formacie BRGA8 z wstępnie pomnożona lub bez alfa. Jeśli ramka przychodząca nie jest w tym formacie, metoda statyczna Konwertuj jest używana do konwertowania mapy bitowej oprogramowania na poprawny format.

Następnie metoda Interlocked.Exchange służy do zamiany odwołania do przychodzącej mapy bitowej z mapą bitową backbuffer. Ta metoda zamienia te odwołania w operacji atomowej, która jest bezpieczna dla wątków. Po zamianie stary obraz backbuffer, który teraz jest przechowywany w zmiennej softwareBitmap, zostaje usunięty w celu zwolnienia zasobów.

Następnie CoreDispatcher skojarzony z elementem Image służy do utworzenia zadania uruchamianego w wątku interfejsu użytkownika przez wywołanie RunAsync. Ponieważ zadania asynchroniczne będą wykonywane w ramach zadania, wyrażenie lambda przekazane do RunAsync jest zadeklarowane za pomocą słowa kluczowego async.

W ramach zadania sprawdzana jest zmienna _taskRunning, aby upewnić się, że w danym momencie jest uruchomione tylko jedno wystąpienie zadania. Jeśli zadanie nie jest jeszcze uruchomione, _taskRunning jest ustawiona na wartość true, aby zapobiec ponownemu uruchomieniu zadania. W podczas pętliInterlocked.Exchange jest wywoływana w celu skopiowania z backbuffer do tymczasowego SoftwareBitmap do momentu, aż obraz backbuffer ma wartość null. Za każdym razem, gdy wypełniana jest tymczasowa bitmapa, właściwość Source obiektu Image jest rzutowana na typ SoftwareBitmapSource, a następnie wywoływana jest metoda SetBitmapAsync, aby ustawić źródło obrazu.

Na koniec zmienna _taskRunning jest ustawiona na wartość false, aby można było uruchomić zadanie ponownie przy następnym wywołaniu programu obsługi.

Uwaga / Notatka

Jeśli uzyskujesz dostęp do obiektów SoftwareBitmap lub Direct3DSurface udostępnianych przez właściwość VideoMediaFrame obiektu MediaFrameReference, system tworzy silne odwołanie do tych obiektów, co oznacza, że nie zostaną one usunięte podczas wywoływania Dispose na zawierającym je obiekcie MediaFrameReference. Należy bezpośrednio wywołać metodę Dispose dla SoftwareBitmap lub Direct3DSurface w celu natychmiastowego usunięcia obiektów. W przeciwnym razie moduł zbierający nieużytki ostatecznie zwolni pamięć dla tych obiektów, ale nie można przewidzieć, kiedy to nastąpi. Jeśli liczba przydzielonych bitmap lub powierzchni przekroczy maksymalną ilość dozwoloną przez system, przepływ nowych ramek zostanie chwilowo zatrzymany. Można na przykład skopiować pobrane ramki przy użyciu metody SoftwareBitmap.Copy, a następnie zwolnić oryginalne ramki, aby przezwyciężyć to ograniczenie. Ponadto w przypadku utworzenia MediaFrameReader przy użyciu przeciążenia CreateFrameReaderAsync(Windows.Media.Capture.Frame.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSizeSize) lub CreateFrameReaderAsync(Windows.Media.Capture.Frame.MediaFrameSource InputSource, System.String outputSubtype), zwracane ramki są kopiami oryginalnych danych ramek i dlatego nie powodują zatrzymania pozyskiwania ramek po ich zatrzymaniu.

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 = iFrameReaderImageControl.DispatcherQueue.TryEnqueue(
            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)iFrameReaderImageControl.Source;
                    await imageSource.SetBitmapAsync(latestBitmap);
                    latestBitmap.Dispose();
                }

                taskRunning = false;
            });
    }

    if (mediaFrameReference != null)
    {
        mediaFrameReference.Dispose();
    }
}

Oczyszczanie zasobów

Po zakończeniu odczytywania ramek pamiętaj, aby zatrzymać czytnik ramek medialnych przez wywołanie StopAsync, wyrejestrowanie obsługi FrameArrived i zutylizowanie obiektu MediaCapture.

await m_mediaFrameReader.StopAsync();
m_mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
m_mediaCapture.Dispose();
m_mediaCapture = null;

Aby uzyskać więcej informacji na temat czyszczenia obiektów przechwytywania multimediów w przypadku wstrzymania aplikacji, zobacz Show the camera preview in a WinUI 3 app (Wyświetlanie podglądu aparatu w aplikacji WinUI 3).

Klasa pomocnika FrameRenderer

Ta sekcja zawiera pełną listę kodu dla klasy pomocniczej, która ułatwia wyświetlanie ramek ze źródeł kolorów, podczerwieni i głębokości w aplikacji. Zazwyczaj chcesz zrobić coś więcej z danymi głębi i podczerwieni niż tylko wyświetlić je na ekranie, ale ta klasa pomocnika jest przydatnym narzędziem do demonstrowania funkcji czytnika ramek i debugowania własnej implementacji czytnika ramek. Kod w tej sekcji został dostosowany z przykładu Ramek Aparatu.

Klasa pomocnika FrameRenderer implementuje następujące metody.

  • Konstruktor FrameRenderer — konstruktor inicjuje klasę pomocnika, aby użyć elementu Image XAML, który został przekazany do wyświetlania ramek multimedialnych.
  • ProcessFrame — ta metoda wyświetla ramkę multimediów reprezentowaną przez element MediaFrameReferencew elemencie Obraz, który został przekazany do konstruktora. Zazwyczaj należy wywołać tę metodę z programu obsługi zdarzeń FrameArrived, przekazując ramkę zwróconą przez TryAcquireLatestFrame.
  • ConvertToDisplayableImage — ta metoda sprawdza format ramki multimedialnej i w razie potrzeby konwertuje go na format wyświetlany. W przypadku obrazów kolorowych oznacza to, że format kolorów to BGRA8 i że tryb alfa mapy bitowej jest wstępnie pomnożony. W przypadku ramek głębokości lub podczerwieni każda linia skanowa jest przetwarzana w celu przekonwertowania wartości głębokości lub podczerwieni na gradient psuedocolor, przy użyciu PsuedoColorHelper klasy, która jest również uwzględniona w próbce i wymienione poniżej.

Uwaga / Notatka

Aby można było manipulować pikselami na obrazach SoftwareBitmap, należy uzyskać dostęp do natywnego buforu pamięci. W tym celu należy użyć interfejsu IMemoryBufferByteAccess COM zawartego na poniższej liście kodu i zaktualizować właściwości projektu, aby umożliwić kompilację niebezpiecznego kodu. Aby uzyskać więcej informacji, zobacz Tworzenie, edytowanie i zapisywanie obrazów map bitowych.

[GeneratedComInterface]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe partial interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}
class FrameRenderer
{
    private Image m_imageElement;
    private SoftwareBitmap m_backBuffer;
    private bool _taskRunning = false;

    public FrameRenderer(Image imageElement)
    {
        m_imageElement = imageElement;
        m_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 m_backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref m_backBuffer, softwareBitmap);

            // UI thread always reset m_backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = m_imageElement.DispatcherQueue.TryEnqueue(
                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 m_backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)m_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 m_backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref m_backBuffer, softwareBitmap);

            // UI thread always reset m_backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = m_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 m_backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)m_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }
}

Użyj elementu MultiSourceMediaFrameReader, aby uzyskać skorelowane czasowo ramki z wielu źródeł

Począwszy od systemu Windows 10 w wersji 1607, można użyć multiSourceMediaFrameReader , aby odbierać skorelowane czasowo ramki z wielu źródeł. Ten interfejs API ułatwia przetwarzanie, które wymaga użycia ramek z wielu źródeł, które zostały wykonane w bliskim odstępie czasowym, na przykład wykorzystując klasę DepthCorrelatedCoordinateMapper. Jednym z ograniczeń stosowania tej nowej metody jest to, że zdarzenia dostarczone przez ramkę są wywoływane tylko w przypadku najwolniejszego źródła przechwytywania. Dodatkowe ramki z szybszych źródeł zostaną pominięte. Ponadto, ponieważ system oczekuje, że ramki docierają z różnych źródeł o różnych szybkościach, nie rozpoznaje automatycznie, czy źródło całkowicie przestało generować ramki. Przykładowy kod w tej sekcji pokazuje, jak za pomocą zdarzenia utworzyć własną logikę limitu czasu, która jest wywoływana, jeśli skorelowane ramki nie docierają do limitu czasu zdefiniowanego przez aplikację.

Kroki korzystania z MultiSourceMediaFrameReader są podobne do kroków dotyczących używania MediaFrameReader opisanych wcześniej w tym artykule. W tym przykładzie zostaną użyte źródła koloru i głębokości. Zadeklaruj zmienne tekstowe do przechowywania identyfikatorów źródeł ramek multimedialnych, które będą używane do wybierania ramek z każdego źródła. Następnie zadeklaruj ManualResetEventSlim, CancellationTokenSourceoraz EventHandler, który posłuży do implementacji logiki limitu czasu w tym przykładzie.

private MultiSourceMediaFrameReader m_multiFrameReader = null;
private string m_colorSourceId = null;
private string m_depthSourceId = null;


private readonly ManualResetEventSlim m_frameReceived = new ManualResetEventSlim(false);
private readonly CancellationTokenSource m_tokenSource = new CancellationTokenSource();
public event EventHandler CorrelationFailed;

Korzystając z technik opisanych wcześniej w tym artykule, wykonaj zapytanie dotyczące MediaFrameSourceGroup zawierające źródła kolorów i głębokości wymagane w tym przykładowym scenariuszu. Po wybraniu żądanej grupy źródła ramki pobierz MediaFrameSourceInfo dla każdego źródła ramki.

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

Utwórz i zainicjuj obiekt MediaCapture, przekazując wybraną grupę źródłową ramki w ustawieniach inicjowania.

m_mediaCapture = new MediaCapture();

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

await m_mediaCapture.InitializeAsync(settings);

Po zainicjowaniu obiektu MediaCapture pobierz obiekty MediaFrameSource dla kamer kolorów i głębokości. Zapisz identyfikator dla każdego źródła, aby można było wybrać ramkę przychodzącą dla odpowiedniego źródła.

MediaFrameSource colorSource =
    m_mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Color);

MediaFrameSource depthSource =
    m_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;
}

m_colorSourceId = colorSource.Info.Id;
m_depthSourceId = depthSource.Info.Id;

Utwórz i zainicjuj MultiSourceMediaFrameReader, wywołując CreateMultiSourceFrameReaderAsync i przekazując tablicę źródeł ramek, które czytnik będzie używał. Zarejestruj program obsługi zdarzeń dla zdarzenia FrameArrived. W tym przykładzie utworzono instancję pomocniczej klasy FrameRenderer, opisanej wcześniej w tym artykule, aby renderować ramki na kontrolce obrazu . Uruchom czytnik ramek, wywołując StartAsync.

Zarejestruj procedurę obsługi dla zdarzenia CorellationFailed, które zostało zadeklarowane wcześniej w przykładu. Zasygnalizujemy to zdarzenie, jeśli jedno ze używanych źródeł ramek multimedialnych przestanie produkować ramki. Na koniec wywołaj Task.Run, aby wywołać metodę pomocnika limitu czasu, NotifyAboutCorrelationFailure, w osobnym wątku. Implementacja tej metody jest pokazana w dalszej części tego artykułu.

m_multiFrameReader = await m_mediaCapture.CreateMultiSourceFrameReaderAsync(
    new[] { colorSource, depthSource });

m_multiFrameReader.FrameArrived += MultiFrameReader_FrameArrived;

m_frameRenderer = new FrameRenderer(iFrameReaderImageControl);

MultiSourceMediaFrameReaderStartStatus startStatus =
    await m_multiFrameReader.StartAsync();

if (startStatus != MultiSourceMediaFrameReaderStartStatus.Success)
{
    throw new InvalidOperationException(
        "Unable to start reader: " + startStatus);
}

this.CorrelationFailed += MainWindow_CorrelationFailed;
Task.Run(() => NotifyAboutCorrelationFailure(m_tokenSource.Token));

Zdarzenie FrameArrived jest zgłaszane za każdym razem, gdy nowa ramka jest dostępna ze wszystkich źródeł ramek multimedialnych zarządzanych przez MultiSourceMediaFrameReader. Oznacza to, że zdarzenie zostanie wywołane w rytmie najwolniejszego źródła multimediów. Jeśli jedno źródło generuje wiele ramek w czasie, gdy wolniejsze źródło generuje jedną ramkę, dodatkowe ramki z szybkiego źródła zostaną porzucone.

Pobierz MultiSourceMediaFrameReference skojarzone ze zdarzeniem, wywołując TryAcquireLatestFrame. Pobierz MediaFrameReference skojarzone ze źródłem każdej ramki multimedialnej, wywołując TryGetFrameReferenceBySourceId, przekazując identyfikatory zapisane podczas inicjowania czytnika ramek.

Wywołaj metodę Set obiektu ManualResetEventSlim, aby zasygnalizować, że ramki dotarły. To zdarzenie sprawdzimy w metodzie NotifyCorrelationFailure uruchomionej w osobnym wątku.

Na koniec przeprowadź dowolne przetwarzanie w ramkach multimedialnych skorelowanych czasowo. Ten przykład po prostu wyświetla kadr z źródła danych głębokości.

private void MultiFrameReader_FrameArrived(MultiSourceMediaFrameReader sender, MultiSourceMediaFrameArrivedEventArgs args)
{
    using (MultiSourceMediaFrameReference muxedFrame =
        sender.TryAcquireLatestFrame())
    using (MediaFrameReference colorFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(m_colorSourceId))
    using (MediaFrameReference depthFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(m_depthSourceId))
    {
        // Notify the listener thread that the frame has been received.
        m_frameReceived.Set();
        m_frameRenderer.ProcessFrame(depthFrame);
    }
}

Metoda pomocnika NotifyCorrelationFailure została uruchomiona w osobnym wątku po uruchomieniu czytnika ramek. Przy użyciu tej metody sprawdź, czy odebrane zdarzenie ramki zostało zasygnalizowane. Pamiętaj, że w programie obsługi FrameArrived ustawiamy to zdarzenie za każdym razem, gdy pojawi się zestaw skorelowanych ramek. Jeśli zdarzenie nie zostało zasygnalizowane przez jakiś czas zdefiniowany przez aplikację — 5 sekund jest rozsądną wartością — a zadanie nie zostało anulowane przy użyciu CancellationToken, prawdopodobnie jedno ze źródeł ramek multimedialnych przestało odczytywać ramki. W takim przypadku zazwyczaj chcesz zamknąć czytnik ramek, więc zgłoś zdarzenie CorrelationFailed zdefiniowane przez aplikację. W procedurze obsługi tego zdarzenia można zatrzymać czytnik ramek i wyczyścić skojarzone zasoby, jak pokazano wcześniej w tym artykule.

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, m_frameReceived.WaitHandle }, 5000)
            == WaitHandle.WaitTimeout)
    {
        CorrelationFailed?.Invoke(this, EventArgs.Empty);
    }
}
private async void MainWindow_CorrelationFailed(object sender, EventArgs e)
{
    await m_multiFrameReader.StopAsync();
    m_multiFrameReader.FrameArrived -= MultiFrameReader_FrameArrived;
    m_mediaCapture.Dispose();
    m_mediaCapture = null;
}

Użyj trybu buforowanego przechwytywania ramek, aby zachować sekwencję uzyskanych ramek

Począwszy od systemu Windows 10 w wersji 1709, można ustawić właściwość AcquisitionModeMediaFrameReader lub MultiSourceMediaFrameReaderbuforowane, aby zachować sekwencję ramek przekazywanych do aplikacji ze źródła ramek.

m_mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

W domyślnym trybie pozyskiwania czasu rzeczywistego, jeśli wiele ramek jest pobieranych ze źródła, podczas gdy aplikacja nadal obsługuje zdarzenie FrameArrived dla poprzedniej ramki, system wyśle aplikację ostatnio nabytą ramkę i upuści dodatkowe ramki oczekujące w buforze. Zapewnia to aplikacji najnowszą dostępną ramkę w każdej chwili. Jest to zazwyczaj najbardziej przydatny tryb dla aplikacji przetwarzania obrazów w czasie rzeczywistym.

W trybie pozyskiwania buforowanego system zachowa wszystkie klatki w buforze i udostępni je aplikacji przez zdarzenie FrameArrived w otrzymanej kolejności. Należy pamiętać, że w tym trybie, gdy bufor systemu na ramki zostanie wypełniony, system wstrzyma pobieranie nowych ramek, dopóki aplikacja nie ukończy obsługi zdarzenia FrameArrived dla poprzednich ramek, co zwolni więcej miejsca w buforze.

Używanie usługi MediaSource do wyświetlania ramek w elemecie MediaPlayerElement

Począwszy od systemu Windows w wersji 1709, można wyświetlać klatki uzyskane z MediaFrameReader bezpośrednio na stronie XAML w kontrolce MediaPlayerElement. Jest to osiągane przy użyciu MediaSource.CreateFromMediaFrameSource do utworzenia obiektu MediaSource, który może być używany bezpośrednio przez MediaPlayer skojarzony z MediaPlayerElement. Aby uzyskać szczegółowe informacje na temat pracy z MediaPlayer i MediaPlayerElement, zobacz Odtwarzanie audio i wideo za pomocąMediaPlayer.

W poniższych przykładach kodu pokazano prostą implementację, która wyświetla klatki z kamery przedniej i tylnej jednocześnie w stronie XAML.

Najpierw dodaj dwie kontrolki MediaPlayerElement do strony XAML.

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

Następnie, korzystając z technik przedstawionych w poprzednich sekcjach tego artykułu, wybierz MediaFrameSourceGroup, która zawiera MediaFrameSourceInfo obiekty dla kamer kolorowych na przednim i tylnym panelu. Należy pamiętać, że MediaPlayer nie konwertuje ramek z formatów innych niż kolory, takich jak głębokość lub dane podczerwieni, na dane kolorów. Użycie innych typów czujników może spowodować nieoczekiwane wyniki.

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

Zainicjuj obiekt MediaCapture, aby użyć wybranego obiektu MediaFrameSourceGroup.

m_mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await m_mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

Na koniec wywołaj MediaSource.CreateFromMediaFrameSource, aby utworzyć MediaSource dla każdego źródła ramek przy użyciu właściwości Id skojarzonego obiektu MediaFrameSourceInfo, aby wybrać jedno ze źródeł ramek w kolekcji MediaCapture obiektu FrameSources. Zainicjuj nowy obiekt MediaPlayer i przypisz go do MediaPlayerElement, wywołując SetMediaPlayer. Następnie ustaw właściwość Source do nowo utworzonego obiektu MediaSource.

var frameMediaSource1 = MediaSource.CreateFromMediaFrameSource(m_mediaCapture.FrameSources[frontSourceInfo.Id]);
mediaPlayerElement1.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement1.MediaPlayer.Source = frameMediaSource1;
mediaPlayerElement1.AutoPlay = true;

var frameMediaSource2 = MediaSource.CreateFromMediaFrameSource(m_mediaCapture.FrameSources[backSourceInfo.Id]);
mediaPlayerElement2.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement2.MediaPlayer.Source = frameMediaSource2;
mediaPlayerElement2.AutoPlay = true;

Użyj profili wideo, aby wybrać źródło ramki

Profil kamery reprezentowany przez obiekt MediaCaptureVideoProfile reprezentuje zestaw możliwości zapewnianych przez określone urządzenie przechwytywania, takie jak szybkość klatek, rozdzielczości lub zaawansowane funkcje, takie jak przechwytywanie HDR. Urządzenie przechwytywania może obsługiwać wiele profilów, co pozwala wybrać ten, który jest zoptymalizowany pod kątem scenariusza przechwytywania. Począwszy od systemu Windows 10 w wersji 1803, można użyć MediaCaptureVideoProfile, aby wybrać źródło ramki multimedialnej z określonymi możliwościami przed zainicjowaniem obiektu MediaCapture. Poniższy przykładowy kod wyszukuje profil wideo obsługujący HDR z Szeroką Gamą Kolorów (WCG) i zwraca obiekt MediaCaptureInitializationSettings, którego można użyć do zainicjowania MediaCapture aby użyć wybranego urządzenia i profilu.

Najpierw wywołaj MediaFrameSourceGroup.FindAllAsync, aby uzyskać listę wszystkich grup źródłowych ramek multimedialnych dostępnych na bieżącym urządzeniu. Przejdź przez każdą grupę źródłową i wywołaj MediaCapture.FindKnownVideoProfiles, aby uzyskać listę wszystkich profili wideo dla bieżącej grupy źródłowej, które obsługują określony profil, w tym przypadku HDR ze zdjęciem WCG. Jeśli zostanie znaleziony profil spełniający kryteria, utwórz nowy obiekt MediaCaptureInitializationSettings i ustaw VideoProfile na wybrany profil oraz VideoDeviceId na właściwość Id bieżącej grupy źródłowej ramek multimedialnych.

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

Aby uzyskać więcej informacji na temat korzystania z profilów aparatów, zobacz Profile aparatu.