YUV mit Media Foundation in RGB konvertieren
· Go Komura · 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:
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.
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"]
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.
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.
- 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
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.
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.
- 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.
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.
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.
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.
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_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.
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.
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.
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
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.
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:
- Muster A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Muster B (
NV12/YUY2entgegennehmen 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_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.
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.
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...
Fallstricke bei Shared Memory und Best Practices für die Praxis
Wir ordnen die Fallstricke beim praktischen Einsatz von Shared Memory und ein Design ein, das die Unfallrate senkt – von Synchronisation ...
Eine C# Native AOT DLL aus C/C++ aufrufen
Wie man eine C#-Klassenbibliothek mit Native AOT als native DLL veröffentlicht und Einstiegspunkte mit UnmanagedCallersOnly aus C/C++ auf...
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.