YUV mit Media Foundation in RGB konvertieren

· · Media Foundation, C++, Windows-Entwicklung, Videoverarbeitung, YUV

Sie möchten einen Frame aus einem Video extrahieren, als PNG speichern, an WIC oder GDI übergeben oder in der UI anzeigen. In solchen Fällen möchte die Anwendungsseite eine RGB-Pixelfolge.

Die Frames, die aus einem Media-Foundation-Decoder kommen, liegen aber ganz normal in einem YUV-Format wie NV12 oder YUY2 vor. Behandelt man die rohe Byte-Folge einfach als Bild, entsteht ein eher trauriges Ergebnis: kaputte Farben, Streifenbildung oder ein seltsamer Grünstich.

In einem früheren Artikel, Einführung in Media Foundation – die API aus der COM-Perspektive verstehen, haben wir das große Ganze skizziert, und in Wie Sie mit Media Foundation ein Standbild aus einer MP4 zu einem bestimmten Zeitpunkt extrahieren ging es um die Extraktion von Standbildern. Diesmal befassen wir uns mit dem Schritt dazwischen: der YUV -> RGB-Konvertierung selbst.

In diesem Artikel unterscheiden wir die folgenden zwei Muster.

  • Muster A: IMFSourceReader automatisch bis zu RGB32 konvertieren lassen
  • Muster B: NV12 / YUY2 entgegennehmen und selbst nach RGB konvertieren

Das Ziel ist nicht, sich API-Namen zu merken. Es geht darum, sich vorstellen zu können, wo in Media Foundation YUV auftaucht und wo es zu RGB wird.

Der in diesem Artikel gezeigte Code ist als vollständiges Beispielpaket (C++-Code für Muster A / Muster B, CMake-Konfiguration und Tests für die Pixelkonvertierung) auf GitHub veröffentlicht.

media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)

Voraussetzungen zum Ausführen

Wenn Sie den Code dieses Artikels in Ihr eigenes Projekt übernehmen möchten, benötigen Sie nur Folgendes.

Element Voraussetzung
Betriebssystem Windows 10 oder höher
Compiler MSVC aus Visual Studio 2019 / 2022 (C++17)
SDK Windows SDK (Media-Foundation-Header und Importbibliotheken). Im Visual-Studio-Workload „Desktopentwicklung mit C++“ enthalten
Build Die Beispiele nutzen CMake 3.20 oder höher. Sie können das Visual-Studio-Projekt aber auch von Hand anlegen

Es müssen vier Bibliotheken gelinkt werden. Der Code im Artikel verwendet dafür #pragma comment(lib, ...), aber Sie können sie ebenso gut in den Projekteinstellungen angeben.

  • mfplat.lib
  • mfreadwrite.lib
  • mfuuid.lib
  • ole32.lib

Der Code dieses Artikels setzt außerdem voraus, dass CoInitializeEx und MFStartup bereits ausgeführt wurden. Nur die Pixel-Konvertierungsformel (5.6.) ist betriebssystemunabhängig; im GitHub-Beispiel ist sie deshalb in einen separaten Header ausgelagert, sodass sie sich auch unter Linux mit g++ testen lässt.

1. Das Ergebnis vorweg

Vorab kurz zusammengefasst:

  • Für die Extraktion einiger weniger Standbilder oder die Erzeugung von Thumbnails ist es am bequemsten, MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING zu aktivieren und MFVideoFormat_RGB32 anzufordern
  • Diese automatische Konvertierung ist jedoch Software-Verarbeitung und nicht für die Echtzeitwiedergabe optimiert
  • Wer eine eigene Konvertierung schreibt, kommt am schnellsten voran, wenn er zunächst NV12 und YUY2 richtig versteht
  • YUV -> RGB ist nicht einfach „drei Koeffizienten anwenden, fertig“ — in der Praxis spielen Subsampling, Range, Matrix und Stride mit hinein
  • Die Media-Foundation-Dokumentation verwendet durchgängig den Begriff YUV, aber bei digitalem Video ist es einfacher, ihn im Grunde als Y’CbCr zu lesen
  • Am häufigsten werden Farben in der Praxis dadurch verfälscht, dass MF_MT_YUV_MATRIX und MF_MT_VIDEO_NOMINAL_RANGE nicht geprüft werden, sowie durch die Annahme, der Stride entspreche width * bytesPerPixel

Kurz gesagt: Wenn Sie es sich einfach machen wollen, lassen Sie den Source Reader RGB32 ausgeben. Wenn Sie Massenverarbeitung oder Kontrolle über die Farbe brauchen, nehmen Sie die Frames als YUV entgegen und konvertieren selbst. Das sind die zwei Optionen.

2. Zuerst ein Bild

Es geht schneller, wenn wir uns zunächst anhand eines Diagramms ansehen, was innerhalb von Media Foundation passiert.

Muster AMuster BMP4 / H.264 / HEVCDecoderYUV-Frame wie NV12 / YUY2 / YV12Video Processing des Source ReaderRGB32Eigener KonvertierungscodeBGRA / RGB

Liegt der Inhalt der Videodatei in einem komprimierten Format wie H.264 oder HEVC vor, wandelt der Decoder ihn zunächst in unkomprimierte Frames zurück. Diese unkomprimierten Frames sind nicht zwangsläufig RGB. Im Windows-Video-Bereich ist vielmehr ein YUV-Format der Normalfall.

Wenn die Anwendung also RGB möchte, wählt sie eine der beiden folgenden Optionen.

  1. Media Foundation die Frames bis zu RGB32 konvertieren lassen
  2. YUV entgegennehmen und mit eigenem Code nach RGB umwandeln

Genau um diesen Scheideweg geht es in diesem Artikel.

3. Zuerst die Beziehung zwischen YUV und RGB klären

3.1. Es heißt YUV, ist aber eigentlich Y’CbCr

Windows-API-Namen und -Dokumentation verwenden durchgängig den Begriff YUV. Im Kontext von digitalem Video kann man U jedoch praktisch bedenkenlos als Cb und V als Cr lesen.

Grob gesagt gilt:

  • Y ist der helligkeitsnahe Anteil
  • U / V sind die Farbdifferenzanteile
  • Bei RGB trägt jedes Pixel direkt Rot-, Grün- und Blauwerte

So sieht die Beziehung aus.

Das menschliche Auge ist für die Feinheit der Helligkeit empfindlicher als für die Feinheit der Farbe. Deshalb zahlt sich bei Video ein Design aus, das Y fein und U/V etwas gröber auflöst. Das ist der Grund, warum YUV-Formate so verbreitet sind.

3.2. 4:4:4 / 4:2:2 / 4:2:0 – wie stark die Farbe ausgedünnt wird

Das ist der Kernpunkt beim Lesen von YUV.

Notation Bedeutung Typische Beispiele
4:4:4 Jedes Pixel hat eigene Y/U/V-Werte AYUV, I444
4:2:2 2 horizontal benachbarte Pixel teilen sich ein U/V-Paar YUY2, UYVY, I422
4:2:0 Ein 2x2-Pixel-Block teilt sich ein U/V-Paar NV12, YV12, I420

Es lohnt sich, sich zunächst nur die Struktur der beiden in der Praxis häufigsten Formate anzusehen.

Klären wir vorab einen Begriff: Stride (auch Pitch genannt) ist die Anzahl der Bytes pro Zeile. Er ist nicht die Bildbreite selbst, sondern gibt an, um wie viele Bytes man einschließlich des Paddings am Zeilenende bis zum Anfang der nächsten Zeile vorrückt. In diesem Artikel verwenden wir Stride und Pitch synonym. Auch Microsoft Learn nutzt beide Bezeichnungen, sodass Sie sie nicht unterscheiden müssen.

In den folgenden Diagrammen steht W für Width, H für Height und S für Stride. Wichtig ist: S >= W, aber nicht notwendigerweise S == W.

NV12 (4:2:0, planar) / width = W, height = H, stride = S

  <----------- S Bytes ------------>
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (Padding)           |    |
 | Y Y Y Y Y | (Padding)           |    | Y-Plane
 | Y Y Y Y Y | (Padding)           |    | S * H Bytes
 | Y Y Y Y Y | (Padding)           |    |
 +-----------+---------------------+  --+  <- Plane-Grenze = S * H ab dem Anfang
 | U V U V U | (Padding)           |    |
 | U V U V U | (Padding)           |    | UV-Plane
 +-----------+---------------------+  --+  Höhe ist H / 2 Zeilen

  Y von Zeile y   : yPlane  + S * y
  UV von Zeile y  : uvPlane + S * (y / 2)
  Anfang UV-Plane : scanline0 + S * H

Bei NV12 teilen sich die 4 Pixel eines 2x2-Blocks ein U/V-Paar. Y gibt es für jedes einzelne Pixel. Die UV-Plane verwendet denselben Stride wie die Y-Plane, hat aber nur halb so viele Zeilen. Deshalb liegt die Plane-Grenze bei S * H und nicht bei W * H (dazu mehr in 7.5.).

YUY2 (4:2:2, packed) / width = W, height = H, stride = S

  <-------------- S Bytes ------------------>
  <------- W * 2 Bytes -------->
 +-----------------------------+----------+
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (Padding)|   Zeile 0
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (Padding)|   Zeile 1
 +-----------------------------+----------+

  Anfang von Zeile y : scanline0 + S * y
  2 Pixel = 4 Bytes (Y, U, Y, V)
  Nur eine Plane (packed, also keine Grenze)

Bei YUY2 teilen sich zwei horizontal benachbarte Pixel ein U/V-Paar. Y0 und Y1 sind getrennt, aber U0 und V0 werden geteilt. Da das Format packed ist, entfällt die Berechnung einer Plane-Grenze, aber für den Zeilenwechsel wird trotzdem der Stride verwendet.

An dieser Stelle wird klar, dass YUV -> RGB keine einfache Pixel-für-Pixel-Ersetzung ist. Zunächst muss überlegt werden, wie die gemeinsam genutzten U/V-Werte welchen Pixeln zugeordnet werden.

3.3. YUV -> RGB ist „Farbraumkonvertierung + Sampling-Konvertierung“

Wirft man einen Blick auf Media Foundations Extended Color Information, zeigt sich, dass eine streng korrekte Farbkonvertierung recht viele Stufen umfasst: inverse Quantisierung, Chroma-Upsampling, YUV -> RGB, die Transferfunktion, die Konvertierung der Primärfarben und schließlich die Quantisierung.

Für praktischen 8-Bit-SDR-Code ist es aber leichter verständlich, wenn man zunächst die folgenden drei Ebenen unterscheidet.

  1. Das Subsampling rückgängig machen Die U/V-Werte von 4:2:0 oder 4:2:2 so ausweiten, dass jedes Pixel sie referenzieren kann
  2. Die Range rückgängig machen Video-Y nutzt normalerweise 16..235, U/V 16..240, also diese Skalierung zurücknehmen
  3. Die Matrix anwenden Mit Koeffizienten wie BT.601 oder BT.709 nach RGB konvertieren

Praktisch gesehen bedeutet YUV -> RGB-Konvertierung also, Folgendes zu entscheiden:

  • welches U/V die Farbe dieses Pixels ist
  • mit welchen Koeffizienten dieses Y/U/V zurück in RGB umgerechnet wird

3.4. Gehen Sie mit BT.601 und BT.709 nachlässig um, driften die Farben schleichend

Die Media-Foundation-Dokumentation beschreibt die Beziehung so, dass BT.601 für SDTV und darunter bevorzugt wird, BT.709 dagegen für Video oberhalb von SD.

Es ist aber keine gute Idee, hier stillschweigend zu vermuten, „die Auflösung ist groß, also wird es wohl 709 sein“. Farbverschiebungen führen nicht zum Absturz, weshalb sie leicht unbemerkt in den Betrieb gelangen.

Media Foundation kann Farbrauminformationen als Medientyp-Attribute führen. Mindestens diese beiden sollten Sie prüfen:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Prüfen Sie diese beiden Attribute und lassen Sie explizit nur die Kombinationen zu, die Ihr Code unterstützt — so verringern Sie die Gefahr, dass später etwas still und leise schiefgeht.

3.5. Die erste Formel, die man sich merken sollte: BT.601 im Limited Range

Die typische Formel für 8-Bit-BT.601 lautet:

C = Y - 16
D = U - 128
E = V - 128

R = clip(1.164383 * C + 1.596027 * E)
G = clip(1.164383 * C - 0.391762 * D - 0.812968 * E)
B = clip(1.164383 * C + 2.017232 * D)

Bei BT.709 ändern sich die Koeffizienten. Das zeigen wir später auch im Code.

Wichtiger als das Auswendiglernen der Koeffizienten ist hier die Struktur: Von Y wird der Schwarzwert 16 abgezogen, U/V werden um 128 zentriert betrachtet.

4. Muster A: Media Foundation automatisch konvertieren lassen

4.1. Wann sich dieser Ansatz eignet

Dieser Ansatz eignet sich zum Beispiel für folgende Situationen.

  • Sie möchten ein einzelnes Standbild aus einer MP4 extrahieren
  • Sie möchten einige Thumbnails erzeugen
  • Sie möchten ein RGB-Bild an WIC übergeben
  • Es muss keine Echtzeitwiedergabe sein, Batch- oder Tool-Einsatz genügt

Der Source Reader bietet über MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING eine Funktion, die eine eingeschränkte YUV -> RGB32-Videoverarbeitung durchführt.

Wie auch Microsoft Learn festhält, handelt es sich dabei jedoch um Software-Verarbeitung, die nicht für die Wiedergabe optimiert ist. Wenn Sie Hunderte Frames pro Sekunde verarbeiten möchten, ist das nicht ganz das richtige Werkzeug.

4.2. Was Sie einstellen müssen, damit RGB32 herauskommt

Der Ablauf ist recht geradlinig.

  1. In den an MFCreateSourceReaderFromURL übergebenen Attributes MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE setzen
  2. Den Video-Stream auswählen
  3. Über SetCurrentMediaType MFMediaType_Video / MFVideoFormat_RGB32 anfordern
  4. Mit ReadSample das Sample lesen

Allein dadurch führt die hinter dem Decoder eingesetzte eingeschränkte Videoverarbeitung die YUV -> RGB32-Konvertierung für Sie durch.

4.3. Code

Der folgende Code setzt voraus, dass CoInitializeEx und MFStartup bereits ausgeführt wurden. In minimaler Form sieht das ungefähr so aus.

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <wrl/client.h>

#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")

using Microsoft::WRL::ComPtr;

HRESULT CreateSourceReaderWithAutoRgb(
    const wchar_t* path,
    IMFSourceReader** ppReader)
{
    if (!path || !ppReader) return E_POINTER;
    *ppReader = nullptr;

    ComPtr<IMFAttributes> attrs;
    HRESULT hr = MFCreateAttributes(&attrs, 2);
    if (FAILED(hr)) return hr;

    hr = attrs->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
    if (FAILED(hr)) return hr;

    hr = MFCreateSourceReaderFromURL(path, attrs.Get(), ppReader);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr)) return hr;

    ComPtr<IMFMediaType> outType;
    hr = MFCreateMediaType(&outType);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        outType.Get());
    if (FAILED(hr)) return hr;

    return S_OK;
}

HRESULT ReadOneRgb32Sample(
    IMFSourceReader* reader,
    IMFSample** ppSample,
    LONGLONG* pTimestamp100ns)
{
    if (!reader || !ppSample) return E_POINTER;
    *ppSample = nullptr;
    if (pTimestamp100ns) *pTimestamp100ns = 0;

    DWORD streamIndex = 0;
    DWORD flags = 0;
    LONGLONG timestamp = 0;

    HRESULT hr = reader->ReadSample(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        0,
        &streamIndex,
        &flags,
        &timestamp,
        ppSample);

    if (FAILED(hr)) return hr;
    if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
    if (*ppSample == nullptr) return MF_E_INVALID_STREAM_DATA;

    if (pTimestamp100ns) *pTimestamp100ns = timestamp;
    return S_OK;
}

Ruft man anschließend GetCurrentMediaType auf, lassen sich die tatsächliche Ausgabegröße und der Stride prüfen.

4.4. Stärken dieses Ansatzes

Der Vorteil dieses Ansatzes ist, dass er Sie schnell zu einem korrekten Bild bringt.

  • Sie müssen die 4:2:0- / 4:2:2-Expansion nicht selbst schreiben
  • Er nimmt Ihnen den Großteil des Aufwands für Matrix-Handling / Deinterlacing ab
  • Die Ausgabe lässt sich leicht an WIC oder GDI übergeben
  • Für die Verarbeitung weniger Frames ist er völlig praxistauglich

Für Tools zur Standbildextraktion ist es sehr naheliegend, hiermit zu beginnen.

4.5. Aber es gibt auch Fallstricke

Diese automatische Konvertierung hat folgende Eigenschaften.

Element Inhalt
Konvertierungsziel grundsätzlich RGB32
Implementierung Software-Verarbeitung
Geeignet für wenige Frames, Thumbnails, Offline-Verarbeitung
Nicht geeignet für D3D-basiertes Echtzeit-Rendering, Verarbeitung großer Frame-Mengen
Unverträgliche Attribute MF_SOURCE_READER_D3D_MANAGER, MF_READWRITE_DISABLE_CONVERTERS

Und noch ein wichtiger Punkt: der Umgang mit dem 4. Byte von RGB32. Im Speicher liegt Windows-RGB32 in der Reihenfolge Blue / Green / Red / Alpha oder Don’t Care vor. Es ist kein ARGB32. Übergeben Sie es an WIC als 32bppBGRA, ist es sicherer, das 4. Byte mit 0xFF zu füllen, um es undurchsichtig zu machen.

Das haben wir bereits im vorherigen Artikel zur Standbildextraktion als leicht zu übersehenden Punkt erwähnt.

5. Muster B: Die Konvertierung selbst schreiben

5.1. Wann sich dieser Ansatz eignet

Eine eigene Konvertierung eignet sich zum Beispiel in folgenden Fällen.

  • Sie verarbeiten große Mengen an Frames und möchten die Konvertierung selbst optimieren
  • Sie möchten NV12 direkt an die GPU oder SIMD-Code weiterreichen
  • Sie möchten BT.601 / BT.709 / Range explizit handhaben
  • Sie möchten ein anderes Ausgabeformat als RGB32 erzeugen
  • Die eingeschränkte automatische Konvertierung des Source Reader reicht nicht aus

Man kann sagen: Sie übernehmen selbst die Verantwortung für Durchsatz und Farbe, gewinnen dafür aber Freiheit.

5.2. Gesamtablauf der manuellen Konvertierung

Die Schritte sind wie folgt.

  1. Die Ausgabe des Source Reader auf NV12 oder YUY2 setzen
  2. Mit GetCurrentMediaType den tatsächlichen Subtype und die Attribute abrufen
  3. MF_MT_FRAME_SIZE, MF_MT_DEFAULT_STRIDE, MF_MT_YUV_MATRIX und MF_MT_VIDEO_NOMINAL_RANGE prüfen
  4. Den Buffer aus dem Sample extrahieren und sperren
  5. Ermitteln, welches Y/U/V jedes Pixel referenziert
  6. Die Matrix anwenden und nach BGRA schreiben

Der Code dieses Artikels beschränkt sich auf 8-Bit SDR / progressive / NV12 oder YUY2 / Limited Range. Diese Einschränkung der Annahmen ist keine Faulheit, sondern durchaus wichtig. Eine YUV-Konvertierung, die „erst einmal alles akzeptiert“, neigt dazu, Farben still und leise zu zerstören.

5.3. Zunächst den Ausgabe-Medientyp explizit festlegen

Zunächst teilen wir dem Source Reader mit: „Bitte gib YUV unverändert aus.“ Auch hier setzen wir voraus, dass CoInitializeEx / MFStartup bereits erfolgt sind.

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <wrl/client.h>

using Microsoft::WRL::ComPtr;

HRESULT ConfigureSourceReaderForSubtype(
    IMFSourceReader* reader,
    REFGUID subtype)
{
    if (!reader) return E_POINTER;

    HRESULT hr = reader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr)) return hr;

    hr = reader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr)) return hr;

    ComPtr<IMFMediaType> outType;
    hr = MFCreateMediaType(&outType);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_SUBTYPE, subtype);
    if (FAILED(hr)) return hr;

    hr = reader->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        outType.Get());
    if (FAILED(hr)) return hr;

    return S_OK;
}

Hier übergeben Sie als subtype entweder MFVideoFormat_NV12 oder MFVideoFormat_YUY2.

Zu beachten ist, dass der angeforderte Subtype nicht garantiert unverändert übernommen wird. Was tatsächlich herauskommt, prüfen Sie mit GetCurrentMediaType.

5.4. Vor der Konvertierung nur unterstützte Farbinformationen zulassen

Bei der manuellen Konvertierung entnehmen wir zunächst dem Medientyp die minimal nötigen Informationen. Das Beispiel dieses Artikels akzeptiert nur NV12 / YUY2 und lässt als Matrix nur BT.601 oder BT.709, als Range nur MFNominalRange_16_235 zu.

#include <vector>

struct DecodedFrameInfo
{
    GUID subtype = GUID_NULL;
    UINT32 width = 0;
    UINT32 height = 0;
    LONG defaultStride = 0;
    MFVideoTransferMatrix matrix = MFVideoTransferMatrix_Unknown;
    MFNominalRange nominalRange = MFNominalRange_Unknown;
};

HRESULT GetDefaultStride(
    IMFMediaType* pType,
    LONG* plStride)
{
    if (!pType || !plStride) return E_POINTER;

    LONG stride = 0;
    HRESULT hr = pType->GetUINT32(
        MF_MT_DEFAULT_STRIDE,
        reinterpret_cast<UINT32*>(&stride));

    if (FAILED(hr))
    {
        GUID subtype = GUID_NULL;
        UINT32 width = 0;
        UINT32 height = 0;

        hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
        if (FAILED(hr)) return hr;

        hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
        if (FAILED(hr)) return hr;

        hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &stride);
        if (FAILED(hr)) return hr;

        hr = pType->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(stride));
        if (FAILED(hr)) return hr;
    }

    *plStride = stride;
    return S_OK;
}

HRESULT GetStrictDecodedFrameInfo(
    IMFMediaType* pType,
    DecodedFrameInfo* pInfo)
{
    if (!pType || !pInfo) return E_POINTER;

    HRESULT hr = pType->GetGUID(MF_MT_SUBTYPE, &pInfo->subtype);
    if (FAILED(hr)) return hr;

    if (pInfo->subtype != MFVideoFormat_NV12 &&
        pInfo->subtype != MFVideoFormat_YUY2)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &pInfo->width, &pInfo->height);
    if (FAILED(hr)) return hr;

    hr = GetDefaultStride(pType, &pInfo->defaultStride);
    if (FAILED(hr)) return hr;

    UINT32 value = 0;

    hr = pType->GetUINT32(MF_MT_YUV_MATRIX, &value);
    if (FAILED(hr)) return hr;

    pInfo->matrix = static_cast<MFVideoTransferMatrix>(value);
    if (pInfo->matrix != MFVideoTransferMatrix_BT601 &&
        pInfo->matrix != MFVideoTransferMatrix_BT709)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    hr = pType->GetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, &value);
    if (FAILED(hr)) return hr;

    pInfo->nominalRange = static_cast<MFNominalRange>(value);
    if (pInfo->nominalRange != MFNominalRange_16_235)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    return S_OK;
}

Hier sind wir bewusst strikt. Die Enum-Dokumentation von Media Foundation enthält zwar Aussagen wie „Unknown wird als BT.709 behandelt“, aber wenn man das in der Praxis stillschweigend abrundet, werden Farbverschiebungen schwerer bemerkbar. Zumindest in einer ersten Implementierung ist es sicherer, nicht unterstützte Kombinationen als Fehler zu behandeln.

Wann Unknown zurückkommt

Vielleicht denken Sie, das sei zu streng — deshalb listen wir die Wege auf, über die Unknown zustande kommt. Meist handelt es sich um Fälle, in denen das ursprüngliche Videomaterial keine Farbinformation besitzt.

  • Die VUI von H.264 / HEVC enthält keine Farbinformation. Laut Spezifikation gilt matrix_coefficients als „nicht angegeben“, wenn colour_description_present_flag gleich 0 ist. Läuft diese Information ohne Angabe durch den Decoder, bleibt auch die weitergereichte Matrix nicht angegeben
  • Rohes YUV von einem Aufnahmegerät oder aus einem alten Container. Ein Pfad ohne Farbraumbeschreibung
  • Mitunter ist das Attribut MF_MT_YUV_MATRIX selbst gar nicht gesetzt. In diesem Fall liefert GetUINT32 keinen Wert zurück und schlägt mit MF_E_ATTRIBUTENOTFOUND fehl (im obigen Code wird das über FAILED(hr) einfach abgewiesen)

Wichtig ist hier: Unknown bedeutet nicht „es ist bekannt, dass es BT.709 ist“, sondern „es ist unbekannt“. Wendet man 709 auf Material in SD-Auflösung an, verschieben sich die Farben — und umgekehrt genauso.

Darauf aufbauend gibt es zwei mögliche Vorgehensweisen.

  • Strikt abweisen (der Ansatz dieses Artikels): einen Fehler als „nicht unterstützt“ zurückgeben und die aufrufende Ebene entscheiden lassen, dass „dieses Material nicht unterstützt wird“. Es ist sicherer, klar zu sagen, dass man es nicht verarbeiten kann, als dass Farben still und leise verschieben
  • Einen Standardwert festlegen und durchlassen: Wenn Sie es unbedingt durchlassen müssen, protokollieren Sie, welche Annahme bei Unknown getroffen wurde. Machen Sie dabei ausdrücklich deutlich, dass „601 / 709 anhand der Auflösung festgelegt wurde“

So oder so: Vermeiden Sie es unbedingt, stillschweigend abzurunden. Farbverschiebungen führen nicht zum Absturz und gelangen so unbemerkt in den Betrieb.

Bei Kameras und JPEG-basierten Quellen möchte man Pfade mit Full Range mitunter gesondert behandeln. Hier vermischen wir das nicht stillschweigend, sondern verfolgen die Strategie, die von diesem Code akzeptierten Annahmen explizit einzugrenzen.

5.5. Den Buffer unter Vertrauen auf den Stride lesen

Auch das ist ziemlich wichtig.

  • MF_MT_DEFAULT_STRIDE ist der minimale Stride
  • Der tatsächliche Sample-Buffer kann einen actual Stride mit Padding haben
  • Ist IMF2DBuffer::Lock2D verfügbar, sollten Sie es bevorzugen

Macht man das Helper-Pattern aus Microsoft Learns Uncompressed Video Buffers direkt einsatzbereit, ergibt sich Folgendes.

class BufferLock
{
public:
    explicit BufferLock(IMFMediaBuffer* buffer)
        : m_buffer(buffer),
          m_2dBuffer(nullptr),
          m_locked(false)
    {
        if (m_buffer)
        {
            m_buffer->AddRef();
            m_buffer->QueryInterface(IID_PPV_ARGS(&m_2dBuffer));
        }
    }

    ~BufferLock()
    {
        Unlock();

        if (m_2dBuffer)
        {
            m_2dBuffer->Release();
            m_2dBuffer = nullptr;
        }

        if (m_buffer)
        {
            m_buffer->Release();
            m_buffer = nullptr;
        }
    }

    HRESULT Lock(
        LONG defaultStride,
        DWORD heightInPixels,
        BYTE** ppScanline0,
        LONG* pActualStride)
    {
        if (!m_buffer || !ppScanline0 || !pActualStride) return E_POINTER;
        if (m_locked) return MF_E_INVALIDREQUEST;

        if (m_2dBuffer)
        {
            HRESULT hr = m_2dBuffer->Lock2D(ppScanline0, pActualStride);
            if (FAILED(hr)) return hr;

            m_locked = true;
            return S_OK;
        }

        BYTE* pData = nullptr;
        HRESULT hr = m_buffer->Lock(&pData, nullptr, nullptr);
        if (FAILED(hr)) return hr;

        *pActualStride = defaultStride;
        if (defaultStride < 0)
        {
            *ppScanline0 =
                pData + static_cast<size_t>(-defaultStride) * (heightInPixels - 1);
        }
        else
        {
            *ppScanline0 = pData;
        }

        m_locked = true;
        return S_OK;
    }

    void Unlock()
    {
        if (!m_locked) return;

        if (m_2dBuffer)
        {
            m_2dBuffer->Unlock2D();
        }
        else
        {
            m_buffer->Unlock();
        }

        m_locked = false;
    }

private:
    IMFMediaBuffer* m_buffer;
    IMF2DBuffer* m_2dBuffer;
    bool m_locked;
};

Die empfohlenen YUV-Surface-Definitionen gehen von top-left / positivem Stride aus, aber für den tatsächlichen Buffer-Zugriff ist es sicherer, den von der API zurückgegebenen Stride (= Pitch) unverändert zu verwenden. Legt man sich hier fest auf width, bricht das später still und leise.

5.6. Die Pro-Pixel-Konvertierungsformel in Code umsetzen

Hier behandeln wir nur die Limited-Range-Varianten von BT.601 und BT.709. Als Ausgabe wählen wir BGRA32, das sich leicht an WIC oder GDI übergeben lässt.

inline BYTE ClampToByte(double value)
{
    if (value <= 0.0) return 0;
    if (value >= 255.0) return 255;
    return static_cast<BYTE>(value + 0.5);
}

HRESULT ConvertLimitedYuvPixelToBgra(
    BYTE y,
    BYTE u,
    BYTE v,
    MFVideoTransferMatrix matrix,
    BYTE* dstPixel)
{
    if (!dstPixel) return E_POINTER;

    const double c = static_cast<double>(y) - 16.0;
    const double d = static_cast<double>(u) - 128.0;
    const double e = static_cast<double>(v) - 128.0;

    double r = 0.0;
    double g = 0.0;
    double b = 0.0;

    switch (matrix)
    {
    case MFVideoTransferMatrix_BT601:
        r = 1.164383 * c + 1.596027 * e;
        g = 1.164383 * c - 0.391762 * d - 0.812968 * e;
        b = 1.164383 * c + 2.017232 * d;
        break;

    case MFVideoTransferMatrix_BT709:
        r = 1.164383 * c + 1.792741 * e;
        g = 1.164383 * c - 0.213249 * d - 0.532909 * e;
        b = 1.164383 * c + 2.112402 * d;
        break;

    default:
        return MF_E_INVALIDMEDIATYPE;
    }

    dstPixel[0] = ClampToByte(b);
    dstPixel[1] = ClampToByte(g);
    dstPixel[2] = ClampToByte(r);
    dstPixel[3] = 255;

    return S_OK;
}

Was hier passiert, ist einfach.

  • Von Y wird 16 abgezogen
  • Von U / V wird 128 abgezogen
  • Die Koeffizienten der jeweiligen Matrix werden angewendet
  • Das Ergebnis wird auf 0..255 geclippt
  • Das 4. Byte von BGRA wird auf 255 gesetzt

5.7. NV12 nach BGRA32 konvertieren

NV12 ist 4:2:0, daher teilen sich die 4 Pixel eines 2x2-Blocks dasselbe U/V. Als minimale Implementierung ist es am verständlichsten, diese gemeinsam genutzte Chroma direkt für alle 4 Pixel zu verwenden.

HRESULT ConvertNv12ToBgra32(
    IMFMediaBuffer* buffer,
    const DecodedFrameInfo& info,
    std::vector<BYTE>& dstBgra)
{
    if (!buffer) return E_POINTER;
    if (info.subtype != MFVideoFormat_NV12) return MF_E_INVALIDMEDIATYPE;
    if ((info.width & 1u) != 0 || (info.height & 1u) != 0)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    dstBgra.resize(static_cast<size_t>(info.width) * info.height * 4);

    BufferLock lock(buffer);

    BYTE* scanline0 = nullptr;
    LONG actualStride = 0;
    HRESULT hr = lock.Lock(
        info.defaultStride,
        info.height,
        &scanline0,
        &actualStride);
    if (FAILED(hr)) return hr;

    if (actualStride <= 0)
    {
        lock.Unlock();
        return MF_E_INVALIDMEDIATYPE;
    }

    const BYTE* yPlane = scanline0;

    // Der Anfang der UV-Plane liegt "Stride × Height" Bytes weiter vorn.
    // Achtung: nicht width × height (siehe Diagramm in 3.2.)
    const BYTE* uvPlane =
        scanline0 + static_cast<size_t>(actualStride) * info.height;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        // Der Zeilenwechsel erfolgt immer in Einheiten des Stride
        const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;

        // Da es sich um 4:2:0 handelt, teilen sich 2 vertikale Zeilen eine UV-Zeile -> y / 2
        // Die UV-Plane verwendet denselben Stride wie die Y-Plane
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);

        // Die Ausgabe ist dicht gepacktes BGRA ohne Padding, daher width * 4
        BYTE* dstRow =
            dstBgra.data() + static_cast<size_t>(info.width) * 4 * y;

        for (UINT32 x = 0; x < info.width; ++x)
        {
            const BYTE Y = yRow[x];

            // In der UV-Plane liegen [U, V] abwechselnd.
            // Da sich 2 horizontale Pixel ein Paar teilen, ermitteln wir zunächst mit (x / 2),
            // das wievielte Paar es ist, und da 1 Paar = 2 Bytes sind, ergibt * 2 die Byte-Position.
            // +0 ist U, +1 ist V.
            //   x = 0, 1 -> uvRow[0], uvRow[1]
            //   x = 2, 3 -> uvRow[2], uvRow[3]
            const BYTE U = uvRow[(x / 2) * 2 + 0];
            const BYTE V = uvRow[(x / 2) * 2 + 1];

            hr = ConvertLimitedYuvPixelToBgra(
                Y,
                U,
                V,
                info.matrix,
                dstRow + static_cast<size_t>(x) * 4);
            if (FAILED(hr))
            {
                lock.Unlock();
                return hr;
            }
        }
    }

    lock.Unlock();
    return S_OK;
}

Dieser Code interpretiert das Chroma-Upsampling nach dem Nearest-Neighbor-Prinzip. Optisch ist das oft völlig praxistauglich, aber wer die höchste Bildqualität anstrebt, fährt konzeptionell sauberer mit einem Design, das zunächst das 4:2:0 -> 4:2:2 -> 4:4:4-Upconversion durchführt, wie es im YUV-Artikel von Microsoft Learn beschrieben ist.

5.8. YUY2 nach BGRA32 konvertieren

YUY2 ist packed 4:2:2. Da sich hier lediglich 2 Pixel ein U/V-Paar teilen, lässt es sich etwas leichter lesen als NV12.

#include <cstddef>

HRESULT ConvertYuy2ToBgra32(
    IMFMediaBuffer* buffer,
    const DecodedFrameInfo& info,
    std::vector<BYTE>& dstBgra)
{
    if (!buffer) return E_POINTER;
    if (info.subtype != MFVideoFormat_YUY2) return MF_E_INVALIDMEDIATYPE;
    if ((info.width & 1u) != 0) return MF_E_INVALIDMEDIATYPE;

    dstBgra.resize(static_cast<size_t>(info.width) * info.height * 4);

    BufferLock lock(buffer);

    BYTE* scanline0 = nullptr;
    LONG actualStride = 0;
    HRESULT hr = lock.Lock(
        info.defaultStride,
        info.height,
        &scanline0,
        &actualStride);
    if (FAILED(hr)) return hr;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        const BYTE* src =
            scanline0 +
            static_cast<ptrdiff_t>(actualStride) * static_cast<ptrdiff_t>(y);

        BYTE* dstRow =
            dstBgra.data() + static_cast<size_t>(info.width) * 4 * y;

        for (UINT32 x = 0; x < info.width; x += 2)
        {
            const BYTE Y0 = src[0];
            const BYTE U  = src[1];
            const BYTE Y1 = src[2];
            const BYTE V  = src[3];

            hr = ConvertLimitedYuvPixelToBgra(
                Y0,
                U,
                V,
                info.matrix,
                dstRow + static_cast<size_t>(x) * 4);
            if (FAILED(hr))
            {
                lock.Unlock();
                return hr;
            }

            hr = ConvertLimitedYuvPixelToBgra(
                Y1,
                U,
                V,
                info.matrix,
                dstRow + static_cast<size_t>(x + 1) * 4);
            if (FAILED(hr))
            {
                lock.Unlock();
                return hr;
            }

            src += 4;
        }
    }

    lock.Unlock();
    return S_OK;
}

Bei YUY2 liegen die Bytes als Y0 U Y1 V vor, sodass die Struktur „U/V wird für je 2 Pixel wiederverwendet“ direkt sichtbar ist. Dadurch lässt sich hierfür leichter ein mentales Modell aufbauen als für NV12.

5.9. Der Einstiegspunkt beim Aufruf aus einem Sample

Zum Schluss wird es einfacher nutzbar, wenn Sie aus dem IMFSample einen zusammenhängenden Buffer extrahieren und nach Subtype verzweigen.

HRESULT ConvertSampleToBgra32(
    IMFSample* sample,
    const DecodedFrameInfo& info,
    std::vector<BYTE>& dstBgra)
{
    if (!sample) return E_POINTER;

    ComPtr<IMFMediaBuffer> buffer;
    HRESULT hr = sample->ConvertToContiguousBuffer(&buffer);
    if (FAILED(hr)) return hr;

    if (info.subtype == MFVideoFormat_NV12)
    {
        return ConvertNv12ToBgra32(buffer.Get(), info, dstBgra);
    }

    if (info.subtype == MFVideoFormat_YUY2)
    {
        return ConvertYuy2ToBgra32(buffer.Get(), info, dstBgra);
    }

    return MF_E_INVALIDMEDIATYPE;
}

Damit sieht die vorgelagerte Abfolge so aus:

  • den Reader erstellen
  • NV12 oder YUY2 anfordern
  • aus GetCurrentMediaType ein DecodedFrameInfo erstellen
  • ReadSample
  • ConvertSampleToBgra32

Der eigentliche aufrufende Code sieht zum Beispiel so aus.

ComPtr<IMFMediaType> currentType;
HRESULT hr = reader->GetCurrentMediaType(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    &currentType);
if (FAILED(hr)) return hr;

DecodedFrameInfo info;
hr = GetStrictDecodedFrameInfo(currentType.Get(), &info);
if (FAILED(hr)) return hr;

DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> sample;

hr = reader->ReadSample(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    0,
    nullptr,
    &flags,
    &timestamp,
    &sample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (!sample) return MF_E_INVALID_STREAM_DATA;

std::vector<BYTE> bgra;
hr = ConvertSampleToBgra32(sample.Get(), info, bgra);
if (FAILED(hr)) return hr;

// bgra kann als top-down / 32bpp BGRA behandelt werden

5.10. Wo die „eigene Konvertierung“ ansetzt

Der bisherige Code hat die Form, dass die Anwendung nach dem Source Reader konvertiert. Das ist am leichtesten verständlich.

Wenn Sie die Konvertierung jedoch in die Media-Foundation-Pipeline einfügen möchten, gibt es auch andere Designs.

  • einen eigenen MFT schreiben
  • den Video Processor MFT / XVP verwenden
  • auf der GPU-Seite einen NV12 -> RGB-Shader schreiben

Geht man so weit, ändert sich das Thema etwas, weshalb wir uns diesmal auf den Code der Anwendungsseite beschränkt haben. Es ist aber nützlich zu wissen, dass es zwischen „Media Foundation überlassen“ und „alles selbst in der Anwendung erledigen“ mit dem Video Processor MFT einen Mittelweg gibt.

5.11. Prüfen, ob die Konvertierung korrekt gelungen ist

Farbfehler sind schwer zu erkennen, deshalb prüfen wir „es läuft“ und „es ist korrekt“ getrennt. Das geschieht in den folgenden zwei Stufen.

Stufe 1: Bekannte Werte eingeben und mit der Handrechnung abgleichen

Statt sofort ein Video durchlaufen zu lassen, ist es zuverlässiger, ConvertLimitedYuvPixelToBgra bekannte Y/U/V-Werte zu übergeben. Dafür braucht es weder eine Videodatei noch Media Foundation.

Für BT.601 im Limited Range ergeben sich für typische Farben folgende Y/U/V-Werte und die erwarteten Werte, wenn man sie in die Formel aus 5.6. einsetzt.

Farbe Y U V Erwartetes R G B
Schwarz 16 128 128 0 0 0
Weiß 235 128 128 255 255 255
Rot 81 90 240 254 0 0
Blau 41 240 110 0 0 255

Für Rot etwa ergibt sich mit C = 81 - 16 = 65, D = 90 - 128 = -38, E = 240 - 128 = 112, eingesetzt in die Formel:

R = 1.164383 * 65 + 1.596027 * 112       = 254.44  -> 254
G = 1.164383 * 65 - 0.391762 * (-38)
                  - 0.812968 * 112       =  -0.48  ->   0
B = 1.164383 * 65 + 2.017232 * (-38)     =  -0.97  ->   0

So ergibt sich das. Die Ausgabe liegt in der Reihenfolge BGRA vor, als Byte-Folge also 00 00 FE FF.

Wichtig ist hier, dass Rot nicht 255, sondern 254 ergibt. Der Grund liegt nicht in der Genauigkeit der Koeffizienten. Er liegt darin, dass die Eingabewerte Y/U/V bereits gerundete Ganzzahlen sind.

Rechnet man das theoretische Rot (255, 0, 0) in den Limited Range von BT.601 um, ergibt sich Y = 16 + 219 × 0,299 = 81,481, U = 90,203, und V genau 240. In dem Moment, in dem dies als 8-Bit-Stichprobe gespeichert wird, verschwindet dieser Nachkommaanteil und Y wird zu 81. Das verlorene 0,481 wird beim Zurückrechnen zu einer Einbuße von 0,481 × 1,164383 ≒ 0,56. 255 − 0,56 = 254,44 — die oben gezeigten 254,44 stammen genau von hier. Selbst mit unendlich genauen Koeffizienten bliebe es bei 254,44; die Rundung auf 6 Nachkommastellen wirkt sich erst unterhalb der 4. Nachkommastelle aus und erscheint in einer 8-Bit-Ausgabe gar nicht.

Auch wie am Ende auf eine Ganzzahl gerundet wird, beeinflusst das Ergebnis. ClampToByte aus 5.6. begrenzt den Wert auf [0, 255] und schneidet dann value + 0.5 ab — das ist also kaufmännisches Runden. Bei einfachem Abschneiden (static_cast<BYTE>(value)) bleibt dieses Rot zwar ebenfalls bei 254, aber bei Werten nahe einer Grenze wie B = 255,04 bei Blau oder R = 0,38 bei Rot verschiebt es sich um 1. Bevor Sie mit einer anderen Implementierung abgleichen, prüfen Sie, welche der beiden Varianten diese verwendet.

Mit anderen Worten: Eine Abweichung von ±1 bis 2 lässt sich also nicht mit „unterschiedlicher Koeffizientengenauigkeit“ begründen, sondern mit zwei Dingen: „durch die Stichprobenbildung fallen Nachkommastellen weg“ und „die Rundungsstrategie unterscheidet sich je nach Implementierung“. Umgekehrt gilt: Eine Abweichung, die sich mit diesen beiden Punkten nicht erklären lässt, ist ein echter Fehler. Wird Rot zu 250, vertauschen sich Rot und Blau, oder heben sich nur die dunklen Bereiche an — solche Abweichungen sollten Sie nicht auf die Koeffizientengenauigkeit, sondern auf die Voraussetzungen der Konvertierung zurückführen (Verwechslung von BT.601 und BT.709, Verwechslung von Full Range und Limited Range, vertauschtes U und V, falsch gelesener Stride). Wer das als „Genauigkeitsproblem“ abtut, übersieht behebbare Fehler.

Als Test geschrieben genügt diese Form.

#include <cstdlib>  // std::abs

// Prüft, ob die Abweichung vom Erwartungswert innerhalb der Toleranz liegt.
// Als Erwartungswert wird die "theoretische Farbe" angegeben (bei Rot 255, 0, 0). Die durch
// die Stichprobenbildung verlorenen Nachkommastellen und die unterschiedliche Rundungsstrategie
// werden über tolerance aufgefangen. Die Koeffizientengenauigkeit ist nicht der Grund
static bool CheckPixel(
    BYTE y, BYTE u, BYTE v,
    MFVideoTransferMatrix matrix,
    int expectedR, int expectedG, int expectedB,
    int tolerance = 2)
{
    BYTE bgra[4] = {};
    if (FAILED(ConvertLimitedYuvPixelToBgra(y, u, v, matrix, bgra)))
    {
        return false;
    }

    return std::abs(static_cast<int>(bgra[2]) - expectedR) <= tolerance
        && std::abs(static_cast<int>(bgra[1]) - expectedG) <= tolerance
        && std::abs(static_cast<int>(bgra[0]) - expectedB) <= tolerance
        && bgra[3] == 255;  // Alpha ist immer undurchsichtig
}

// Verwendung (BT.601 limited range)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0);      // Schwarz
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // Weiß
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0);     // Rot (ergibt in der Formel R=254)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255);    // Blau

Dasselbe lässt sich auch für BT.709 machen. Da die Koeffizienten anders sind, ändern sich auch die Y/U/V-Werte. Rot bei BT.709 ist zum Beispiel Y=63, U=102, V=240. Schickt man die 601-Werte unverändert durch den 709-Zweig, verschieben sich die Farben, sodass sich das Vertauschen der Matrix sofort erwischen lässt, wenn man die Tests in eigene Zeilen trennt.

Im GitHub-Beispiel ist nur diese Pixel-Konvertierung in einen betriebssystemunabhängigen Header ausgelagert, sodass dieser Test auch ohne Windows läuft.

Stufe 2: Die Ausgaben von Muster A und Muster B gegenüberstellen

Stimmt das einzelne Pixel, folgt als Nächstes der gesamte Frame. Aus demselben Video, zum selben Zeitpunkt, werden auf den beiden folgenden Wegen jeweils ein Bild entnommen und Pixel für Pixel verglichen:

  1. Muster A (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. Muster B (NV12 / YUY2 entgegennehmen und selbst konvertieren)
So lesen Sie die Differenz:
  Für jedes Pixel |A.R - B.R|, |A.G - B.G|, |A.B - B.B| bilden
  Den Maximalwert sowie den Anteil der Pixel ermitteln, die den Schwellenwert überschreiten

Erwarten Sie hier keine exakte Übereinstimmung. Dafür gibt es zwei Gründe.

  • Die Video Processing des Source Reader führt das Chroma-Upsampling möglicherweise mit einer anderen Methode als Nearest-Neighbor durch. Die manuelle Implementierung aus 5.7. ist eine minimale Implementierung, die die gemeinsame Chroma direkt für alle 4 Pixel verwendet, weshalb an Kanten größere Abweichungen auftreten
  • Rundung und der Umgang mit Zwischenpräzision unterscheiden sich

Worauf Sie also achten sollten, ist nicht „stimmt es überein“, sondern das Muster, in dem die Abweichung auftritt.

Beobachtete Abweichung Was zu vermuten ist
Flächen stimmen überein, nur an Farbgrenzen gibt es Unterschiede Unterschied beim Chroma-Upsampling. Im Rahmen des Erwarteten
Das gesamte Bild ist gleichmäßig verschoben Verwechslung von Matrix (601 / 709) oder Range (16..235 / 0..255)
Streifenbildung, diagonale Verschiebung Fest codierter Stride. Siehe 7.2. und 7.5.
Rot und Blau sind vertauscht Verwechslung von BGRA und RGBA
Alles wirkt transparent / komplett schwarz Das 4. Byte wurde nicht mit 0xFF gefüllt. Siehe 7.1.

Betrachtet man die „Form“ der Abweichung, lässt sich ziemlich gut eingrenzen, wo man suchen sollte: Ist die Verschiebung gleichmäßig im ganzen Bild, liegt es an der Formel oder den Farbinformationen; ist sie lokal begrenzt, liegt es an der Indizierung oder am Stride.

6. Wofür sollte man sich entscheiden?

Sind Sie unentschlossen, schafft die folgende Tabelle recht gut Klarheit.

Gesichtspunkt Automatische Konvertierung (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) Manuelle Konvertierung
Implementierungsgeschwindigkeit
Extraktion einiger Standbilder
Große Frame-Mengen / Echtzeit
Explizite Kontrolle über Matrix / Range
Kombination mit GPU / D3D ○〜◎
Ausgabe außer RGB32 gewünscht
Verständnis der Grundlagen

Für Ihre erste Implementierung ist folgende Faustregel hilfreich.

  • Erst einmal etwas Laufendes haben -> automatische Konvertierung
  • Verantwortung für Farbe und Performance übernehmen -> manuelle Konvertierung

In der Praxis ist auch die Reihenfolge „zunächst mit der automatischen Konvertierung ein korrektes Bild bestätigen und erst danach auf den manuellen Pfad umstellen“ recht wirkungsvoll. Wenn Sie von Anfang an alles selbst übernehmen, wird schwer erkennbar, an welcher Stelle das Bild kaputtgegangen ist.

7. Fallstricke, die man in der Praxis leicht übersieht

7.1. RGB32 fälschlich für RGBA mit Alpha halten

Im Speicher liegt RGB32 als B, G, R, Alpha oder Don't Care vor. Speichert man es unverändert als BGRA in einer PNG, kann das 4. Byte 0 sein, sodass das Bild transparent wird. Es ist sicherer, vor dem Speichern 0xFF einzutragen.

7.2. Den Stride fest auf width * bytesPerPixel codieren

Ein ziemlich häufiger Fehler. Der tatsächliche Sample-Buffer kann Padding enthalten, daher gilt als Grundregel: Für den Wechsel zwischen Zeilen wird der tatsächliche (actual) Stride verwendet.

7.3. MF_MT_DEFAULT_STRIDE mit dem tatsächlichen Pitch verwechseln

MF_MT_DEFAULT_STRIDE ist „der minimale Stride, wenn dieses Format in zusammenhängendem Speicher abgebildet wird“. Für den tatsächlichen Pitch des Sample-Buffers bevorzugen Sie den von IMF2DBuffer::Lock2D zurückgegebenen Wert. (Pitch ist ein anderer Name für Stride. Wie in 3.2. erwähnt, verwenden wir die Begriffe in diesem Artikel synonym.)

7.4. 601 / 709 stillschweigend erraten, ohne die Color-Metadaten zu prüfen

Farbfehler sind schwer zu erkennen. Sie führen auch nicht zum Absturz. Genau das macht sie tückisch.

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Diese sollten Sie zumindest prüfen. Und die richtige Einstellung ist ungefähr: Werte, die Ihr Code nicht unterstützt, sollten einen Fehler auslösen.

7.5. Die UV-Plane von NV12 fälschlich mit width * height abschneiden

Der Plane-Offset ergibt sich aus dem tatsächlichen Stride und der Height. Nicht aus width * height. Geht man hier nachlässig vor, verschieben sich Farben oder das Bild wird beschädigt.

7.6. Interlaced Video unter der Annahme von Progressive verarbeiten

Das manuelle Beispiel dieses Artikels setzt Progressive voraus. Liest man Interlaced-Material unverändert als ein einzelnes Field, kann kammartiges Rauschen entstehen. Ist Deinterlacing nötig, ziehen Sie eher die automatische Video Processing des Source Reader oder den Video Processor MFT in Betracht.

7.7. Die Qualität des 4:2:0-Chroma-Upsamplings ignorieren

Die NV12-Konvertierung dieses Artikels legt zugunsten der Verständlichkeit die gemeinsam genutzte Chroma unverändert auf jedes Pixel. Für viele Zwecke reicht das, aber wer Bildqualität priorisiert, sollte sich mit dem Upconversion-Ansatz aus der Dokumentation zu den empfohlenen YUV-Formaten befassen.

8. Zusammenfassung

Wenn Sie mit Media Foundation von YUV nach RGB konvertieren, verlieren Sie sich deutlich seltener, wenn Sie folgende Einordnung im Kopf haben.

  • Hinter dem Decoder kommt normalerweise NV12 oder YUY2 heraus, nicht RGB
  • Wenn Sie es sich einfach machen wollen, fordern Sie mit MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING RGB32 an
  • Wenn Sie Kontrolle wollen, nehmen Sie NV12 / YUY2 entgegen und konvertieren selbst nach BGRA
  • Auf dem manuellen Pfad sichern Sie sich vor der Formel zuerst Sampling / Range / Matrix / Stride ab
  • Bleiben BT.601 / BT.709, 16..235 und 4:2:0 / 4:2:2 unklar, entstehen Farbverschiebungen oder kaputte Bilder

YUV -> RGB wirkt anfangs etwas sperrig. Sitzt aber einmal das Bild —

  • NV12 teilt U/V in 2x2-Blöcken
  • YUY2 teilt U/V über 2 horizontale Pixel
  • auf dieses U/V und Y wird die Matrix angewendet

— wird es ziemlich unkompliziert. Die geheimnisvollen, kosmisch anmutenden Byte-Folgen beginnen, wie richtig sinnvolle Pixel auszusehen.

9. Referenzen

Beispielcode zu diesem Artikel

Verwandte Artikel von KomuraSoft

Microsoft Learn

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

Media Foundation, Source Reader, das Speichern von Bildern und die Konvertierung von Videoframes sind Implementierungsthemen der Windows-Medienverarbeitung, die gut zur Windows-Anwendungsentwicklung passen.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Warum gibt der Media-Foundation-Decoder YUV statt RGB aus?
Weil das menschliche Auge für die Feinheit der Helligkeit empfindlicher ist als für die Feinheit der Farbe, zahlt sich bei Video ein Design aus, das Y (den helligkeitsnahen Anteil) fein und U/V (die Farbdifferenzanteile) grob auflöst. Deshalb sind die unkomprimierten Frames, die aus einem Decoder im Windows-Video-Bereich kommen, in der Regel YUV-Formate wie NV12 oder YUY2. Im Kontext von digitalem Video lässt sich die Sache leichter einordnen, wenn man YUV im Grunde als Y'CbCr liest.
Wie kommt man am einfachsten an RGB-Frames?
Indem Sie in IMFSourceReader MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING aktivieren und MFVideoFormat_RGB32 anfordern. Für die Extraktion einiger weniger Standbilder oder die Erzeugung von Thumbnails ist das am bequemsten. Diese automatische Konvertierung ist jedoch Software-Verarbeitung und nicht für die Echtzeitwiedergabe optimiert. Wer Massenverarbeitung oder Kontrolle über die Farbe braucht, nimmt die Frames als YUV entgegen und konvertiert selbst.
Worauf muss man achten, wenn man YUV selbst nach RGB konvertiert?
Es reicht nicht, drei Koeffizienten anzuwenden — Subsampling (4:2:0 / 4:2:2), Range, Matrix und Stride spielen ebenfalls mit hinein. In der Praxis werden Farben am häufigsten dadurch verfälscht, dass MF_MT_YUV_MATRIX und MF_MT_VIDEO_NOMINAL_RANGE nicht geprüft werden und dass man annimmt, der Stride entspreche width × bytesPerPixel. Der schnellste Weg ist, zunächst den Aufbau von NV12 und YUY2 wirklich zu verstehen.
Was unterscheidet NV12 und YUY2?
NV12 ist ein 4:2:0-Format: Auf die Y-Plane folgt eine UV-Plane, in der U und V abwechselnd liegen, und die 4 Pixel eines 2x2-Blocks teilen sich ein U/V-Paar. YUY2 ist ein 4:2:2-Format, bei dem sich zwei horizontal benachbarte Pixel ein U/V-Paar teilen. Beide Formate kommen in der Praxis häufig vor, unterscheiden sich aber darin, wie stark die Farbe ausgedünnt wird (Subsampling).

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog