YUV mit Media Foundation in RGB konvertieren
· Aktualisiert am: · Go Komura · Media Foundation, C++, Windows-Entwicklung, Videoverarbeitung, YUV
Änderungsverlauf (1 Aktualisierungen, zuletzt am 3. Sep 2026)
Protokoll der Änderungen an diesem Artikel. Wo eine Fassung vor der Aktualisierung archiviert wurde, bleibt sie über einen dauerhaften Link mit DOI lesbar.
- Die 21 der 22 Abbildungen dieses Artikels, die in der Übersetzung fehlten, wurden samt Bildunterschriften ergänzt. Die Graphen entsprechen exakt den japanischen; übersetzt sind nur die Beschriftungen. Fassung vor dieser Aktualisierung lesen (DOI: 10.5281/zenodo.22173470)
- Erstveröffentlichung
Diesen Artikel zitieren(DOI: 10.5281/zenodo.22173469)
Dieser Artikel ist auf Zenodo archiviert. Unten stehen sowohl der DOI, der immer auf die neueste Fassung verweist, als auch der DOI, der auf die gerade gelesene Fassung festgelegt ist.
Go Komura (2026). YUV mit Media Foundation in RGB konvertieren. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173469 https://comcomponent.com/de/blog/media-foundation-yuv-to-rgb-conversion-patterns/
- DOI (neueste Fassung)
- 10.5281/zenodo.22173469
- DOI (diese Fassung)
- 10.5281/zenodo.22279093
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:
IMFSourceReaderautomatisch bis zu RGB32 konvertieren lassen - Muster B:
NV12/YUY2entgegennehmen 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.libmfreadwrite.libmfuuid.libole32.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_PROCESSINGzu aktivieren undMFVideoFormat_RGB32anzufordern - 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
NV12undYUY2richtig 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_MATRIXundMF_MT_VIDEO_NOMINAL_RANGEnicht geprüft werden, sowie durch die Annahme, der Stride entsprechewidth * 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.
flowchart TB
accTitle: Die zwei Optionen dieses Artikels
accDescr: Diagramm der beiden Optionen, die dieser Artikel behandelt: Wer es sich einfach machen will, lässt den Source Reader RGB32 ausgeben; wer Massenverarbeitung oder Kontrolle über die Farbe braucht, nimmt die Frames als YUV entgegen und konvertiert selbst.
want1["RGB-Frames werden gebraucht"] -->|"es sich einfach machen"| pa1["Muster A: Source Reader RGB32 ausgeben lassen"]
want1 -->|"Massenverarbeitung / Farbkontrolle"| pb1["Muster B: YUV entgegennehmen und selbst konvertieren"]
Abbildung 1: Wer es sich einfach machen will, nimmt Muster A; wer Durchsatz und Verantwortung für die Farbe übernimmt, Muster B.
2. Zuerst ein Bild
Es geht schneller, wenn wir uns zunächst anhand eines Diagramms ansehen, was innerhalb von Media Foundation passiert.
flowchart LR
File["MP4 / H.264 / HEVC"] --> Decoder["Decoder"]
Decoder --> YUV["YUV-Frame wie NV12 / YUY2 / YV12"]
YUV -->|Muster A| SRVP["Video Processing des Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|Muster B| App["Eigener Konvertierungscode"]
App --> RGB2["BGRA / RGB"]
Abbildung 2: Der Decoder liefert YUV-Frames, und von dort gabelt sich der Weg zu RGB in Muster A und Muster B.
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.
- Media Foundation die Frames bis zu RGB32 konvertieren lassen
- 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:
Yist der helligkeitsnahe AnteilU/Vsind die Farbdifferenzanteile- Bei
RGBträ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.
flowchart TB
accTitle: Warum YUV-Formate verwendet werden
accDescr: Diagramm dazu, dass sich ein Design auszahlt, das Y fein und U/V grob auflöst, weil das menschliche Auge für die Feinheit der Helligkeit empfindlicher ist als für die der Farbe, und dass genau das der Grund für die Verbreitung der YUV-Formate ist.
eye1["Das menschliche Auge ist für Helligkeit empfindlich"] --> dsn1["Y fein und U/V grob auflösen"]
dsn1 --> why1["Grund, warum YUV-Formate verwendet werden"]
Abbildung 3: Das YUV-Design löst Y fein und U/V grob auf, passend zur Empfindlichkeit des Auges für Helligkeit.
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.
flowchart TB
accTitle: Die Teilungseinheiten von NV12 und YUY2 im Vergleich
accDescr: Diagramm dazu, dass sich bei NV12 die 4 Pixel eines 2x2-Blocks ein U/V-Paar teilen und bei YUY2 zwei horizontal benachbarte Pixel, weshalb zuerst zu überlegen ist, welchen Pixeln die geteilten U/V-Werte zugeordnet werden.
nv1["NV12 (4:2:0)"] --> sh1["4 Pixel eines 2x2-Blocks teilen sich ein U/V-Paar"]
yy1["YUY2 (4:2:2)"] --> sh2["2 horizontal benachbarte Pixel teilen sich ein U/V-Paar"]
sh1 --> asn1["Festlegen, welchem Pixel welches U/V zugeordnet wird"]
sh2 --> asn1
Abbildung 4: In beiden Formaten werden U/V geteilt, sodass die Konvertierung nicht mit einer Ersetzung Pixel für Pixel auskommt.
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.
- 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
- Die Range rückgängig machen Video-Y nutzt normalerweise 16..235, U/V 16..240, also diese Skalierung zurücknehmen
- Die Matrix anwenden
Mit Koeffizienten wie
BT.601oderBT.709nach 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
Diese beiden Fragen zu beantworten, ist die eigentliche Verarbeitung.
flowchart TB
accTitle: Die drei Ebenen für praktischen Code
accDescr: Diagramm dazu, dass sich die Konvertierung von YUV nach RGB in praktischem 8-Bit-SDR-Code leichter verstehen lässt, wenn man sie in drei Ebenen unterteilt: das Subsampling rückgängig machen, die Range rückgängig machen und die Matrix anwenden.
y1["YUV-Frame"] --> up1["Subsampling rückgängig machen"]
up1 --> rg1["Range rückgängig machen (z. B. 16..235)"]
rg1 --> mx1["Matrix anwenden (601 / 709)"]
mx1 --> rgb2["RGB"]
Abbildung 5: Die Konvertierung besteht nicht aus drei Koeffizienten, sondern aus den drei Ebenen Subsampling, Range und Matrix.
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_MATRIXMF_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.
flowchart TB
accTitle: Ablauf, um den Farbraum nicht zu erraten
accDescr: Diagramm des Ablaufs, bei dem MF_MT_YUV_MATRIX und MF_MT_VIDEO_NOMINAL_RANGE geprüft und nur die unterstützten Kombinationen explizit durchgelassen werden, weil stillschweigendes Erraten von 601 oder 709 anhand der Auflösung dazu führt, dass Farbverschiebungen unbemerkt in den Betrieb gelangen.
gs1["Anhand der Auflösung stillschweigend erraten"] --> sl1["Farbverschiebungen führen nicht zum Absturz"]
sl1 --> op1["Gelangen unbemerkt in den Betrieb"]
at1["Die Attribute Matrix und Range prüfen"] --> ps1["Nur unterstützte Kombinationen durchlassen"]
ps1 -.-> sf1["Stille Fehler lassen sich vermeiden"]
Abbildung 6: Den Farbraum nicht erraten, sondern die Attribute prüfen und nur die unterstützten Kombinationen explizit durchlassen.
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.
flowchart TB
accTitle: Der Aufbau der Konvertierungsformel
accDescr: Diagramm des Aufbaus der Konvertierungsformel: Von Y wird der Schwarzwert 16 abgezogen, U und V werden um 128 zentriert betrachtet, die Koeffizienten der jeweiligen Matrix werden angewendet und das Ergebnis wird geclippt.
yy2["Von Y 16 abziehen (Schwarzwert)"] --> co1["Die Koeffizienten der Matrix anwenden"]
uv1["Von U / V 128 abziehen (Mittelpunkt)"] --> co1
co1 --> cl1["Auf 0..255 clippen"]
Abbildung 7: Zu merken sind nicht die Koeffizienten, sondern der Aufbau der Formel: Schwarzwert 16 und Mittelpunkt 128.
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.
flowchart TB
accTitle: Wofür sich die automatische Konvertierung eignet und wofür nicht
accDescr: Diagramm dazu, dass die automatische Konvertierung des Source Reader Software-Verarbeitung ist und nicht für die Wiedergabe optimiert wurde, sich daher für Standbilder, Thumbnails und Batch-Zwecke eignet, sich aber nicht dafür anbietet, Hunderte Frames pro Sekunde darauf zu stützen.
auto1["Automatische Konvertierung des Source Reader"] --> sw1["Software-Verarbeitung"]
sw1 -->|"geeignet"| bat1["Standbilder, Thumbnails, Batch"]
sw1 -.->|"nicht darauf stützen"| rt1["Echtzeitverarbeitung von Hunderten Frames pro Sekunde"]
Abbildung 8: Da die automatische Konvertierung Software-Verarbeitung ist, beschränkt man sie auf Werkzeuge mit wenigen Frames.
4.2. Was Sie einstellen müssen, damit RGB32 herauskommt
Der Ablauf ist recht geradlinig.
- In den an
MFCreateSourceReaderFromURLübergebenen AttributesMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUEsetzen - Den Video-Stream auswählen
- Über
SetCurrentMediaTypeMFMediaType_Video/MFVideoFormat_RGB32anfordern - Mit
ReadSampledas Sample lesen
Allein dadurch führt die hinter dem Decoder eingesetzte eingeschränkte Videoverarbeitung die YUV -> RGB32-Konvertierung für Sie durch.
flowchart TB
accTitle: Die vier Schritte zum Aktivieren der automatischen Konvertierung
accDescr: Diagramm der vier Schritte der automatischen Konvertierung: die Videoverarbeitung über die Attributes aktivieren und den Reader erstellen, den Video-Stream auswählen, RGB32 anfordern und mit ReadSample lesen.
a1["Videoverarbeitung über die Attributes aktivieren"] --> a2["Video-Stream auswählen"]
a2 --> a3["RGB32 anfordern"]
a3 --> a4["Mit ReadSample lesen"]
a4 -.-> a5["Hinter dem Decoder wird nach RGB32 konvertiert"]
Abbildung 9: Allein diese vier Schritte genügen, damit die hinter dem Decoder eingesetzte Videoverarbeitung bis RGB32 führt.
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,
×tamp,
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.
flowchart TB
accTitle: Der Umgang mit dem 4. Byte von RGB32
accDescr: Diagramm dazu, dass bei Windows-RGB32 nicht festgelegt ist, ob das auf B, G und R folgende 4. Byte Alpha oder Don't Care ist, und es deshalb sicherer ist, es vor der Übergabe an WIC als 32bppBGRA mit 0xFF zu füllen und damit undurchsichtig zu machen.
r32["Speicheranordnung von RGB32"] --> bgr1["3 Bytes B, G, R"]
r32 --> b41["Das 4. Byte ist Alpha oder Don't Care"]
b41 -->|"mit 0xFF füllen"| wic1["Als 32bppBGRA an WIC übergebbar"]
b41 -.->|"unverändert übergeben"| tr3["Kann transparent werden"]
Abbildung 10: Da das 4. Byte nicht festgelegt ist, macht man es mit 0xFF undurchsichtig, bevor man es an WIC übergibt.
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
NV12direkt an die GPU oder SIMD-Code weiterreichen - Sie möchten
BT.601/BT.709/ Range explizit handhaben - Sie möchten ein anderes Ausgabeformat als
RGB32erzeugen - 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.
flowchart TB
accTitle: Der Kompromiss bei der manuellen Konvertierung
accDescr: Diagramm dazu, dass man bei der manuellen Konvertierung selbst die Verantwortung für Durchsatz und Farbe übernimmt und dafür Optimierung, die Anbindung an GPU und SIMD, die explizite Kontrolle über Matrix und Range sowie Freiheit beim Ausgabeformat gewinnt.
own1["Manuelle Konvertierung wählen"] --> res1["Verantwortung für Durchsatz und Farbe selbst tragen"]
res1 --> fr1["Freiheit bei Optimierung, GPU / SIMD und Ausgabeformat"]
res1 --> fr2["Explizite Kontrolle über Matrix / Range"]
Abbildung 11: Die manuelle Konvertierung erkauft mit Verantwortung die Freiheit bei Performance und Farbe.
5.2. Gesamtablauf der manuellen Konvertierung
Die Schritte sind wie folgt.
- Die Ausgabe des Source Reader auf
NV12oderYUY2setzen - Mit
GetCurrentMediaTypeden tatsächlichen Subtype und die Attribute abrufen MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIXundMF_MT_VIDEO_NOMINAL_RANGEprüfen- Den Buffer aus dem Sample extrahieren und sperren
- Ermitteln, welches Y/U/V jedes Pixel referenziert
- 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.
flowchart TB
accTitle: Gesamtablauf der manuellen Konvertierung
accDescr: Diagramm der Schritte der manuellen Konvertierung: die Ausgabe auf NV12 oder YUY2 setzen, den tatsächlichen Medientyp und die Attribute prüfen, den Buffer sperren, für jedes Pixel Y/U/V ermitteln und mit angewendeter Matrix nach BGRA schreiben.
f1["NV12 / YUY2 anfordern"] --> f2["Tatsächlichen Subtype und die Attribute prüfen"]
f2 --> f3["Buffer sperren"]
f3 --> f4["Y/U/V jedes Pixels ermitteln"]
f4 --> f5["Matrix anwenden und nach BGRA schreiben"]
f2 -.-> nar1["Enger gefasste Annahmen zerstören seltener Farben"]
Abbildung 12: Die manuelle Konvertierung läuft in der Reihenfolge Anfordern, Prüfen, Sperren, Referenzieren, Konvertieren, und je enger die Annahmen, desto sicherer.
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.
flowchart TB
accTitle: Anforderung und tatsächliche Ausgabe getrennt betrachten
accDescr: Diagramm des Ablaufs, bei dem mit GetCurrentMediaType geprüft wird, was tatsächlich herauskommt, weil der über SetCurrentMediaType angeforderte Subtype nicht garantiert unverändert übernommen wird.
req1["Subtype anfordern"] -.->|"wird nicht garantiert übernommen"| out2["Tatsächliche Ausgabe"]
out2 --> gct1["Mit GetCurrentMediaType prüfen"]
gct1 --> use1["Die weitere Verarbeitung mit dem geprüften Wert schreiben"]
Abbildung 13: Eine Anforderung bleibt eine Anforderung; die tatsächliche Ausgabe prüfen Sie vor der Verwendung immer 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_coefficientsals „nicht angegeben“, wenncolour_description_present_flaggleich 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_MATRIXselbst gar nicht gesetzt. In diesem Fall liefertGetUINT32keinen Wert zurück und schlägt mitMF_E_ATTRIBUTENOTFOUNDfehl (im obigen Code wird das überFAILED(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
Unknowngetroffen 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.
flowchart TB
accTitle: Die Verzweigung, wenn die Matrix Unknown ist
accDescr: Diagramm dazu, dass Unknown nicht heißt, dass BT.709 bekannt ist, sondern dass es unbekannt ist, dass sich daraus zwei Vorgehensweisen ergeben, nämlich striktes Abweisen per Fehler oder Durchlassen mit protokollierter Annahme, und dass stillschweigendes Abrunden unbedingt zu vermeiden ist.
unk1["Matrix ist Unknown, also unbekannt"] -->|"Ansatz dieses Artikels"| st4["Strikt per Fehler abweisen"]
unk1 -->|"falls es durchgelassen werden muss"| dflt1["Die getroffene Annahme protokollieren und durchlassen"]
unk1 -.->|"nur das vermeiden"| mute1["Stillschweigend abrunden"]
Abbildung 14: Unknown heißt „unbekannt“, also entweder abweisen oder mit Protokoll durchlassen, aber nie stillschweigend abrunden.
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_STRIDEist der minimale Stride- Der tatsächliche Sample-Buffer kann einen actual Stride mit Padding haben
- Ist
IMF2DBuffer::Lock2Dverfü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.
flowchart TB
accTitle: Die Rangfolge beim Stride
accDescr: Diagramm der Rangfolge, nach der bei verfügbarem Lock2D dessen Wert bevorzugt und eine Festlegung anhand der Width vermieden wird, weil MF_MT_DEFAULT_STRIDE der minimale Stride ist und der tatsächliche Buffer einen Stride mit Padding haben kann.
p1["Der von Lock2D zurückgegebene actual Stride"] -->|"wenn verfügbar, höchste Priorität"| acc1["Wert für den Buffer-Zugriff"]
p2["MF_MT_DEFAULT_STRIDE (Minimalwert)"] -->|"Fallback"| acc1
p3["Festlegung anhand der Width"] -.->|"bricht still und leise"| acc1
Abbildung 15: Für den Zeilenwechsel hat der von Lock2D gemeldete Stride Vorrang, und nichts wird anhand der Width fest codiert.
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
Ywird 16 abgezogen - Von
U/Vwird 128 abgezogen - Die Koeffizienten der jeweiligen Matrix werden angewendet
- Das Ergebnis wird auf 0..255 geclippt
- Das 4. Byte von BGRA wird auf
255gesetzt
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.
flowchart TB
accTitle: Zwei Designs für das Chroma-Upsampling
accDescr: Diagramm dazu, dass die minimale Implementierung die gemeinsame Chroma nach Nearest-Neighbor-Art direkt für 4 Pixel verwendet und damit optisch oft völlig praxistauglich ist, während für höchste Bildqualität ein Design konzeptionell sauberer ist, das vor der Konvertierung von 4:2:0 über 4:2:2 nach 4:4:4 upconvertiert.
min1["Minimale Implementierung: gemeinsame Chroma direkt verwenden"] --> pr1["Optisch oft völlig praxistauglich"]
hq1["Erst upconvertieren, dann konvertieren"] --> pr2["Konzeptionell sauber, Bildqualität zuerst"]
hq1 -.-> steps1["In der Reihenfolge 4:2:0 -> 4:2:2 -> 4:4:4"]
Abbildung 16: Wann die minimale Implementierung mit direkt verwendeter Chroma passt und wann das stufenweise upconvertierende Design.
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
NV12oderYUY2anfordern- aus
GetCurrentMediaTypeeinDecodedFrameInfoerstellen ReadSampleConvertSampleToBgra32
In dieser Reihenfolge lässt sich die vorgelagerte Stufe aufbauen.
flowchart TB
accTitle: Die Verzweigung in der Einstiegsfunktion
accDescr: Diagramm des Aufbaus der Einstiegsfunktion, die aus dem Sample einen zusammenhängenden Buffer entnimmt, bei NV12 zur NV12-Konvertierung und bei YUY2 zur YUY2-Konvertierung verzweigt und alles andere als Fehler behandelt.
smp1["IMFSample"] --> cont1["Zusammenhängenden Buffer entnehmen"]
cont1 -->|"NV12"| cnv1["Zur NV12-Konvertierung"]
cont1 -->|"YUY2"| cnv2["Zur YUY2-Konvertierung"]
cont1 -->|"alles andere"| err1["Als Fehler behandeln"]
Abbildung 17: Am Einstieg zuerst einen zusammenhängenden Buffer bilden, dann nach Subtype verzweigen und Nichtunterstütztes schlicht als Fehler abweisen.
Der eigentliche aufrufende Code sieht zum Beispiel so aus.
ComPtr<IMFMediaType> currentType;
HRESULT hr = reader->GetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
¤tType);
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,
×tamp,
&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
MFTschreiben - 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.
flowchart TB
accTitle: Die Grenze zwischen tolerierbarer Abweichung und echtem Fehler
accDescr: Diagramm der Abgrenzung, nach der sich Abweichungen von plus/minus 1 bis 2 durch die bei der Stichprobenbildung verlorenen Nachkommastellen und durch unterschiedliche Rundungsstrategien erklären lassen, während eine damit nicht erklärbare Abweichung ein echter Fehler ist, bei dem die Voraussetzungen der Konvertierung infrage zu stellen sind.
dif1["Die Abweichung vom Erwartungswert betrachten"] -->|"Abweichung von ±1 bis 2"| exp1["Durch Stichprobenrundung und Rundungsstrategie erklärbar"]
dif1 -->|"nicht erklärbare Abweichung"| bug1["Echter Fehler"]
bug1 --> prem1["Die Voraussetzungen Matrix, Range, U/V und Stride infrage stellen"]
Abbildung 18: Kleine Abweichungen erklären sich aus Stichprobenbildung und Rundung, und was sich so nicht erklärt, deutet auf verwechselte Voraussetzungen hin.
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. Herangezogen wird dasselbe Video zum selben Zeitpunkt, und zwar über zwei Wege:
- Muster A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Muster B (
NV12/YUY2entgegennehmen und selbst konvertieren)
Auf diesen beiden Wegen entnehmen Sie jeweils ein Bild und vergleichen es Pixel für Pixel.
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.
flowchart TB
accTitle: Die zweistufige Verifikation
accDescr: Diagramm der zweistufigen Verifikation, bei der zuerst bekannte Y/U/V-Werte für ein einzelnes Pixel mit der Handrechnung abgeglichen werden und danach die Frames von Muster A und Muster B aus demselben Video zum selben Zeitpunkt verglichen werden, um anhand der Form der Abweichung einzugrenzen, wo zu suchen ist.
st5["Stufe 1: 1 Pixel mit der Handrechnung abgleichen"] --> st6["Stufe 2: Die Frames der beiden Wege abgleichen"]
st6 --> shp1["Anhand der Form der Abweichung eingrenzen, wo zu suchen ist"]
st5 -.-> osf1["Weder Video noch Media Foundation nötig"]
Abbildung 19: Erst die Prüfung eines einzelnen Pixels bestehen und dann ganze Frames vergleichen, dann lässt sich die Ursache an der Form der Abweichung eingrenzen.
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.
flowchart TB
accTitle: Die in der Praxis wirksame Reihenfolge
accDescr: Diagramm des praktischen Vorgehens, bei dem zuerst mit der automatischen Konvertierung ein korrektes Bild bestätigt und erst danach auf den manuellen Pfad umgestellt wird, weil sich so leichter erkennen lässt, an welcher Stelle das Bild kaputtgegangen ist.
step1["Zuerst mit der automatischen Konvertierung ein korrektes Bild bestätigen"] --> step2["Danach auf den manuellen Pfad umstellen"]
step2 --> good1["Die kaputte Stelle lässt sich leichter eingrenzen"]
allin1["Von Anfang an alles selbst übernehmen"] -.-> lost1["Schwer erkennbar, wo es kaputtgegangen ist"]
Abbildung 20: Erst das korrekte Bild sichern und dann zur eigenen Implementierung wechseln, dann fällt das Eingrenzen der kaputten Stelle leichter.
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_MATRIXMF_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.
flowchart TB
accTitle: Wie sich die Plane-Grenze von NV12 bestimmt
accDescr: Diagramm dazu, dass sich der Anfang der UV-Plane von NV12 aus dem Produkt aus tatsächlichem Stride und Height ergibt und dass ein Abschneiden anhand des Produkts aus Width und Height zu Farbverschiebungen und beschädigten Bildern führt.
head1["Buffer-Anfang (Y-Plane)"] -->|"um Stride × Height vorrücken"| uv2["Anfang der UV-Plane"]
wr1["Anhand von Width × Height abschneiden"] -.-> brk1["Farbverschiebung, beschädigtes Bild"]
Abbildung 21: Die Grenze der UV-Plane ergibt sich aus Stride × Height und wird nicht anhand von Width × Height abgeschnitten.
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
NV12oderYUY2heraus, nicht RGB - Wenn Sie es sich einfach machen wollen, fordern Sie mit
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGRGB32an - Wenn Sie Kontrolle wollen, nehmen Sie
NV12/YUY2entgegen 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..235und4:2:0/4:2:2unklar, entstehen Farbverschiebungen oder kaputte Bilder
YUV -> RGB wirkt anfangs etwas sperrig. Sitzt aber einmal das Bild —
NV12teilt U/V in 2x2-BlöckenYUY2teilt 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.
flowchart TB
accTitle: Das Bild, das man im Kopf behalten sollte
accDescr: Diagramm dazu, dass die rätselhaften Byte-Folgen wie sinnvolle Pixel aussehen, sobald das Bild sitzt, dass NV12 U/V in 2x2-Blöcken teilt, YUY2 U/V über 2 horizontale Pixel teilt und auf dieses U/V und Y die Matrix angewendet wird.
im2["NV12: U/V in 2x2 geteilt"] --> ap1["Auf dieses U/V und Y die Matrix anwenden"]
im3["YUY2: U/V über 2 horizontale Pixel geteilt"] --> ap1
ap1 --> see1["Die Byte-Folge sieht wie sinnvolle Pixel aus"]
Abbildung 22: Sitzen die beiden Punkte Teilungseinheit und Matrix, lässt sich eine YUV-Byte-Folge unkompliziert lesen.
9. Referenzen
Beispielcode zu diesem Artikel
Verwandte Artikel von KomuraSoft
- Einführung in Media Foundation – die API aus der COM-Perspektive verstehen
- Wie Sie mit Media Foundation ein Standbild aus einer MP4 zu einem bestimmten Zeitpunkt extrahieren
Microsoft Learn
- Source Reader
- Using the Source Reader to Process Media Data
- MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- IMFSourceReader::SetCurrentMediaType
- Recommended 8-Bit YUV Formats for Video Rendering
- Extended Color Information
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- MF_MT_VIDEO_NOMINAL_RANGE attribute
- MFVideoTransferMatrix enumeration
- Video Processor MFT
- Uncompressed RGB Video Subtypes
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
MP4-Frames mit Media Foundation Bilder und Text einbrennen
Wie man mit Media Foundation Bild und Text in jeden Frame eines MP4-Videos einbrennt und daraus ein neues MP4 erzeugt – die Aufgabenteilu...
Ein Standbild aus einer MP4 zu einem bestimmten Zeitpunkt mit Media Foundation extrahieren
Wie man mit dem Source Reader die einem bestimmten Zeitpunkt nächstgelegene Frame aus einer MP4 holt, Stride und den RGB32-Alphakanal kor...
Media Foundation verstehen – Warum sich die API wie COM anfühlt
Was Media Foundation eigentlich ist, zusammen mit dem grundlegenden Vokabular der Windows-Medien-APIs – COM, HRESULT, IMFSourceReader, MF...
Time Travel Debugging — Langlaufende Fehler, die sich nicht reproduzieren, aufzeichnen und zurückspulen
Ein Fehler, der nur einmal im Monat auftritt, hinterlässt im Absturz-Dump nur das Ergebnis. Mit Time Travel Debugging (TTD) in WinDbg zei...
Warum Argumente zerbrechen — Die Regeln der Windows-Kommandozeilenargumente
Windows übergibt CreateProcess eine einzige Zeichenfolge, die der Empfänger zerlegt. Behandelt die Regeln von CommandLineToArgvW, CRT und...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
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.
Technische Beratung und Design-Review
Wenn Sie die Aufgabenteilung bei der YUV-/RGB-Konvertierung, den Farbraum, den Stride und das Design des Konvertierungswegs vorab klären möchten, lässt sich dieses Thema gut als technische Beratung bzw. Design-Review angehen.
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.