Come convertire YUV in RGB con Media Foundation
· Aggiornato il: · Go Komura · 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
IMFSourceReaderporti automaticamente i fotogrammi fino a RGB32 - Modello B: ricevi
NV12/YUY2e 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.libmfreadwrite.libmfuuid.libole32.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_PROCESSINGe richiedereMFVideoFormat_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
NV12eYUY2prima - 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_MATRIXeMF_MT_VIDEO_NOMINAL_RANGE, e supporre che lo stride siawidth * 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.
flowchart TB
accTitle: Le due scelte di questo articolo
accDescr: Diagramma 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.
want1["Servono fotogrammi RGB"] -->|"voglio il percorso facile"| pa1["Modello A: Source Reader emette RGB32"]
want1 -->|"grandi volumi / controllo del colore"| pb1["Modello 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.
flowchart LR
File["MP4 / H.264 / HEVC"] --> Decoder["decodificatore"]
Decoder --> YUV["YUV frame come NV12 / YUY2 / YV12"]
YUV -->|Modello A| SRVP["Source Reader elaborazione video"]
SRVP --> RGB1["RGB32"]
YUV -->|Modello B| App["Il tuo codice di conversione"]
App --> RGB2["BGRA / 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.
- Chiedi al Media Foundation di portare i fotogrammi fino al RGB32
- 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/Vsono i componenti della differenza di coloreRGBogni 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.
flowchart TB
accTitle: Perché si usano i formati della famiglia YUV
accDescr: Diagramma 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.
eye1["L'occhio umano è sensibile alla luminosità"] --> dsn1["Tenere Y fine e U/V grossolano"]
dsn1 --> why1["Il 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.
flowchart TB
accTitle: Confronto fra le unità di condivisione di NV12 e YUY2
accDescr: Diagramma 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.
nv1["NV12 (4:2:0)"] --> sh1["4 pixel di un blocco 2x2 condividono una coppia U/V"]
yy1["YUY2 (4:2:2)"] --> sh2["2 pixel orizzontali condividono una coppia U/V"]
sh1 --> asn1["Decidere a quale pixel assegnare che cosa"]
sh2 --> asn1
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.
- Annulla il sottocampionamento Espandi 4:2:0 o 4:2:2 U / V in modo che ogni pixel possa fare riferimento a un valore
- Annulla l’intervallo Il video Y normalmente utilizza 16..235 e U / V utilizza 16..240, quindi annulla il ridimensionamento
- Applica la matrice
Converti in RGB utilizzando coefficienti come
BT.601oBT.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
flowchart TB
accTitle: I tre livelli da fissare nel codice pratico
accDescr: Diagramma 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.
y1["Fotogramma YUV"] --> up1["Annullare il sottocampionamento"]
up1 --> rg1["Annullare l'intervallo (16..235 ecc.)"]
rg1 --> mx1["Applicare la matrice (601 / 709)"]
mx1 --> rgb2["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_MATRIXMF_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.
flowchart TB
accTitle: Il percorso per non indovinare lo spazio colore
accDescr: Diagramma 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.
gs1["Indovinare in silenzio dalla risoluzione"] --> sl1["La deriva del colore non provoca crash"]
sl1 --> op1["Entra in produzione senza essere notata"]
at1["Guardare gli attributi di matrice e intervallo"] --> ps1["Lasciar passare solo le combinazioni supportate"]
ps1 -.-> sf1["Si 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.
flowchart TB
accTitle: La struttura della formula di conversione
accDescr: Diagramma 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.
yy2["Sottrarre 16 da Y (livello del nero)"] --> co1["Moltiplicare per i coefficienti della matrice"]
uv1["Sottrarre 128 da U / V (centro)"] --> co1
co1 --> cl1["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.
flowchart TB
accTitle: Quando la conversione automatica è adatta e quando no
accDescr: Diagramma 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.
auto1["Conversione automatica del Source Reader"] --> sw1["Elaborazione software"]
sw1 -->|"adatta"| bat1["Immagini fisse, miniature, batch"]
sw1 -.->|"non affidarsi"| rt1["Centinaia 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.
- Negli attributi passati a
MFCreateSourceReaderFromURL, impostareMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - Selezionare il flusso video
- Richiesta
MFMediaType_Video/MFVideoFormat_RGB32tramiteSetCurrentMediaType - Leggi i campioni con
ReadSample
Solo questo fa sì che l’elaborazione video limitata inserita dietro il decoder faccia il YUV -> RGB32 per te.
flowchart TB
accTitle: I quattro passi per abilitare la conversione automatica
accDescr: Diagramma 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.
a1["Abilitare l'elaborazione video negli attributi"] --> a2["Selezionare il flusso video"]
a2 --> a3["Richiedere RGB32"]
a3 --> a4["Leggere con ReadSample"]
a4 -.-> a5["Dietro 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,
×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;
}
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.
flowchart TB
accTitle: La gestione del quarto byte di RGB32
accDescr: Diagramma 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.
r32["Disposizione in memoria di RGB32"] --> bgr1["I 3 byte B, G, R"]
r32 --> b41["Il quarto byte è alpha o don't care"]
b41 -->|"riempire con 0xFF"| wic1["Si passa a WIC come 32bppBGRA"]
b41 -.->|"passare così com'è"| tr3["Può 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
NV12direttamente 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à.
flowchart TB
accTitle: Il compromesso della conversione manuale
accDescr: Diagramma 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.
own1["Scegliere la conversione manuale"] --> res1["Assumersi carico di elaborazione e responsabilità del colore"]
res1 --> fr1["Libertà di ottimizzazione, GPU / SIMD, formato di output"]
res1 --> fr2["Controllo 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.
- Impostare l’uscita Source Reader su
NV12oYUY2 - Ottieni il sottotipo e gli attributi effettivi tramite
GetCurrentMediaType - Controllare
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIXeMF_MT_VIDEO_NOMINAL_RANGE - Estrarre il buffer dal campione e bloccarlo
- Determinare a quale Y / U / V fa riferimento ciascun pixel
- 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.
flowchart TB
accTitle: Flusso complessivo della conversione manuale
accDescr: Diagramma 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.
f1["Richiedere NV12 / YUY2"] --> f2["Verificare sottotipo e attributi effettivi"]
f2 --> f3["Bloccare il buffer"]
f3 --> f4["Determinare Y/U/V di ogni pixel"]
f4 --> f5["Applicare la matrice e scrivere BGRA"]
f2 -.-> nar1["Restringere 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.
flowchart TB
accTitle: Distinguere la richiesta dall'output effettivo
accDescr: Diagramma 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.
req1["Richiedere un sottotipo"] -.->|"non è detto che passi così com'è"| out2["Output effettivo"]
out2 --> gct1["Verificare con GetCurrentMediaType"]
gct1 --> use1["Scrivere 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_flagvale 0 il campomatrix_coefficientsviene 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_MATRIXnon è proprio presente. In quel casoGetUINT32non restituisce alcun valore e fallisce conMF_E_ATTRIBUTENOTFOUND(nel codice sopra viene respinto direttamente dalFAILED(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.
flowchart TB
accTitle: Il bivio quando la matrice è Unknown
accDescr: Diagramma 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.
unk1["Matrice Unknown = non lo sappiamo"] -->|"la scelta di questo articolo"| st4["Respingere con un errore, in modo severo"]
unk1 -->|"se proprio devi lasciar passare"| dflt1["Lasciar passare registrando l'ipotesi nel log"]
unk1 -.->|"solo questo va evitato"| mute1["Arrotondare 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.
flowchart TB
accTitle: L'ordine di priorità dello stride
accDescr: Diagramma 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.
p1["Stride effettivo restituito da Lock2D"] -->|"prima scelta, se disponibile"| acc1["Valore usato per accedere al buffer"]
p2["MF_MT_DEFAULT_STRIDE (valore minimo)"] -->|"fallback"| acc1
p3["Valore fissato a priori in base a width"] -.->|"si rompe in silenzio"| acc1
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.
flowchart TB
accTitle: I due progetti per il chroma upsampling
accDescr: Diagramma 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.
min1["Implementazione minima: usare la crominanza condivisa così com'è"] --> pr1["Visivamente è spesso più che sufficiente"]
hq1["Convertire prima con l'upconversion"] --> pr2["Teoricamente più pulito, priorità alla qualità"]
hq1 -.-> steps1["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
NV12oYUY2 - costruisci un
DecodedFrameInfodaGetCurrentMediaType ReadSampleConvertSampleToBgra32
flowchart TB
accTitle: La ramificazione nella funzione di ingresso
accDescr: Diagramma 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.
smp1["IMFSample"] --> cont1["Estrarre un buffer contiguo"]
cont1 -->|"NV12"| cnv1["Verso la conversione per NV12"]
cont1 -->|"YUY2"| cnv2["Verso la conversione per YUY2"]
cont1 -->|"altro"| err1["Restituire 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,
¤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 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.
flowchart TB
accTitle: Dove passa il confine fra differenza tollerabile e vero bug
accDescr: Diagramma 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.
dif1["Guardare la differenza rispetto al valore atteso"] -->|"differenza di ±1–2"| exp1["Spiegabile con quantizzazione e regola di conversione in intero"]
dif1 -->|"differenza non spiegabile"| bug1["Vero bug"]
bug1 --> prem1["Sospettare 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,
- Modello A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Modello B (ricevi
NV12/YUY2e 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.
flowchart TB
accTitle: La verifica in due fasi
accDescr: Diagramma 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.
st5["Fase 1: confrontare un pixel con il calcolo manuale"] --> st6["Fase 2: confrontare i fotogrammi dei due percorsi"]
st6 --> shp1["Restringere il campo dalla forma della differenza"]
st5 -.-> osf1["Non 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.
flowchart TB
accTitle: L'ordine di lavoro efficace nella pratica
accDescr: Diagramma 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.
step1["Confermare prima l'immagine corretta con la conversione automatica"] --> step2["Poi sostituirla con il percorso manuale"]
step2 --> good1["È facile isolare il punto rotto"]
allin1["Affrontare tutto dall'inizio"] -.-> lost1["Difficile 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_MATRIXMF_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.
flowchart TB
accTitle: Come si calcola il confine dei piani in NV12
accDescr: Diagramma 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.
head1["Inizio del buffer (piano Y)"] -->|"avanzare di stride × height"| uv2["Inizio del piano UV"]
wr1["Tagliare con width × height"] -.-> brk1["Deriva 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.
8. Riepilogo
Quando si converte YUV in RGB con Media Foundation, tenere presente il seguente quadro rende molto più difficile perdersi.
- Dietro il decoder,
NV12oYUY2— non RGB — è quello che normalmente esce - Se desideri il percorso facile, richiedi
RGB32tramiteMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING - Se vuoi il controllo, ricevi
NV12/YUY2e 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..235e4:2:0/4:2:2porta a una deriva dei colori o a immagini interrotte
YUV -> RGB all’inizio è un po’ inavvicinabile. Ma una volta che l’immagine di
NV12condivide U / V su 2x2 blocchiYUY2condivide 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.
flowchart TB
accTitle: L'immagine da tenere in testa
accDescr: Diagramma 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.
im2["NV12: U/V condivisa su blocchi 2x2"] --> ap1["Applicare la matrice a quella U/V insieme a Y"]
im3["YUY2: U/V condivisa su 2 pixel orizzontali"] --> ap1
ap1 --> see1["La 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
- Cos’è Media Foundation - Perché inizi a vedere il volto di COM e i Windows Media API
- 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
Microsoft Learn
- Source Reader
- Utilizzo di Source Reader per elaborare i dati multimediali
- Attributo MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
- IMFSourceReader::SetCurrentMediaType
- Formati YUV a 8 bit consigliati per il rendering video
- Informazioni estese sul colore
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- Attributo MF_MT_VIDEO_NOMINAL_RANGE
- MFVideoTransferMatrix enumerazione
- Video Processor MFT
- Sottotipi video RGB non compressi
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Estrazione di un'immagine fissa da un MP4 in un momento specifico con Media Foundation
Come acquisire il fotogramma più vicino a un determinato tempo in un MP4 con Source Reader, correggere il passo e il RGB32 byte alfa e sa...
Un'introduzione a Media Foundation: comprendere l'API attraverso una lente COM
Spieghiamo cos'è Media Foundation, insieme al vocabolario di base di Windows media API - COM, HRESULT, IMFSourceReader, MFTs - nell'ordin...
L'API thread pool Win32 — Concorrenza senza creare thread, tramite CreateThreadpoolWork
State spargendo chiamate CreateThread per tutto il codice nativo? Questo articolo spiega l'API thread pool Win32 ridisegnata in Vista — i...
Named pipe in pratica — L'IPC standard di Windows, dalla progettazione alla sicurezza
Guida pratica alle named pipe, il meccanismo standard di comunicazione tra processi su Windows. L'articolo organizza, a partire dalle fon...
DllMain e il loader lock — Il vero motivo per cui vi dicono di «non fare niente nell'inizializzazione della DLL»
Perché non dovete chiamare LoadLibrary o sincronizzarvi con altri thread da DllMain. A partire dalle fonti primarie, l'articolo spiega co...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
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.
Consulenza tecnica e revisione del progetto
Se desideri definire in anticipo la divisione delle responsabilità per la conversione YUV / RGB, gli spazi colore, lo stride e la progettazione del percorso di conversione, questo argomento funziona bene come consulenza tecnica / revisione del progetto.
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.