Come convertire YUV in RGB con Media Foundation

· Aggiornato il: · · Media Foundation, C++, Sviluppo Windows, Elaborazione video, YUV

Cronologia delle revisioni (1 aggiornamenti, ultimo il 3 Sep 2026)

Registro delle modifiche apportate a questo articolo. Dove una versione precedente è stata archiviata, resta leggibile tramite un link permanente con DOI.

Sono state ripristinate 21 delle 22 figure dell'articolo che mancavano nella traduzione, con le relative didascalie. I grafi sono identici a quelli giapponesi; sono tradotte solo le etichette. Leggi la versione precedente a questo aggiornamento (DOI: 10.5281/zenodo.22173478)
Prima pubblicazione
Citare questo articolo(DOI: 10.5281/zenodo.22173477)

Questo articolo è archiviato su Zenodo. Qui sotto trovi sia il DOI che rimanda sempre all'ultima versione sia quello fissato alla versione che stai leggendo.

Go Komura (2026). Come convertire YUV in RGB con Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173477 https://comcomponent.com/it/blog/2026/03/15/002-media-foundation-yuv-to-rgb-conversion-patterns/

DOI (ultima versione)
10.5281/zenodo.22173477
DOI (questa versione)
10.5281/zenodo.22279090

Vuoi estrarre un fotogramma da un video e salvarlo come PNG, passarlo a WIC o GDI o visualizzarlo nell’UI. In queste situazioni, il lato dell’applicazione richiede dati pixel RGB.

Tuttavia, i frame che escono da un decoder Media Foundation sono, abbastanza comunemente, formati della famiglia YUV come NV12 o YUY2. Se tratti il ​​flusso di byte grezzi come un’immagine così com’è, ottieni un’immagine piuttosto triste: colori interrotti, bande o una tinta stranamente verdastra.

In un post precedente, Cos’è Media Foundation - Perché inizi a vedere il volto di COM e i Windows Media API, abbiamo trattato il quadro generale e in Come estrarre un’immagine fissa da un MP4 in un momento specifico con Media Foundation: una versione a file singolo che puoi incollare direttamente in un file .cpp abbiamo trattato l’estrazione di immagini fisse. Questa volta affrontiamo il passaggio intermedio: la conversione YUV -> RGB stessa.

In questo articolo separiamo e organizziamo i due modelli seguenti.

  • Modello A: lascia che IMFSourceReader porti automaticamente i fotogrammi fino a RGB32
  • Modello B: ricevi NV12 / YUY2 e converti tu stesso in RGB

L’obiettivo non è memorizzare i nomi API. È poter immaginare, nella tua testa, dove nel Media Foundation appare il YUV e dove si trasforma in RGB.

Il codice visualizzato in questo articolo è pubblicato su GitHub come set di campioni completo (codice C++ per il Modello A / Modello B, configurazione CMake e test per la conversione dei pixel).

modelli-di-conversione-yuv-to-rgb-media-foundation - esempi-blog-komurasoft (GitHub)

Prerequisiti per eseguire i campioni

Se vuoi portare il codice di questo articolo nel tuo progetto, ti servono solo queste cose.

Elemento Prerequisito
OS Windows 10 o successivo
Compilatore MSVC di Visual Studio 2019 / 2022 (C++17)
SDK Windows SDK (header e librerie di importazione di Media Foundation). Incluso nel workload “Sviluppo desktop con C++” di Visual Studio
Build I campioni usano CMake 3.20 o successivo. Puoi anche creare un progetto Visual Studio a mano

Le librerie da collegare sono 4. Nel codice dell’articolo le scriviamo con #pragma comment(lib, ...), ma puoi specificarle anche nelle impostazioni del progetto.

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

Inoltre, il codice dell’articolo presuppone che CoInitializeEx e MFStartup siano già stati eseguiti. Solo la formula di conversione di 1 pixel (5.6) è indipendente dal sistema operativo, quindi nel campione GitHub è stata spostata in un header separato e può essere testata anche con g++ su Linux.

1. Prima la conclusione

Riassumendo le conclusioni in anticipo:

  • Per estrarre alcune immagini fisse o generare miniature, il percorso più semplice è abilitare MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING e richiedere MFVideoFormat_RGB32
  • Tuttavia, questa conversione automatica è un’elaborazione software e non è ottimizzata per la riproduzione in tempo reale
  • Se hai intenzione di scrivere la tua conversione, il percorso più breve è comprendere correttamente NV12 e YUY2 prima
  • YUV -> RGB non è solo “moltiplica per tre coefficienti e il gioco è fatto” — in pratica, sottocampionamento, intervallo, matrice e stride entrano tutti in gioco
  • La documentazione Media Foundation utilizza ampiamente il termine YUV, ma per i video digitali è più facile da leggere se si presuppone che effettivamente significhi Y’CbCr
  • Le cose che più spesso rompono i colori nella pratica sono non guardare MF_MT_YUV_MATRIX e MF_MT_VIDEO_NOMINAL_RANGE, e supporre che lo stride sia width * bytesPerPixel

In breve: se vuoi il percorso facile, prendi l’uscita Source Reader RGB32. Se hai bisogno di un’elaborazione di grandi volumi o di un controllo sul colore, ricevi i fotogrammi come YUV e convertili tu stesso. Queste sono le due scelte.

Le due scelte di questo articoloDiagramma che mostra le due scelte trattate nell'articolo: se vuoi il percorso facile lascia che Source Reader emetta RGB32, se ti servono grandi volumi o il controllo sul colore ricevi i fotogrammi come YUV e convertili tu stesso.voglio il percorso facilegrandi volumi / controllo del coloreServono fotogrammi RGBModello A: Source Reader emette RGB32Modello B: ricevi YUV e converti tu

Figura 1: se scegli la comodità prendi il Modello A, se ti assumi il carico di elaborazione e la responsabilità del colore prendi il Modello B.

2. Inizia con un’immagine

È più veloce osservare prima un diagramma di ciò che accade all’interno di Media Foundation.

Modello AModello BMP4 / H.264 / HEVCdecodificatoreYUV frame come NV12 / YUY2 / YV12Source Reader elaborazione videoRGB32Il tuo codice di conversioneBGRA / RGB

Figura 2: il decoder emette fotogrammi YUV, e da lì la strada verso RGB si divide fra il Modello A e il Modello B.

Se il contenuto del file video è in un formato compresso come H.264 o HEVC, il decoder lo trasforma prima in fotogrammi non compressi. Questi frame non compressi non sono necessariamente RGB. Infatti, nel mondo dei video Windows, i formati della famiglia YUV sono la norma.

Quindi, quando l’applicazione richiede RGB, scegli una delle seguenti.

  1. Chiedi al Media Foundation di portare i fotogrammi fino al RGB32
  2. Ricevi YUV e trasformalo in RGB con il tuo codice

Questo articolo riguarda esattamente quel bivio.

3. Risolvere prima la relazione tra YUV e RGB

3.1. Dice YUV, ma in realtà si tratta di Y’CbCr

I nomi e la documentazione di Windows API utilizzano ampiamente il termine YUV. Nel contesto del video digitale, tuttavia, puoi leggere U come Cb e V come Cr praticamente senza problemi.

In parole povere:

  • Y è il componente orientato alla luminosità
  • U / V sono i componenti della differenza di colore
  • RGB ogni pixel trasporta direttamente Rosso / Verde / Blu

Questa è la relazione.

L’occhio umano è più sensibile ai dettagli più fini nella luminosità che nel colore. Quindi, per i video, un design che mantiene Y al massimo dettaglio e U / V un po’ più grossolano ripaga. Questo è il motivo per cui i formati della famiglia YUV sono così ampiamente utilizzati.

Perché si usano i formati della famiglia YUVDiagramma che mostra come l'occhio umano sia più sensibile ai dettagli di luminosità che a quelli di colore, per cui ripaga un progetto che tiene Y al massimo dettaglio e U/V più grossolano, ed è questo il motivo per cui si usano tanto i formati della famiglia YUV.L'occhio umano è sensibile alla luminositàTenere Y fine e U/V grossolanoIl motivo per cui si usano i formati YUV

Figura 3: tenere Y al massimo dettaglio e U/V più grossolano è il progetto della famiglia YUV, calibrato sulla sensibilità dell’occhio alla luminosità.

3.2. 4:4:4 / 4:2:2 / 4:2:0 è “Quanto è diluito il colore”

Questa è la chiave di lettura di YUV.

Notazione Significato Esempi tipici
4:4:4 Ogni pixel ha il proprio Y / U / V AYUV, I444
4:2:2 2 pixel condividono orizzontalmente una coppia U / V YUY2, UYVY, I422
4:2:0 Un blocco di 2x2 pixel condivide una coppia U / V NV12, YV12, I420

È di grande aiuto osservare innanzitutto la forma dei due formati che incontri maggiormente nella pratica.

Prima però fissiamo un termine. Lo stride (detto anche pitch) è il numero di byte di una riga. Non è la larghezza dell’immagine in sé: indica quanti byte occorre avanzare per raggiungere l’inizio della riga successiva, padding di fine riga incluso. In questo articolo usiamo stride e pitch con lo stesso significato. Anche Microsoft Learn usa entrambi i termini, quindi non serve distinguerli in lettura.

Nei diagrammi seguenti W è la width, H la height e S lo stride. Il punto è che vale S >= W, ma non necessariamente S == W.

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

  <----------- S byte ------------>
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (padding)           |    |
 | Y Y Y Y Y | (padding)           |    | piano Y
 | Y Y Y Y Y | (padding)           |    | S * H byte
 | Y Y Y Y Y | (padding)           |    |
 +-----------+---------------------+  --+  <- confine dei piani = S * H dall'inizio
 | U V U V U | (padding)           |    |
 | U V U V U | (padding)           |    | piano UV
 +-----------+---------------------+  --+  altezza H / 2 righe

  Riga y, Y      : yPlane  + S * y
  Riga y, UV     : uvPlane + S * (y / 2)
  Inizio piano UV: scanline0 + S * H

In NV12, i 4 pixel di un blocco 2x2 condividono una singola coppia U / V. Y esiste per ogni singolo pixel. Il piano UV usa lo stesso stride del piano Y, ma ha metà delle righe. Per questo il confine fra i piani è S * H e non W * H (ci torniamo in 7.5).

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

  <-------------- S byte ----------------->
  <------- W * 2 byte -------->
 +-----------------------------+----------+
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (padding)|   riga 0
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (padding)|   riga 1
 +-----------------------------+----------+

  Inizio della riga y: scanline0 + S * y
  2 pixel = 4 byte (Y, U, Y, V)
  Un solo piano (packed, quindi senza confini)

In YUY2, 2 pixel orizzontali condividono una coppia U / V. Y0 e Y1 sono separati, ma U0 e V0 sono condivisi. Essendo packed non serve calcolare il confine fra i piani, ma per spostarsi fra le righe si usa comunque lo stride.

A questo punto puoi già vedere che YUV -> RGB non è una semplice sostituzione di un pixel con un pixel. Per prima cosa devi pensare a come assegnare l’U / V condiviso a quali pixel.

Confronto fra le unità di condivisione di NV12 e YUY2Diagramma che mostra come in NV12 i 4 pixel di un blocco 2x2 condividano una coppia U/V e in YUY2 la condividano 2 pixel orizzontali, per cui occorre decidere prima a quali pixel assegnare la coppia U/V condivisa.NV12 (4:2:0)4 pixel di un blocco 2x2 condividono una coppia U/VYUY2 (4:2:2)2 pixel orizzontali condividono una coppia U/VDecidere a quale pixel assegnare che cosa

Figura 4: in entrambi i formati U/V è condivisa, quindi la conversione non si riduce a sostituire un pixel alla volta.

3.3. YUV -> RGB è “Conversione spazio colore + conversione campionamento”

Se guardi Informazioni estese sul colore di Media Foundation, la conversione del colore rigorosamente corretta prevede diverse fasi: quantizzazione inversa, sovracampionamento della crominanza, YUV -> RGB, la funzione di trasferimento, conversione delle primarie e infine quantizzazione.

Detto questo, come codice pratico per SDR a 8 bit, è più facile da capire se lo dividi nei tre livelli seguenti.

  1. Annulla il sottocampionamento Espandi 4:2:0 o 4:2:2 U / V in modo che ogni pixel possa fare riferimento a un valore
  2. Annulla l’intervallo Il video Y normalmente utilizza 16..235 e U / V utilizza 16..240, quindi annulla il ridimensionamento
  3. Applica la matrice Converti in RGB utilizzando coefficienti come BT.601 o BT.709

In altre parole, in termini pratici, la conversione YUV -> RGB è il processo di decisione:

  • quale U / V è il colore per quel pixel
  • quali coefficienti utilizzare per riconvertire quel Y / U / V in RGB
I tre livelli da fissare nel codice praticoDiagramma che mostra come nel codice pratico a 8 bit SDR convenga dividere la conversione da YUV a RGB in tre livelli: annullare il sottocampionamento, annullare l'intervallo e applicare la matrice.Fotogramma YUVAnnullare il sottocampionamentoAnnullare l'intervallo (16..235 ecc.)Applicare la matrice (601 / 709)RGB

Figura 5: la conversione non sono tre coefficienti, ma i tre livelli sottocampionamento, intervallo e matrice.

3.4. Tratta BT.601 e BT.709 con noncuranza e i colori deriveranno lentamente

La documentazione di Media Foundation descrive la relazione come BT.601 preferito per SDTV e precedenti e BT.709 per i video oltre SD.

Tuttavia, indovinare silenziosamente “la risoluzione è grande, quindi deve essere 709” non è una grande idea. La deriva del colore non si blocca, quindi entra facilmente in produzione senza essere notata.

Media Foundation può contenere informazioni sullo spazio colore come attributi media type. Come minimo, guarda questi due:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Guardando questi due e accettando esplicitamente solo le combinazioni supportate dal tuo codice rende molto meno probabili gli incidenti silenziosi in seguito.

Il percorso per non indovinare lo spazio coloreDiagramma che mostra come indovinare in silenzio 601 o 709 dalla risoluzione porti la deriva del colore in produzione senza che venga notata, e come guardare gli attributi MF_MT_YUV_MATRIX e MF_MT_VIDEO_NOMINAL_RANGE e lasciar passare esplicitamente solo le combinazioni supportate eviti gli incidenti silenziosi.Indovinare in silenzio dalla risoluzioneLa deriva del colore non provoca crashEntra in produzione senza essere notataGuardare gli attributi di matrice e intervalloLasciar passare solo le combinazioni supportateSi evitano gli incidenti silenziosi

Figura 6: non indovinare lo spazio colore: guarda gli attributi e lascia passare esplicitamente solo le combinazioni che sai gestire.

3.5. La prima formula da imparare è la versione limited range di BT.601

La formula canonica BT.601 a 8 bit si presenta così.

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)

Per BT.709 i coefficienti cambiano. Lo mostreremo nel codice più tardi.

Ciò che conta qui non è memorizzare i coefficienti ma la struttura: sottrarre il livello del nero 16 da Y e visualizzare U / V centrato su 128.

La struttura della formula di conversioneDiagramma che mostra la struttura della formula di conversione: sottrarre il livello del nero 16 da Y, guardare U e V centrati su 128, moltiplicare per i coefficienti della matrice e ritagliare il risultato.Sottrarre 16 da Y (livello del nero)Moltiplicare per i coefficienti della matriceSottrarre 128 da U / V (centro)Ritagliare a 0..255

Figura 7: da ricordare non sono i coefficienti, ma la struttura della formula: livello del nero 16 e centro 128.

4. Modello A: lascia che Media Foundation converta automaticamente

4.1. Quando questa è una buona soluzione

Questo approccio è adatto a situazioni come le seguenti.

  • Vuoi estrarre una singola immagine fissa da un MP4
  • Vuoi creare alcune miniature
  • Vuoi un’immagine RGB da consegnare a WIC
  • L’uso del batch o degli strumenti va bene; questa non è la riproduzione in tempo reale

Source Reader ha una funzione che esegue elaborazione video limitata di YUV -> RGB32 quando si utilizza MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING.

Tuttavia, come nota anche Microsoft Learn, si tratta di elaborazione software e non ottimizzata per la riproduzione. Se vuoi elaborare centinaia di fotogrammi al secondo, affidarti a questo non è proprio lo strumento giusto.

Quando la conversione automatica è adatta e quando noDiagramma che mostra come la conversione automatica del Source Reader sia elaborazione software non ottimizzata per la riproduzione, quindi adatta a estrazione di immagini fisse, miniature e uso batch, ma non a cui affidarsi per centinaia di fotogrammi al secondo.adattanon affidarsiConversione automatica del Source ReaderElaborazione softwareImmagini fisse, miniature, batchCentinaia di fotogrammi al secondo in tempo reale

Figura 8: la conversione automatica è elaborazione software, quindi conviene usarla solo per strumenti che trattano pochi fotogrammi.

4.2. Cosa hai impostato per ottenere RGB32

Il flusso è abbastanza semplice.

  1. Negli attributi passati a MFCreateSourceReaderFromURL, impostare MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. Selezionare il flusso video
  3. Richiesta MFMediaType_Video / MFVideoFormat_RGB32 tramite SetCurrentMediaType
  4. Leggi i campioni con ReadSample

Solo questo fa sì che l’elaborazione video limitata inserita dietro il decoder faccia il YUV -> RGB32 per te.

I quattro passi per abilitare la conversione automaticaDiagramma che mostra i quattro passi della conversione automatica: abilitare l'elaborazione video negli attributi e creare il Reader, selezionare il flusso video, richiedere RGB32 e leggere con ReadSample.Abilitare l'elaborazione video negli attributiSelezionare il flusso videoRichiedere RGB32Leggere con ReadSampleDietro il decoder avviene la conversione in RGB32

Figura 9: bastano quattro passi perché l’elaborazione video inserita dietro il decoder porti i fotogrammi fino a RGB32.

4.3. Codice

Il codice seguente presuppone che CoInitializeEx e MFStartup siano già stati eseguiti. Una versione minimale assomiglia più o meno a questa.

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

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

using Microsoft::WRL::ComPtr;

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

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

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

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

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

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

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

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

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

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

    return S_OK;
}

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

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

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

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

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

Successivamente, chiamando GetCurrentMediaType è possibile verificare la dimensione e lo stride effettivi dell’output.

4.4. Punti di forza di questo approccio

L’aspetto positivo di questo approccio è che ti porta rapidamente a un’immagine corretta.

  • Non è necessario scrivere tu stesso l’espansione 4:2:0 / 4:2:2
  • Nasconde gran parte del fastidio della gestione / deinterlacciamento della matrice
  • L’uscita è facile da consegnare a WIC o GDI
  • Per la lavorazione di una manciata di fotogrammi, è perfettamente pratico

Per gli strumenti di estrazione di immagini fisse, iniziare da qui è del tutto naturale.

4.5. Ma ci sono anche delle insidie

Questa conversione automatica ha le seguenti caratteristiche.

Elemento Dettagli
Obiettivo di conversione Fondamentalmente RGB32
Implementazione Elaborazione software
Adatto per Piccoli numeri di fotogrammi, miniature, elaborazione offline
Non adatto per Rendering in tempo reale basato su D3D, elaborazione di frame ad alto volume
Attributi incompatibili MF_SOURCE_READER_D3D_MANAGER, MF_READWRITE_DISABLE_CONVERTERS

E un’altra cosa importante: la gestione del 4° byte in RGB32. In memoria, Windows RGB32 è strutturato come Blu / Verde / Rosso / Alfa o Non importa. Non è ARGB32. Se lo passi a WIC come 32bppBGRA, è più sicuro riempire il quarto byte con 0xFF per renderlo opaco.

La gestione del quarto byte di RGB32Diagramma che mostra come nel formato RGB32 di Windows il quarto byte dopo B, G e R non sia definito come alpha o don't care, per cui è più sicuro riempirlo con 0xFF e renderlo opaco prima di passarlo a WIC come 32bppBGRA.riempire con 0xFFpassare così com'èDisposizione in memoria di RGB32I 3 byte B, G, RIl quarto byte è alpha o don't careSi passa a WIC come 32bppBGRAPuò risultare trasparente

Figura 10: il quarto byte non è definito, quindi rendilo opaco con 0xFF prima di passarlo a WIC.

Abbiamo accennato a questo come a una cosa facile in cui inciampare anche nel precedente articolo sull’estrazione di immagini fisse.

5. Modello B: scrivi tu stesso la conversione

5.1. Quando questa è una buona soluzione

Effettuare la conversione da soli è una buona soluzione in casi come questi.

  • Elabori un gran numero di frame e desideri ottimizzare tu stesso la conversione
  • Vuoi alimentare NV12 direttamente alla GPU o al codice SIMD
  • Vuoi gestire esplicitamente BT.601 / BT.709 / range
  • Vuoi produrre formati di output diversi da RGB32
  • La conversione automatica limitata del Source Reader non è sufficiente

Potresti chiamarlo il modello in cui ti assumi la responsabilità della produttività e del colore in cambio della libertà.

Il compromesso della conversione manualeDiagramma che mostra come la conversione manuale sia il modello in cui ci si assume il carico di elaborazione e la responsabilità del colore in cambio di ottimizzazione, collegamento a GPU e SIMD, controllo esplicito di matrice e intervallo e libertà sui formati di output.Scegliere la conversione manualeAssumersi carico di elaborazione e responsabilità del coloreLibertà di ottimizzazione, GPU / SIMD, formato di outputControllo esplicito di matrice / intervallo

Figura 11: la conversione manuale è la scelta che, in cambio della responsabilità, va a prendersi prestazioni e libertà sul colore.

5.2. Flusso complessivo della conversione manuale

I passaggi sono i seguenti.

  1. Impostare l’uscita Source Reader su NV12 o YUY2
  2. Ottieni il sottotipo e gli attributi effettivi tramite GetCurrentMediaType
  3. Controllare MF_MT_FRAME_SIZE, MF_MT_DEFAULT_STRIDE, MF_MT_YUV_MATRIX e MF_MT_VIDEO_NOMINAL_RANGE
  4. Estrarre il buffer dal campione e bloccarlo
  5. Determinare a quale Y / U / V fa riferimento ciascun pixel
  6. Applica la matrice e scrivi BGRA

Il codice in questo articolo è limitato a 8 bit SDR / progressivo / NV12 o YUY2 / limited range. Restringere le ipotesi qui non è pigrizia: in realtà è importante. Se scrivi una conversione YUV che “accetta tutto per ora”, tende a rompere silenziosamente i colori.

Flusso complessivo della conversione manualeDiagramma che mostra i passi della conversione manuale: richiedere NV12 o YUY2 in uscita, verificare il media type e gli attributi effettivi, bloccare il buffer, determinare Y/U/V di ogni pixel e applicare la matrice scrivendo BGRA.Richiedere NV12 / YUY2Verificare sottotipo e attributi effettiviBloccare il bufferDeterminare Y/U/V di ogni pixelApplicare la matrice e scrivere BGRARestringere le ipotesi rompe meno i colori

Figura 12: la conversione manuale procede per richiesta, verifica, lock, riferimento e conversione, e più si restringono le ipotesi più diventa sicura.

5.3. Innanzitutto, specificare esplicitamente l’output Media Type

Per prima cosa, dì a Source Reader “per favore, emetti il YUV così com’è.” Ancora una volta, ciò presuppone che CoInitializeEx / MFStartup siano stati completati.

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

Qui passi MFVideoFormat_NV12 o MFVideoFormat_YUY2 come subtype.

Tieni presente che non è garantito che il sottotipo richiesto venga accettato così com’è. Controlla cosa esce effettivamente con GetCurrentMediaType.

Distinguere la richiesta dall'output effettivoDiagramma che mostra come il sottotipo richiesto non venga necessariamente accettato così com'è, per cui occorre verificare con GetCurrentMediaType che cosa esce davvero e scrivere il resto del codice su quei valori.non è detto che passi così com'èRichiedere un sottotipoOutput effettivoVerificare con GetCurrentMediaTypeScrivere il resto del codice sui valori verificati

Figura 13: la richiesta resta una richiesta: verifica sempre l’output effettivo con GetCurrentMediaType prima di usarlo.

5.4. Prima della conversione, accetta solo le informazioni sul colore supportate

Per una conversione manuale, estrarre prima le informazioni minime dal tipo di supporto. L’esempio in questo articolo accetta solo NV12 / YUY2 e lascia passare solo BT.601 o BT.709 per la matrice e solo MFNominalRange_16_235 per l’intervallo.

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

Qui siamo deliberatamente severi. La documentazione dell’enumerazione Media Foundation dice cose come “tratta Unknown come BT.709”, ma in pratica, arrotondarlo silenziosamente rende più difficile notare la deriva del colore. Almeno in una prima implementazione, è più sicuro restituire un errore per combinazioni non supportate.

Quando viene restituito Unknown

Potresti pensare che sia troppo severo, quindi elenchiamo i percorsi da cui arriva Unknown. In genere si tratta di casi in cui il video di partenza non porta con sé informazioni sul colore.

  • Le VUI di H.264 / HEVC non contengono informazioni sul colore. Per lo standard, quando colour_description_present_flag vale 0 il campo matrix_coefficients viene trattato come non specificato. Se il flusso attraversa il decoder senza questa informazione, anche la matrice che arriva a valle resta non specificata
  • YUV grezzo proveniente da un dispositivo di acquisizione o da un container datato. È un percorso che non porta con sé alcuna descrizione dello spazio colore
  • A volte l’attributo MF_MT_YUV_MATRIX non è proprio presente. In quel caso GetUINT32 non restituisce alcun valore e fallisce con MF_E_ATTRIBUTENOTFOUND (nel codice sopra viene respinto direttamente dal FAILED(hr))

Il punto importante è che Unknown non significa “sappiamo che è BT.709” ma “non lo sappiamo”. Applicare 709 a materiale in risoluzione SD sposta i colori, e vale anche il contrario.

Da qui la strategia si divide in due.

  • Respingere in modo severo (la scelta di questo articolo): restituisci un errore come caso non supportato e lascia che sia il livello superiore a stabilire che quel materiale non è gestibile. Dichiarare apertamente di non poterlo trattare è più sicuro di una deriva silenziosa dei colori
  • Fissare un valore predefinito e lasciar passare: se proprio devi lasciar passare il fotogramma, registra nel log quale ipotesi hai fatto in presenza di Unknown. E rendi esplicito di aver deciso 601 / 709 in base alla risoluzione

In entrambi i casi, evita solo di arrotondare in silenzio. La deriva del colore non provoca crash, quindi entra in produzione senza che nessuno se ne accorga.

Il bivio quando la matrice è UnknownDiagramma che mostra come Unknown non significhi che si sa che è BT.709 ma che non si sa, per cui la strategia si divide fra respingere con un errore in modo severo e lasciar passare registrando nel log l'ipotesi fatta, evitando in ogni caso di arrotondare in silenzio.la scelta di questo articolose proprio devi lasciar passaresolo questo va evitatoMatrice Unknown = non lo sappiamoRespingere con un errore, in modo severoLasciar passare registrando l'ipotesi nel logArrotondare in silenzio

Figura 14: Unknown vuol dire «non lo sappiamo», quindi scegli fra respingere e lasciar passare con un log, senza mai arrotondare in silenzio.

Con le fotocamere e le sorgenti derivate da JPEG, potresti voler gestire separatamente i percorsi a gamma completa. Qui deliberatamente non li raggruppiamo silenziosamente insieme: la politica è di restringere esplicitamente le ipotesi accettate da questo codice.

5.5. Leggi il buffer fidandoti dello stride

Anche questa parte è abbastanza importante.

  • MF_MT_DEFAULT_STRIDE è lo stride minimo
  • Il buffer del campione effettivo potrebbe avere uno stride effettivo che include il padding
  • Se IMF2DBuffer::Lock2D è disponibile, preferiscilo

Prendere il pattern helper da Uncompressed Video Buffers di Microsoft Learn e renderlo direttamente utilizzabile ci dà questo.

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

Le definizioni di superficie YUV consigliate sono in alto a sinistra / stride positivo, ma per l’accesso effettivo al buffer è più sicuro utilizzare lo stride (= pitch) restituito dall’API, così com’è. Se qui fissi a priori un valore in base a width, le cose si romperanno silenziosamente più tardi.

L'ordine di priorità dello strideDiagramma che mostra come MF_MT_DEFAULT_STRIDE sia lo stride minimo, come il buffer effettivo possa avere uno stride che include il padding, e come vada preferito il valore restituito da Lock2D evitando di fissare a priori lo stride in base a width.prima scelta, se disponibilefallbacksi rompe in silenzioStride effettivo restituito da Lock2DValore usato per accedere al bufferMF_MT_DEFAULT_STRIDE (valore minimo)Valore fissato a priori in base a width

Figura 15: per spostarti fra le righe usa lo stride misurato da Lock2D e non fissarlo a priori partendo da width.

5.6. Trasformare la formula di conversione per pixel in codice

Qui trattiamo solo le versioni limited range di BT.601 e BT.709. L’output è BGRA32, che è facile da trasferire a WIC o GDI.

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

Ciò che sta accadendo qui è semplice.

  • Sottrai 16 da Y
  • Sottrai 128 da U / V
  • Moltiplicare per i coefficienti della matrice data
  • Ritaglia il risultato a 0..255
  • Imposta il 4° byte BGRA su 255

5.7. Conversione di NV12 in BGRA32

NV12 è 4:2:0, quindi i 4 pixel di un blocco 2x2 condividono lo stesso U / V. Come implementazione minima, l’approccio più comprensibile è utilizzare la crominanza condivisa direttamente per tutti e 4 i pixel.

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;

    // L'inizio del piano UV si trova avanzando di "stride × height".
    // Attenzione: non è width × height (vedi il diagramma di 3.2.)
    const BYTE* uvPlane =
        scanline0 + static_cast<size_t>(actualStride) * info.height;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        // Lo spostamento fra le righe avviene sempre in unità di stride
        const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;

        // Essendo 4:2:0, 2 righe verticali condividono una riga di UV -> y / 2
        // Anche il piano UV usa lo stesso stride del piano Y
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);

        // L'output è BGRA compatto senza padding, quindi 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];

            // Nel piano UV [U, V] si alternano.
            // Poiché 2 pixel orizzontali condividono una coppia, con (x / 2) si
            // ricava "quale coppia" e, dato che una coppia = 2 byte, si moltiplica
            // per 2 per ottenere la posizione in byte. +0 è U, +1 è 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;
}

Questo codice interpreta il sovracampionamento della crominanza in un modo del vicino più vicino. Visivamente è spesso perfettamente funzionante, ma se miri alla massima qualità, un design che esegue prima la conversione 4:2:0 -> 4:2:2 -> 4:4:4, come descritto nell’articolo Microsoft Learn YUV, è teoricamente più pulito.

I due progetti per il chroma upsamplingDiagramma che mostra come l'implementazione minima usi la crominanza condivisa direttamente su 4 pixel con un'interpretazione di tipo nearest-neighbor, spesso più che sufficiente all'occhio, mentre per la massima qualità sia teoricamente più pulito convertire prima da 4:2:0 a 4:2:2 e poi a 4:4:4.Implementazione minima: usare la crominanza condivisa così com'èVisivamente è spesso più che sufficienteConvertire prima con l'upconversionTeoricamente più pulito, priorità alla qualitàOrdine 4:2:0 → 4:2:2 → 4:4:4

Figura 16: quando usare l’implementazione minima con la crominanza condivisa e quando il progetto con upconversion graduale.

5.8. Conversione di YUY2 in BGRA32

YUY2 è compresso 4:2:2. Due pixel condividono semplicemente una coppia U / V, quindi è un po’ più facile da leggere rispetto a 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;
}

In YUY2, i byte sono disposti come Y0 U Y1 V, quindi la struttura di “riutilizzare l’U / V per ogni 2 pixel” è direttamente visibile. Ciò rende il modello mentale più facile da costruire rispetto a NV12.

5.9. Il punto di ingresso quando si chiama da un campione

Infine, se si estrae un buffer contiguo da IMFSample e lo si ramifica per sottotipo, diventa facile da usare.

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

Con questo, i passaggi precedenti diventano:

  • creare il lettore
  • richiesta NV12 o YUY2
  • costruisci un DecodedFrameInfo da GetCurrentMediaType
  • ReadSample
  • ConvertSampleToBgra32
La ramificazione nella funzione di ingressoDiagramma che mostra la struttura della funzione di ingresso: si estrae un buffer contiguo dal sample e si rama verso la conversione per NV12, verso quella per YUY2 oppure verso un errore per gli altri sottotipi.NV12YUY2altroIMFSampleEstrarre un buffer contiguoVerso la conversione per NV12Verso la conversione per YUY2Restituire un errore

Figura 17: all’ingresso si passa a un buffer contiguo, poi si rama per sottotipo e i casi non supportati finiscono onestamente in errore.

Il codice di chiamata effettivo è simile a questo.

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

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

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

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

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

// bgra si può trattare come top-down / 32bpp BGRA

5.10. Dove inserire la “Conversione manuale”

Il codice finora assume la forma di l’applicazione che si converte dopo Source Reader. Questa è la cosa più facile da capire.

Tuttavia, se desideri inserire la conversione all’interno della pipeline Media Foundation, sono disponibili altri progetti.

  • Scrivi il tuo MFT
  • Utilizza Video Processor MFT / XVP
  • Scrivi uno shader NV12 -> RGB sul lato GPU

Andare così lontano cambia in qualche modo l’argomento, quindi questo articolo si concentra sul codice lato applicazione. Tuttavia, è utile sapere che tra “lascia che se ne occupi Media Foundation” e “fai tutto nell’app” c’è una via di mezzo: il Video Processor MFT.

5.11. Verificare che la conversione sia corretta

Gli errori di colore sono difficili da notare ad occhio, quindi separiamo “funziona” da “è corretto”. La verifica procede in due fasi.

Fase 1: confrontare con il calcolo manuale usando valori noti

Prima di far scorrere un video, passare a ConvertLimitedYuvPixelToBgra valori Y/U/V noti. Non servono né file video né Media Foundation.

Per il limited range BT.601, i valori Y/U/V rappresentativi e i valori RGB attesi secondo le formule di 5.6 sono i seguenti.

Colore Y U V R atteso G atteso B atteso
Nero 16 128 128 0 0 0
Bianco 235 128 128 255 255 255
Rosso 81 90 240 254 0 0
Blu 41 240 110 0 0 255

Per esempio, per il rosso: C = 81 - 16 = 65, D = 90 - 128 = -38, E = 240 - 128 = 112. Sostituendo:

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

L’output è in ordine BGRA, quindi in byte 00 00 FE FF.

Il punto importante è perché il rosso non torna 255 ma 254: non è un problema di precisione dei coefficienti, ma del fatto che gli input Y/U/V sono già interi arrotondati.

Se si calcola il rosso (255, 0, 0) in limited range BT.601, si ottiene Y = 16 + 219 × 0.299 = 81.481, U = 90.203, V = 240. Quando si salvano come 8 bit, la parte decimale 0.481 viene persa e Y diventa 81. Quella 0.481 persa, quando si torna indietro, diventa 0.481 × 1.164383 ≈ 0.56 di perdita. 255 − 0.56 = 254.44 — da qui proviene il 254.44 sopra. Anche usando coefficienti a precisione infinita si otterrebbe 254.44; l’arrotondamento a 6 cifre decimali agisce oltre la quarta cifra decimale e non influenza l’output a 8 bit.

Anche come si converte infine in intero cambia il risultato. La ClampToByte di 5.6. limita a [0, 255] e tronca value + 0.5, ovvero arrotonda all’intero più vicino. Con un semplice troncamento (static_cast<BYTE>(value)) il rosso rimane 254, ma valori al bordo come B = 255.04 o R = 0.38 variano di 1. Prima di confrontare due implementazioni, accertati di quale regola usa l’altra parte.

Quindi, i motivi per cui bisogna tollerare ±1–2 non sono “i coefficienti hanno precisioni diverse”, ma “la quantizzazione perde la parte decimale” e “la regola di conversione in intero varia tra le implementazioni”. Al contrario, una differenza che non si spiega con questi due motivi è un vero bug. Se il rosso diventa 250, rosso e blu si scambiano, o le ombre scure si alzano, non è la precisione dei coefficienti: sospetta le premesse della conversione (BT.601 vs BT.709, limited vs full range, scambio U/V, stride letto male). Scartare questi casi come “problema di precisione” fa perdere bug aggiustabili.

Dove passa il confine fra differenza tollerabile e vero bugDiagramma che mostra come una differenza di uno o due livelli si spieghi con la parte decimale persa nella quantizzazione e con la diversa regola di conversione in intero, mentre una differenza non spiegabile così sia un vero bug che impone di sospettare le premesse della conversione.differenza di ±1–2differenza non spiegabileGuardare la differenza rispetto al valore attesoSpiegabile con quantizzazione e regola di conversione in interoVero bugSospettare le premesse: matrice, intervallo, U/V, stride

Figura 18: le differenze piccole si spiegano con quantizzazione e arrotondamento, e quelle che non si spiegano indicano una premessa sbagliata.

Se vuoi scrivere un test, questo modello è sufficiente.

#include <cstdlib>  // std::abs

// Verifica che la differenza rispetto al valore atteso rientri nella tolleranza.
// Il valore atteso è il "colore teorico" (rosso -> 255, 0, 0). La tolleranza
// assorbe le perdite per quantizzazione e le diverse regole di arrotondamento;
// la precisione dei coefficienti non è una scusante.
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 deve essere opaco
}

// Uso (BT.601 limited range)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0);      // nero
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // bianco
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0);     // rosso (la formula dà R=254)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255);    // blu

Lo stesso vale per BT.709, ma i coefficienti cambiano e quindi cambiano i valori Y/U/V. Per BT.709 il rosso è Y=63, U=102, V=240. Non usare i valori del 601 nel ramo 709, o i colori si sposteranno. Separare le righe di test per ogni matrix permette di catturare subito uno scambio di matrix.

Il campione su GitHub isola questa conversione di un singolo pixel in un header indipendente dal sistema operativo, quindi il test gira anche senza Windows.

Fase 2: confrontare gli output del Modello A e del Modello B

Una volta che il singolo pixel torna, passare al confronto fra due intere immagini dello stesso fotogramma. Dallo stesso istante dello stesso video,

  1. Modello A (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. Modello B (ricevi NV12 / YUY2 e converti tu)

estrai un fotogramma per ciascuno dei due percorsi e confrontali pixel per pixel.

Come leggere la differenza:
  Per ogni pixel calcola |A.R - B.R|, |A.G - B.G|, |A.B - B.B|
  Trova il massimo e la percentuale di pixel che superano la soglia

Non aspettarti una corrispondenza perfetta. I motivi sono due.

  • L’elaborazione video del Source Reader potrebbe effettuare il chroma upsampling con metodi diversi dal nearest-neighbor. La nostra implementazione di riferimento in 5.7. usa la crominanza condivisa così com’è su 4 pixel, quindi le differenze emergono soprattutto sui bordi
  • Gestione diversa di arrotondamenti e precisione intermedia

Quindi bisogna guardare il pattern della differenza, non il semplice “coincidono o no”.

Differenza osservata Cosa sospettare
Le aree uniformi coincidono, differenze solo ai bordi di colore Differenza nel chroma upsampling — nel range atteso
Tutta l’immagine è spostata uniformemente Scambio di matrix (601 / 709) o range (16..235 / 0..255)
Si formano strisce o si sposta in diagonale Stride fissato a priori. Vedi 7.2. e 7.5.
Rosso e blu si scambiano Confusione tra BGRA e RGBA
Tutto appare trasparente o completamente nero Il quarto byte non è riempito con 0xFF. Vedi 7.1.

Dal “modulo” della differenza si restringe molto il campo: un offset uniforme indica formula o informazioni colore errate, una differenza locale indice o stride.

La verifica in due fasiDiagramma che mostra la verifica in due fasi: prima si passa un solo pixel con Y/U/V noti e lo si confronta con il calcolo manuale, poi si confrontano i fotogrammi prodotti dai due percorsi sullo stesso istante dello stesso video, restringendo il campo dalla forma della differenza.Fase 1: confrontare un pixel con il calcolo manualeFase 2: confrontare i fotogrammi dei due percorsiRestringere il campo dalla forma della differenzaNon servono né il video né Media Foundation

Figura 19: superata la verifica su un pixel, il confronto sull’intero fotogramma permette di risalire alla causa dalla forma della differenza.

6. Quale dovresti scegliere?

In caso di dubbio, la tabella seguente risolve abbastanza bene le cose.

Aspetto Conversione automatica (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) Conversione manuale
Velocità di implementazione Ottimo Modesto
Estrazione di poche immagini fisse Ottimo Buono
Volume frame elevato / tempo reale Modesto Ottimo
Controllo esplicito di matrice / intervallo Modesto Ottimo
Combinazione con GPU / D3D Modesto Da buono a ottimo
Formati di output diversi da RGB32 Modesto Ottimo
Comprendere i fondamenti Buono Ottimo

Per la tua prima implementazione, questa inquadratura semplifica il compito.

  • Voglio che funzioni prima -> conversione automatica
  • Voglio possedere colore e prestazioni -> conversione manuale

In pratica, anche la sequenza “confermare prima l’immagine corretta con la conversione automatica, quindi sostituirla con il percorso manuale” è abbastanza efficace. Se si affronta tutto dall’inizio, diventa difficile dire dove si è interrotta l’immagine.

L'ordine di lavoro efficace nella praticaDiagramma che mostra come, nella pratica, confermare prima l'immagine corretta con la conversione automatica e poi sostituirla con il percorso manuale renda più facile capire dove l'immagine si è rotta.Confermare prima l'immagine corretta con la conversione automaticaPoi sostituirla con il percorso manualeÈ facile isolare il punto rottoAffrontare tutto dall'inizioDifficile capire dove si è rotto

Figura 20: assicurarsi prima l’immagine corretta e passare poi all’implementazione manuale rende facile isolare il punto rotto.

7. Insidie facili da individuare nella pratica

7.1. Supponendo che RGB32 sia RGBA con Alpha

In memoria, RGB32 è B, G, R, Alpha or Don't Care. Se lo scrivi su un PNG come BGRA così com’è, il quarto byte potrebbe essere 0, rendendo l’immagine trasparente. È più sicuro impostarlo su 0xFF prima di salvare.

7.2. Fissare a priori lo stride come width * bytesPerPixel

Un incidente molto comune. Il buffer di esempio effettivo può contenere padding, quindi la regola è utilizzare lo stride effettivo per spostarsi tra le righe.

7.3. Confondere MF_MT_DEFAULT_STRIDE con il pitch effettivo

MF_MT_DEFAULT_STRIDE è “lo stride minimo quando quel formato è rappresentato in memoria contigua”. Per il pitch effettivo del buffer del campione, preferisci il valore restituito da IMF2DBuffer::Lock2D. (pitch è un altro nome per stride. Come accennato in 3.2., in questo articolo li usiamo con lo stesso significato.)

7.4. Indovina silenziosamente 601 / 709 senza guardare i metadati dei colori

Gli incidenti di colore sono difficili da vedere. Nemmeno loro si schiantano. Questo è ciò che li rende fastidiosi.

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Per lo meno, guarda questi. E l’atteggiamento giusto è più o meno: i valori che il tuo codice non supporta dovrebbero essere errori.

7.5. Tagliare il piano UV di NV12 con width * height

L’offset del piano è determinato dallo stride effettivo e dall’altezza. Non da width * height. Se lo fai in modo sciatto, otterrai colori spostati o immagini corrotte.

Come si calcola il confine dei piani in NV12Diagramma che mostra come l'inizio del piano UV di NV12 si determini con il prodotto fra lo stride effettivo e l'altezza, mentre tagliare con il prodotto fra width e height porti a deriva del colore e immagini corrotte.avanzare di stride × heightInizio del buffer (piano Y)Inizio del piano UVTagliare con width × heightDeriva del colore, immagine corrotta

Figura 21: il confine del piano UV si trova con stride × height, non tagliando a width × height.

7.6. Elaborazione di video interlacciati presupponendo che sia progressivo

Gli esempi manuali in questo articolo presuppongono video progressivo. Leggere il contenuto interlacciato come se ogni fotogramma fosse un singolo campo può produrre artefatti a pettine. Se hai bisogno del deinterlacciamento, è più naturale considerare l’elaborazione video automatica del Source Reader o del Video Processor MFT.

7.7. Ignorare la qualità dell’upsampling cromatico 4:2:0

Per chiarezza, la conversione NV12 in questo articolo utilizza la crominanza condivisa direttamente per ogni pixel. Ciò è sufficiente per molti usi, ma se la priorità è la qualità dell’immagine, vale la pena studiare l’approccio di conversione descritto nella documentazione dei formati YUV consigliati.

Quando si converte YUV in RGB con Media Foundation, tenere presente il seguente quadro rende molto più difficile perdersi.

  • Dietro il decoder, NV12 o YUY2 — non RGB — è quello che normalmente esce
  • Se desideri il percorso facile, richiedi RGB32 tramite MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
  • Se vuoi il controllo, ricevi NV12 / YUY2 e converti tu stesso in BGRA
  • Nel percorso manuale, ottieni campionamento / intervallo / matrice / stride subito prima di preoccuparti della formula
  • Essere vaghi su BT.601 / BT.709, 16..235 e 4:2:0 / 4:2:2 porta a una deriva dei colori o a immagini interrotte

YUV -> RGB all’inizio è un po’ inavvicinabile. Ma una volta che l’immagine di

  • NV12 condivide U / V su 2x2 blocchi
  • YUY2 condivide U / V su 2 pixel orizzontali
  • applicare la matrice a quell’U / V insieme a Y

ti si sistema in testa, diventa piuttosto docile. Quelle misteriose sequenze di byte di colore cosmico iniziano a sembrare pixel propriamente significativi.

L'immagine da tenere in testaDiagramma che mostra come, una volta entrata in testa l'immagine per cui NV12 condivide U/V su blocchi 2x2, YUY2 la condivide su 2 pixel orizzontali e a quella U/V insieme a Y si applica la matrice, le misteriose sequenze di byte inizino a sembrare pixel dotati di senso.NV12: U/V condivisa su blocchi 2x2Applicare la matrice a quella U/V insieme a YYUY2: U/V condivisa su 2 pixel orizzontaliLa sequenza di byte diventa pixel dotati di senso

Figura 22: fissate le due immagini dell’unità di condivisione e della matrice, la sequenza di byte YUV si legge senza fatica.

9. Riferimenti

Codice di esempio per questo articolo

Articoli KomuraSoft correlati

Microsoft Learn

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Sviluppo di applicazioni Windows

Questo argomento tratta Media Foundation, Source Reader, salvataggio di immagini e conversione di fotogrammi video: un tema di implementazione dell'elaborazione multimediale Windows che si adatta bene al nostro servizio di sviluppo di applicazioni Windows.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Perché i frame di un decoder Media Foundation sono in YUV anziché in RGB?
Nel mondo dei video Windows, l'output del decodificatore non compresso è in genere un formato della famiglia YUV come NV12 o YUY2, non RGB. L'occhio umano è più sensibile ai dettagli più fini in termini di luminosità che a colori, quindi i formati video mantengono la componente Y (luminosità) al massimo dettaglio e assottigliano le componenti U / V (differenza di colore): un blocco di 2x2 pixel condivide una coppia U / V in formati 4:2:0 come NV12 e 2 pixel orizzontali condividono una coppia in formati 4:2:2 come YUY2. Quando la tua app necessita di RGB, puoi lasciare che Source Reader venga convertito in RGB32 o convertire tu stesso YUV.
Qual è il modo più semplice per ottenere RGB fotogrammi da un video in Media Foundation?
Abilita MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING durante la creazione di Source Reader e richiedi MFVideoFormat_RGB32 tramite SetCurrentMediaType: il lettore eseguirà quindi la conversione da YUV aRGB32 per te. Questo è particolarmente adatto per estrarre alcune immagini fisse, miniature e strumenti offline. Tuttavia, si tratta dell'elaborazione software che non è ottimizzata per la riproduzione, quindi è lo strumento sbagliato per il rendering in tempo reale basato su D3D o l'elaborazione di fotogrammi a volume elevato ed è incompatibile con MF_SOURCE_READER_D3D_MANAGER e MF_READWRITE_DISABLE_CONVERTERS.
Perché i colori convertiti appaiono leggermente errati o verdastri?
Le cause più comuni sono il non guardare gli attributi della matrice dei colori e dell'intervallo nominale, e il dare per scontato che lo stride sia uguale a width per bytesPerPixel. BT.601 e BT.709 utilizzano coefficienti di conversione diversi e il video Y normalmente utilizza l'intervallo limitato 16-235 con U / V a 16-240, quindi indovinando '709 perché la risoluzione è ampia' i colori vengono spostati silenziosamente senza arresti anomali. Controlla MF_MT_YUV_MATRIX e MF_MT_VIDEO_NOMINAL_RANGE sul media type effettivo e accetta esplicitamente solo le combinazioni supportate dal tuo codice di conversione.
Quando dovrei scrivere la mia conversione da YUV a RGB invece di utilizzare quella di Source Reader?
Scrivi la tua conversione quando elabori un numero elevato di fotogrammi e desideri ottimizzarlo, vuoi alimentare NV12 direttamente alla GPU o al codice SIMD, hai bisogno di un controllo esplicito su BT.601 / BT.709 e sulla gestione dell'intervallo o hai bisogno di formati di output diversi da RGB32. I passaggi consistono nell'impostare l'output Source Reader su NV12 o YUY2, verificare cosa è effettivamente uscito con GetCurrentMediaType, controllare gli attributi dimensione frame, stride, matrice e intervallo, quindi bloccare il buffer e applicare la matrice per pixel. Restringere i presupposti accettati è importante: un convertitore che accetta tutto tende a rompere i colori silenziosamente.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog