Comment convertir du YUV en RGB avec Media Foundation
· Mis à jour le: · Go Komura · Media Foundation, C++, Développement Windows, Traitement vidéo, YUV
Historique des révisions (1 mises à jour, dernière le 3 Sep 2026)
Journal des modifications apportées à cet article. Lorsqu'une version antérieure a été archivée, elle reste consultable via un lien permanent avec DOI.
- Les 21 des 22 figures de l'article qui manquaient dans la traduction ont été rétablies, avec leurs légendes. Les graphes sont identiques aux graphes japonais ; seules les étiquettes sont traduites. Lire la version antérieure à cette mise à jour (DOI: 10.5281/zenodo.21618372)
- Première publication
Citer cet article(DOI: 10.5281/zenodo.21618371)
Cet article est archivé sur Zenodo. Vous trouverez ci-dessous le DOI qui renvoie toujours à la dernière version et celui qui est figé sur la version que vous lisez.
Go Komura (2026). Comment convertir du YUV en RGB avec Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21618371 https://comcomponent.com/fr/blog/2026/03/15/002-media-foundation-yuv-to-rgb-conversion-patterns/
- DOI (dernière version)
- 10.5281/zenodo.21618371
- DOI (cette version)
- 10.5281/zenodo.22279083
Vous voulez extraire une frame d’une video pour l’enregistrer en PNG, la passer à WIC ou à GDI, ou l’afficher dans une UI. Dans ces cas-là, côté application, on veut une suite de pixels RGB.
Or, les frames qui sortent d’un decoder Media Foundation sont, très couramment, dans un format de la famille YUV tel que NV12 ou YUY2. Si l’on traite tel quel ce flux d’octets bruts comme une image, on obtient une image un peu triste : couleurs cassées, bandes, ou une teinte étrangement verdâtre.
Dans un précédent article, Qu’est-ce que Media Foundation - pourquoi on finit par voir le visage de COM et des API media de Windows, nous avons dressé la vue d’ensemble, et dans Comment extraire une image fixe d’une video MP4 à un instant précis avec Media Foundation - une version en un seul fichier prête à coller dans un .cpp, nous avons détaillé l’extraction d’image fixe. Cette fois, nous abordons ce qui se trouve entre les deux : la conversion YUV -> RGB elle-même.
Dans cet article, nous distinguons et organisons les deux approches suivantes.
- Pattern A : laisser
IMFSourceReaderamener automatiquement les frames jusqu’à RGB32 - Pattern B : recevoir
NV12/YUY2et effectuer soi-même la conversion vers RGB
L’objectif n’est pas de mémoriser des noms d’API. C’est de pouvoir se représenter mentalement où, dans Media Foundation, le YUV apparaît, et où il se transforme en RGB.
Par ailleurs, le code présenté dans cet article est publié sur GitHub sous forme d’un ensemble complet d’exemples (code C++ pour les patterns A et B, configuration CMake, et tests de la conversion de pixels).
media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)
Environnement requis pour exécuter le code
Si vous reprenez le code de cet article dans votre propre projet, voici tout ce dont vous avez besoin.
| Élément | Prérequis |
|---|---|
| OS | Windows 10 ou version ultérieure |
| Compilateur | MSVC de Visual Studio 2019 / 2022 (C++17) |
| SDK | Windows SDK (en-têtes et bibliothèques d’importation de Media Foundation). Il est inclus dans la charge de travail « Développement Desktop en C++ » de Visual Studio |
| Build | Les exemples utilisent CMake 3.20 ou version ultérieure. Vous pouvez tout aussi bien créer un projet Visual Studio à la main |
Il y a quatre bibliothèques à lier. Le code de l’article les déclare avec #pragma comment(lib, ...), mais les indiquer dans les paramètres du projet revient au même.
mfplat.libmfreadwrite.libmfuuid.libole32.lib
Par ailleurs, le code de cet article suppose que CoInitializeEx et MFStartup ont déjà été appelés. Seule la formule de conversion d’un pixel (5.6.) est indépendante du système d’exploitation : dans les exemples GitHub, elle est isolée dans un en-tête séparé, ce qui permet de la tester avec g++ sous Linux.
1. D’abord la conclusion
Pour résumer la conclusion d’emblée, voici l’essentiel.
- Pour extraire quelques images fixes ou générer des miniatures, le plus simple est d’activer
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGet de demanderMFVideoFormat_RGB32 - Cependant, cette conversion automatique est un traitement logiciel (software), qui n’est pas optimisé pour la lecture en temps réel
- Si vous écrivez votre propre conversion, le chemin le plus court est de bien comprendre
NV12etYUY2en premier lieu - YUV -> RGB n’est pas « terminé une fois qu’on a multiplié par trois coefficients » : en réalité, le sous-échantillonnage, le range, la matrix et le stride entrent tous en jeu
- La documentation de Media Foundation utilise largement le terme
YUV, mais pour la video numérique, il est plus simple de lire cela en pensant qu’il s’agit en réalité de Y’CbCr - En pratique, ce qui casse le plus souvent les couleurs, c’est de ne pas regarder
MF_MT_YUV_MATRIXetMF_MT_VIDEO_NOMINAL_RANGE, et de supposer que le stride vautwidth * bytesPerPixel
En résumé : si vous voulez la solution de facilité, laissez le Source Reader produire du RGB32. Si vous voulez un traitement massif ou le contrôle des couleurs, recevez le YUV tel quel et convertissez-le vous-même. C’est ce choix binaire.
flowchart TB
accTitle: Le choix binaire de cet article
accDescr: Schéma du choix binaire traité par cet article : laisser le Source Reader produire du RGB32 si l'on veut la solution de facilité, et recevoir le YUV tel quel pour le convertir soi-même si l'on veut un traitement massif et le contrôle des couleurs.
want1["Il faut des frames RGB"] -->|"solution de facilité"| pa1["Pattern A : laisser le Source Reader produire du RGB32"]
want1 -->|"traitement massif / contrôle des couleurs"| pb1["Pattern B : recevoir le YUV et convertir soi-même"]
Figure 1 : si l’on privilégie la facilité, c’est le Pattern A ; si l’on assume le débit et la responsabilité des couleurs, c’est le Pattern B.
2. Commencer par un schéma
Pour commencer, il est plus rapide de visualiser sur un schéma ce qui se passe à l’intérieur de Media Foundation.
flowchart LR
File["MP4 / H.264 / HEVC"] --> Decoder["decoder"]
Decoder --> YUV["Trames YUV telles que NV12 / YUY2 / YV12"]
YUV -->|Pattern A| SRVP["Video processing du Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|Pattern B| App["Votre propre code de conversion"]
App --> RGB2["BGRA / RGB"]
Figure 2 : ce que produit le decoder, ce sont des frames YUV ; à partir de là, le chemin vers le RGB se sépare en Pattern A et Pattern B.
Si le contenu du fichier video est dans un format compressé tel que H.264 ou HEVC, le decoder commence par le reconvertir en frames non compressées. Ces frames non compressées ne sont pas nécessairement en RGB. Au contraire, dans l’écosystème video de Windows, les formats YUV sont la norme.
Ainsi, lorsque l’application a besoin de RGB, on choisit l’une des deux options suivantes.
- Laisser Media Foundation amener les frames jusqu’à RGB32
- Recevoir le YUV et le transformer en RGB avec son propre code
C’est précisément ce point de bifurcation qui fait l’objet de cet article.
3. Clarifier d’abord la relation entre YUV et RGB
3.1. On dit YUV, mais il s’agit en réalité de Y’CbCr
Les noms d’API et la documentation Windows utilisent largement le terme YUV. Cependant, dans le contexte de la video numérique, on peut lire U comme Cb et V comme Cr sans que cela pose de problème.
En simplifiant :
Yest la composante liée à la luminositéU/Vsont les composantes de différence de couleurRGBcorrespond au fait que chaque pixel porte directement Red / Green / Blue
C’est cette relation.
L’œil humain est plus sensible aux détails de luminosité qu’aux détails de couleur. C’est pourquoi, pour la video, une conception qui garde Y avec précision et U/V un peu plus grossièrement est efficace. C’est la raison pour laquelle les formats de la famille YUV sont si largement utilisés.
flowchart TB
accTitle: Pourquoi les formats de la famille YUV sont utilisés
accDescr: Schéma montrant que, l'œil humain étant plus sensible aux détails de luminosité qu'aux détails de couleur, une conception qui garde Y finement et U/V grossièrement est efficace, ce qui explique pourquoi les formats de la famille YUV sont si utilisés.
eye1["L'œil humain est sensible à la luminosité"] --> dsn1["Garder Y finement et U/V grossièrement"]
dsn1 --> why1["Raison de l'usage des formats de la famille YUV"]
Figure 3 : garder Y finement et U/V grossièrement, c’est la conception de la famille YUV, calquée sur la sensibilité de l’œil à la luminosité.
3.2. 4:4:4 / 4:2:2 / 4:2:0 : « à quel point la couleur est sous-échantillonnée »
C’est le point clé pour comprendre le YUV.
| Notation | Signification | Exemples typiques |
|---|---|---|
| 4:4:4 | Chaque pixel possède ses propres Y/U/V | AYUV, I444 |
| 4:2:2 | 2 pixels horizontaux partagent une paire U/V | YUY2, UYVY, I422 |
| 4:2:0 | Un bloc de 2x2 pixels partage une paire U/V | NV12, YV12, I420 |
Il est très utile de commencer par visualiser la structure des deux formats que l’on rencontre le plus souvent en pratique.
Fixons d’abord un terme. Le stride (aussi appelé pitch) est le nombre d’octets d’une ligne. Ce n’est pas la largeur de l’image elle-même : il indique « de combien d’octets avancer pour atteindre le début de la ligne suivante », padding de fin de ligne compris. Dans cet article, stride et pitch sont employés au même sens. Microsoft Learn utilise lui aussi les deux formulations, il n’y a donc pas lieu de les distinguer à la lecture.
Dans les schémas ci-dessous, W désigne la width, H la height et S le stride. Le point essentiel est que S >= W, sans que S == W soit garanti.
NV12 (4:2:0, planar) / width = W, height = H, stride = S
<---------- S octets ----------->
<--- W --->
+-----------+---------------------+ --+
| Y Y Y Y Y | (padding) | |
| Y Y Y Y Y | (padding) | | plan Y
| Y Y Y Y Y | (padding) | | S * H octets
| Y Y Y Y Y | (padding) | |
+-----------+---------------------+ --+ <- limite de plan = S * H depuis le debut
| U V U V U | (padding) | |
| U V U V U | (padding) | | plan UV
+-----------+---------------------+ --+ hauteur : H / 2 lignes
Y de la ligne y : yPlane + S * y
UV de la ligne y : uvPlane + S * (y / 2)
Debut du plan UV : scanline0 + S * H
Dans NV12, les 4 pixels d’un block 2x2 partagent une paire U/V. Y existe pour chaque pixel individuellement.
Le plan UV utilise le même stride que le plan Y, mais son nombre de lignes est divisé par deux. La limite entre les plans se situe donc à S * H, et non à W * H (nous y revenons en 7.5.).
YUY2 (4:2:2, packed) / width = W, height = H, stride = S
<-------------- S octets --------------->
<------- W * 2 octets ------->
+-----------------------------+----------+
| Y0 U0 Y1 V0 Y2 U2 Y3 V2 … | (padding)| ligne 0
| Y0 U0 Y1 V0 Y2 U2 Y3 V2 … | (padding)| ligne 1
+-----------------------------+----------+
Debut de la ligne y : scanline0 + S * y
2 pixels = 4 octets (Y, U, Y, V)
un seul plan (packed, donc pas de limite)
Dans YUY2, 2 pixels horizontaux partagent une paire U/V. Y0 et Y1 sont distincts, mais U0 et V0 sont partagés.
Comme le format est packed, aucun calcul de limite de plan n’est nécessaire, mais le déplacement d’une ligne à l’autre passe malgré tout par le stride.
À ce stade, on voit déjà que YUV -> RGB n’est pas une simple substitution pixel par pixel. Il faut d’abord réfléchir à comment assigner le U/V partagé à quels pixels.
flowchart TB
accTitle: Comparaison des unités de partage de NV12 et YUY2
accDescr: Schéma montrant que NV12 fait partager une paire U/V aux 4 pixels d'un bloc 2x2 et que YUY2 la fait partager à 2 pixels horizontaux, d'où la nécessité de décider d'abord à quel pixel assigner le U/V partagé.
nv1["NV12 (4:2:0)"] --> sh1["4 pixels en 2x2 partagent une paire U/V"]
yy1["YUY2 (4:2:2)"] --> sh2["2 pixels horizontaux partagent une paire U/V"]
sh1 --> asn1["Décider comment les assigner à chaque pixel"]
sh2 --> asn1
Figure 4 : dans les deux formats, U/V est partagé ; la conversion ne se réduit donc pas à une substitution pixel par pixel.
3.3. YUV -> RGB, c’est « une conversion d’espace colorimétrique + une conversion d’échantillonnage »
Si l’on consulte la page Extended Color Information de Media Foundation, on voit qu’une conversion de couleur rigoureuse comporte pas mal d’étapes : inverse quantization, chroma upsampling, YUV -> RGB, transfer function, conversion des primaries, et jusqu’à la quantization.
Cela dit, pour du code pratique en 8-bit SDR, il est plus facile de commencer par comprendre cela en le découpant en trois couches.
- Annuler le sous-échantillonnage Étendre les U/V en 4:2:0 ou 4:2:2 de manière à ce que chaque pixel puisse en référencer une valeur
- Annuler le range Le Y de la video utilise normalement 16..235, et U/V 16..240, donc on annule cette mise à l’échelle
- Appliquer la matrix
Convertir vers RGB à l’aide de coefficients tels que
BT.601ouBT.709
Autrement dit, en pratique, la conversion YUV -> RGB consiste à déterminer :
- quel U/V correspond à la couleur de ce pixel
- avec quels coefficients reconvertir ce Y/U/V en RGB
flowchart TB
accTitle: Les trois couches à retenir pour du code pratique
accDescr: Schéma montrant que, pour du code pratique en 8-bit SDR, découper la conversion de YUV vers RGB en trois couches — annuler le sous-échantillonnage, annuler le range, appliquer la matrix — la rend plus facile à comprendre.
y1["Frame YUV"] --> up1["Annuler le sous-échantillonnage"]
up1 --> rg1["Annuler le range (16..235, etc.)"]
rg1 --> mx1["Appliquer la matrix (601 / 709)"]
mx1 --> rgb2["RGB"]
Figure 5 : la conversion ne tient pas à trois coefficients, mais aux trois couches sous-échantillonnage, range et matrix.
3.4. Traiter BT.601 et BT.709 à la légère fait dériver les couleurs en douceur
La documentation de Media Foundation décrit la relation ainsi : BT.601 est privilégié pour la SDTV et en dessous, BT.709 pour la video au-delà de la SD.
Cependant, deviner silencieusement « la résolution est grande, donc ce doit être du 709 » n’est pas une bonne idée. Une dérive de couleur ne provoque pas de crash, ce qui fait qu’elle passe facilement inaperçue et se retrouve en production.
Media Foundation peut porter l’information d’espace colorimétrique dans les attributs du media type. Il faut au minimum regarder ces deux-là.
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
En vérifiant ces deux attributs et en ne laissant passer explicitement que les combinaisons prises en charge par votre code, vous réduisez le risque d’incidents silencieux par la suite.
flowchart TB
accTitle: Le cheminement pour ne pas deviner l'espace colorimétrique
accDescr: Schéma montrant que deviner silencieusement 601 ou 709 d'après la résolution laisse la dérive de couleur passer inaperçue jusqu'en production, et qu'il vaut mieux regarder les attributs MF_MT_YUV_MATRIX et MF_MT_VIDEO_NOMINAL_RANGE pour ne laisser passer explicitement que les combinaisons prises en charge.
gs1["Deviner silencieusement d'après la résolution"] --> sl1["Une dérive de couleur ne provoque pas de crash"]
sl1 --> op1["Elle passe en production sans être remarquée"]
at1["Regarder les attributs matrix et range"] --> ps1["Ne laisser passer que les combinaisons prises en charge"]
ps1 -.-> sf1["Les incidents silencieux sont évités"]
Figure 6 : ne devinez pas l’espace colorimétrique ; regardez les attributs et ne laissez passer explicitement que les combinaisons que vous savez traiter.
3.5. La première formule à retenir est la version limited range de BT.601
Voici la formule représentative du BT.601 en 8 bits.
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)
Avec BT.709, les coefficients changent. Nous les retrouverons plus loin dans le code.
Ce qui compte ici n’est pas de mémoriser les coefficients, mais de retenir la structure : on soustrait le niveau de noir 16 à Y, et on considère U/V comme centrés sur 128.
flowchart TB
accTitle: La structure de la formule de conversion
accDescr: Schéma présentant la structure de la formule de conversion : soustraire le niveau de noir 16 à Y, considérer U et V comme centrés sur 128, multiplier par les coefficients propres à chaque matrix, puis clipper le résultat.
yy2["Soustraire 16 à Y (niveau de noir)"] --> co1["Multiplier par les coefficients de la matrix"]
uv1["Soustraire 128 à U / V (centre)"] --> co1
co1 --> cl1["Clipper sur 0..255"]
Figure 7 : ce qu’il faut retenir, ce ne sont pas les coefficients, mais la structure de la formule — le niveau de noir 16 et le centre 128.
4. Pattern A : laisser Media Foundation convertir automatiquement
4.1. Dans quels cas est-ce adapté
Cette méthode est adaptée par exemple aux situations suivantes.
- Vous voulez extraire une seule image fixe d’un MP4
- Vous voulez créer quelques miniatures
- Vous voulez obtenir une image RGB à passer à WIC
- Un usage batch ou outillage suffit, sans besoin de lecture en temps réel
Le Source Reader dispose d’une fonctionnalité qui, via MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, effectue un video processing limited de YUV vers RGB32.
Cependant, comme l’indique aussi Microsoft Learn, il s’agit d’un traitement logiciel (software), non optimisé pour la playback. Si vous voulez traiter des centaines de frames par seconde, s’appuyer sur ce mécanisme n’est pas vraiment adapté.
flowchart TB
accTitle: Ce à quoi la conversion automatique est adaptée ou non
accDescr: Schéma montrant que la conversion automatique du Source Reader est un traitement logiciel non optimisé pour la playback : elle convient aux usages batch comme l'extraction d'images fixes et les miniatures, mais on ne peut pas s'appuyer dessus pour traiter des centaines de frames par seconde.
auto1["Conversion automatique du Source Reader"] --> sw1["Traitement logiciel (software)"]
sw1 -->|"adapté"| bat1["Images fixes, miniatures, batch"]
sw1 -.->|"ne pas s'appuyer dessus"| rt1["Temps réel de centaines de frames par seconde"]
Figure 8 : la conversion automatique étant un traitement logiciel, réservez-la aux outils qui ne traitent que quelques frames.
4.2. Que faut-il configurer pour obtenir du RGB32
Le déroulement est assez direct.
- Dans les attributes passés à
MFCreateSourceReaderFromURL, définirMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - Sélectionner le stream video
- Demander
MFMediaType_Video/MFVideoFormat_RGB32viaSetCurrentMediaType - Lire un sample avec
ReadSample
Cela suffit : le video processing limited inséré après le decoder se charge de la conversion YUV -> RGB32 pour vous.
flowchart TB
accTitle: Les quatre étapes pour activer la conversion automatique
accDescr: Schéma des quatre étapes de la conversion automatique : activer le video processing dans les attributes pour créer le Reader, sélectionner le stream video, demander RGB32, puis lire avec ReadSample.
a1["Activer le video processing dans les attributes"] --> a2["Sélectionner le stream video"]
a2 --> a3["Demander RGB32"]
a3 --> a4["Lire avec ReadSample"]
a4 -.-> a5["La conversion en RGB32 a lieu après le decoder"]
Figure 9 : ces quatre étapes suffisent pour que le video processing inséré après le decoder amène les frames jusqu’au RGB32.
4.3. Code
Le code suivant suppose que CoInitializeEx et MFStartup ont déjà été appelés. Dans sa forme minimale, cela ressemble à peu près à ceci.
#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;
}
Ensuite, en appelant GetCurrentMediaType, vous pouvez vérifier la size et le stride réels de la sortie.
4.4. Les points forts de cette méthode
L’avantage de cette méthode, c’est avant tout d’obtenir rapidement une image correcte.
- Vous n’avez pas à écrire vous-même l’expansion en 4:2:0 / 4:2:2
- Elle masque une bonne partie de la complexité de la matrix / du deinterlace
- La sortie est facile à passer à WIC ou à GDI
- Pour traiter quelques frames, c’est tout à fait suffisant en pratique
Pour les outils d’extraction d’image fixe, il est tout naturel de commencer par cette approche.
4.5. Mais il y a aussi des pièges
Cette conversion automatique présente les caractéristiques suivantes.
| Élément | Détail |
|---|---|
| Format de sortie | En général RGB32 |
| Implémentation | Traitement logiciel (software) |
| Cas d’usage adaptés | Petit nombre de frames, miniatures, traitement hors ligne |
| Cas d’usage non adaptés | Rendu temps réel basé sur D3D, traitement massif de frames |
| Attributs incompatibles | MF_SOURCE_READER_D3D_MANAGER, MF_READWRITE_DISABLE_CONVERTERS |
Autre point important : la gestion du 4ᵉ byte de RGB32.
En mémoire, le RGB32 de Windows est organisé en Blue / Green / Red / Alpha or Don’t Care. Ce n’est pas de l’ARGB32. Si vous le passez à WIC en tant que 32bppBGRA, il est plus sûr de remplir le 4ᵉ byte avec 0xFF pour le rendre opaque.
flowchart TB
accTitle: La gestion du 4e octet de RGB32
accDescr: Schéma montrant que le 4e octet qui suit B, G et R dans le RGB32 de Windows n'est défini ni comme alpha ni comme don't care, et qu'il est donc plus sûr de le remplir avec 0xFF pour le rendre opaque avant de passer les données à WIC en 32bppBGRA.
r32["Disposition mémoire de RGB32"] --> bgr1["Les 3 octets B, G, R"]
r32 --> b41["Le 4ᵉ octet est alpha ou don't care"]
b41 -->|"remplir avec 0xFF"| wic1["Peut être passé à WIC en 32bppBGRA"]
b41 -.->|"passer tel quel"| tr3["Peut devenir transparent"]
Figure 10 : le 4ᵉ octet n’étant pas défini, rendez-le opaque avec 0xFF avant de passer les données à WIC.
Nous avions déjà signalé ce point comme un piège facile à rencontrer dans le précédent article sur l’extraction d’image fixe.
5. Pattern B : écrire soi-même la conversion
5.1. Dans quels cas est-ce adapté
La conversion manuelle est adaptée par exemple aux cas suivants.
- Vous traitez un grand nombre de frames et voulez optimiser vous-même la conversion
- Vous voulez envoyer
NV12tel quel vers le GPU ou du code SIMD - Vous voulez gérer explicitement
BT.601/BT.709/ le range - Vous voulez produire un format de sortie autre que
RGB32 - La conversion automatique limited du Source Reader ne suffit pas
On peut dire que c’est le pattern où l’on assume soi-même la responsabilité du débit et des couleurs, en échange d’une plus grande liberté.
flowchart TB
accTitle: Le compromis de la conversion manuelle
accDescr: Schéma montrant que la conversion manuelle consiste à assumer soi-même le débit et la responsabilité des couleurs en échange de l'optimisation, du raccordement au GPU et au SIMD, du contrôle explicite de la matrix et du range, et de la liberté sur le format de sortie.
own1["Choisir la conversion manuelle"] --> res1["Assumer soi-même le débit et la responsabilité des couleurs"]
res1 --> fr1["Liberté d'optimisation, GPU / SIMD, format de sortie"]
res1 --> fr2["Contrôle explicite de la matrix / du range"]
Figure 11 : la conversion manuelle échange de la responsabilité contre des performances et une liberté sur les couleurs.
5.2. Flux global de la conversion manuelle
Voici les étapes.
- Configurer la sortie du Source Reader en
NV12ouYUY2 - Récupérer le subtype et les attributs réels via
GetCurrentMediaType - Vérifier
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIXetMF_MT_VIDEO_NOMINAL_RANGE - Extraire le buffer du sample et le lock
- Déterminer le Y/U/V que référence chaque pixel
- Appliquer la matrix et écrire le résultat en BGRA
Le code de cet article se limite au cas 8-bit SDR / progressive / NV12 ou YUY2 / limited range.
Restreindre les hypothèses de cette manière n’est pas de la paresse, c’est même important. Une conversion YUV conçue pour « tout accepter pour l’instant » a tendance à casser les couleurs silencieusement.
flowchart TB
accTitle: Le flux global de la conversion manuelle
accDescr: Schéma des étapes de la conversion manuelle : configurer la sortie en NV12 ou YUY2, vérifier le media type et les attributs réels, lock le buffer, déterminer le Y/U/V de chaque pixel, puis appliquer la matrix et écrire en BGRA.
f1["Demander NV12 / YUY2"] --> f2["Vérifier le subtype et les attributs réels"]
f2 --> f3["Lock le buffer"]
f3 --> f4["Déterminer le Y/U/V de chaque pixel"]
f4 --> f5["Appliquer la matrix et écrire en BGRA"]
f2 -.-> nar1["Restreindre les hypothèses protège les couleurs"]
Figure 12 : la conversion manuelle se déroule dans l’ordre demander, vérifier, lock, référencer, convertir — et devient d’autant plus sûre que les hypothèses sont restreintes.
5.3. D’abord, spécifier explicitement le media type de sortie
Commençons par indiquer au Source Reader : « je veux que vous produisiez le YUV tel quel ». Ici aussi, on suppose que CoInitializeEx / MFStartup ont déjà été appelés.
#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;
}
Ici, on passe MFVideoFormat_NV12 ou MFVideoFormat_YUY2 comme subtype.
Attention : le subtype demandé n’est pas garanti d’être accepté tel quel. Il faut vérifier ce qui sort réellement avec GetCurrentMediaType.
flowchart TB
accTitle: Distinguer la demande de la sortie réelle
accDescr: Schéma montrant que le subtype demandé via SetCurrentMediaType n'est pas garanti d'être accepté tel quel, et qu'il faut donc vérifier avec GetCurrentMediaType ce qui sort réellement.
req1["Demander un subtype"] -.->|"pas garanti d'être accepté tel quel"| out2["La sortie réelle"]
out2 --> gct1["Vérifier avec GetCurrentMediaType"]
gct1 --> use1["Écrire la suite du traitement avec les valeurs vérifiées"]
Figure 13 : la demande reste une demande ; vérifiez toujours la sortie réelle avec GetCurrentMediaType avant de l’utiliser.
5.4. Avant de convertir, n’accepter que les informations de couleur prises en charge
Pour une conversion manuelle, on commence par extraire du media type les informations minimales nécessaires.
L’exemple de cet article n’accepte que NV12 / YUY2, et ne laisse passer que BT.601 ou BT.709 pour la matrix, et uniquement MFNominalRange_16_235 pour le range.
#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;
}
Ici, on choisit délibérément d’être strict.
La documentation des enums de Media Foundation mentionne bien des choses comme « traiter Unknown comme BT.709 », mais en pratique, arrondir silencieusement ce point rend les dérives de couleur plus difficiles à détecter. Au moins pour une première implémentation, il est plus sûr de considérer comme une erreur toute combinaison non prise en charge.
Dans quels cas Unknown est-il renvoyé
Vous trouverez peut-être cela trop strict ; voici donc les chemins par lesquels Unknown apparaît. Il s’agit pour l’essentiel de cas où la source video ne porte pas d’information de couleur.
- Le VUI du H.264 / HEVC ne contient pas d’information de couleur. Selon la norme, lorsque
colour_description_present_flagvaut 0,matrix_coefficientsest traité comme « non spécifié ». Si le flux traverse le decoder sans cette information, la matrix transmise en aval reste elle aussi non spécifiée - Du YUV brut provenant d’un périphérique de capture ou d’un ancien conteneur. Ce sont des chemins qui ne portent aucune description d’espace colorimétrique
- Il arrive aussi que l’attribut
MF_MT_YUV_MATRIXlui-même soit absent. Dans ce cas,GetUINT32ne renvoie pas de valeur et échoue avecMF_E_ATTRIBUTENOTFOUND(le code ci-dessus le rejette directement viaFAILED(hr))
Le point important ici est qu’Unknown ne signifie pas « on sait que c’est du BT.709 », mais « on ne sait pas ». Appliquer du 709 à une source en résolution SD décale les couleurs, et l’inverse aussi.
Deux politiques se présentent alors.
- Rejeter strictement (la politique de cet article) : retourner une erreur pour cause de cas non pris en charge et laisser la couche supérieure conclure que « cette source n’est pas prise en charge ». Mieux vaut affirmer que l’on ne sait pas traiter le cas plutôt que de laisser les couleurs dériver en silence
- Fixer une valeur par défaut et laisser passer : s’il faut absolument laisser passer, journalisez l’hypothèse retenue en cas d’
Unknown. Indiquez alors explicitement que « 601 / 709 a été fixé d’après la résolution »
Dans les deux cas, la seule chose à éviter est d’arrondir silencieusement. Une dérive de couleur ne provoque pas de crash : elle passe en production sans que personne ne la remarque.
flowchart TB
accTitle: Les branches lorsque matrix vaut Unknown
accDescr: Schéma montrant qu'Unknown ne signifie pas que l'on sait qu'il s'agit de BT.709 mais que l'on ne sait pas, et que deux politiques s'offrent alors — rejeter strictement avec une erreur, ou laisser passer en journalisant l'hypothèse retenue — la seule chose à éviter étant d'arrondir silencieusement.
unk1["matrix à Unknown = on ne sait pas"] -->|"politique de cet article"| st4["Rejeter strictement avec une erreur"]
unk1 -->|"s'il faut laisser passer"| dflt1["Laisser passer en journalisant l'hypothèse"]
unk1 -.->|"à éviter absolument"| mute1["Arrondir silencieusement"]
Figure 14 : Unknown veut dire « on ne sait pas » ; choisissez entre rejeter et laisser passer avec journalisation, mais n’arrondissez jamais en silence.
Avec les caméras ou les sources dérivées du JPEG, on peut vouloir traiter séparément les chemins en full-range. Ici, nous choisissons de ne pas les mélanger silencieusement, et adoptons la politique de restreindre explicitement les hypothèses acceptées par ce code.
5.5. Lire le buffer en faisant confiance au stride
Ce point est également très important.
MF_MT_DEFAULT_STRIDEest le stride minimum- Le sample buffer réel peut avoir un actual stride incluant du padding
- Si
IMF2DBuffer::Lock2Dest disponible, il faut le privilégier
En reprenant le pattern d’assistance (helper) de la page Uncompressed Video Buffers de Microsoft Learn et en le rendant directement utilisable, on obtient ceci.
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;
};
La définition de surface recommandée pour le YUV est top-left / positive stride, mais pour l’accès réel au buffer, il est plus sûr d’utiliser tel quel le stride (= pitch) renvoyé par l’API. Si vous le figez arbitrairement sur la base de width ici, cela finira par casser silencieusement.
flowchart TB
accTitle: L'ordre de priorité pour le stride
accDescr: Schéma de l'ordre de priorité pour le stride : MF_MT_DEFAULT_STRIDE n'est que le stride minimum, le buffer réel peut avoir un stride incluant du padding, il faut donc privilégier la valeur renvoyée par Lock2D et éviter de figer le stride à partir de width.
p1["L'actual stride renvoyé par Lock2D"] -->|"priorité absolue si disponible"| acc1["Valeur utilisée pour l'accès au buffer"]
p2["MF_MT_DEFAULT_STRIDE (valeur minimale)"] -->|"fallback"| acc1
p3["Valeur figée à partir de width"] -.->|"casse silencieusement"| acc1
Figure 15 : pour se déplacer d’une ligne à l’autre, privilégiez la valeur réelle renvoyée par Lock2D et ne figez jamais le stride à partir de width.
5.6. Transformer la formule de conversion par pixel en code
Ici, nous ne traitons que les versions limited range de BT.601 et de BT.709. La sortie sera en BGRA32, facile à passer à WIC ou à 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;
}
Ce qui est fait ici est simple.
- Soustraire 16 à
Y - Soustraire 128 à
U/V - Multiplier par les coefficients correspondant à la matrix
- Clip le résultat sur 0..255
- Mettre le 4ᵉ byte du BGRA à
255
5.7. Convertir NV12 en BGRA32
NV12 étant en 4:2:0, les 4 pixels d’un block 2x2 partagent le même U/V.
Dans une implémentation minimale, la solution la plus claire consiste à utiliser directement ce chroma partagé (shared chroma) pour les 4 pixels.
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;
// Le début du plan UV se trouve « stride × height » plus loin.
// Attention, ce n'est pas width × height (voir le schéma de 3.2.)
const BYTE* uvPlane =
scanline0 + static_cast<size_t>(actualStride) * info.height;
for (UINT32 y = 0; y < info.height; ++y)
{
// Le passage d'une ligne à l'autre se fait toujours en unités de stride
const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;
// En 4:2:0, 2 lignes verticales partagent une même ligne UV -> y / 2
// Le plan UV utilise lui aussi le même stride que le plan Y
const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);
// La sortie est du BGRA compact sans padding, d'où 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];
// Dans le plan UV, [U, V] alternent.
// 2 pixels horizontaux partagent une paire : (x / 2) donne « le numéro de paire »,
// et comme une paire = 2 octets, * 2 convertit en position d'octet. +0 est U, +1 est 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;
}
Ce code interprète le chroma upsampling de manière quasi nearest-neighbor. Visuellement, le résultat est souvent tout à fait exploitable, mais si vous visez la meilleure qualité d’image possible, une conception qui effectue d’abord l’upconversion 4:2:0 -> 4:2:2 -> 4:4:4, comme décrit dans l’article de Microsoft Learn sur le YUV, est théoriquement plus propre.
flowchart TB
accTitle: Deux conceptions du chroma upsampling
accDescr: Schéma montrant que l'implémentation minimale, qui réutilise directement le chroma partagé pour les 4 pixels, est souvent tout à fait exploitable visuellement, et que viser la meilleure qualité d'image conduit à une conception théoriquement plus propre qui convertit après une upconversion de 4:2:0 vers 4:2:2 puis 4:4:4.
min1["Implémentation minimale : réutiliser le chroma partagé"] --> pr1["Souvent tout à fait exploitable visuellement"]
hq1["Convertir après upconversion"] --> pr2["Théoriquement plus propre, priorité à la qualité"]
hq1 -.-> steps1["Dans l'ordre 4:2:0 → 4:2:2 → 4:4:4"]
Figure 16 : choisir entre l’implémentation minimale qui réutilise le chroma partagé et la conception qui procède par upconversion successives.
5.8. Convertir YUY2 en BGRA32
YUY2 est un format packed en 4:2:2.
Comme il ne fait que partager une paire U/V entre 2 pixels, il est un peu plus facile à lire que 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;
}
Dans YUY2, les bytes sont organisés en Y0 U Y1 V, ce qui rend directement visible la structure « on réutilise le U/V tous les 2 pixels ».
Cela rend le mental model plus facile à construire que pour NV12.
5.9. Le point d’entrée lors de l’appel depuis un sample
Enfin, en extrayant un buffer contigu depuis l’IMFSample et en aiguillant selon le subtype, on obtient quelque chose de facile à utiliser.
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;
}
On obtient ainsi le déroulement suivant :
- créer le reader
- demander
NV12ouYUY2 - construire un
DecodedFrameInfoà partir deGetCurrentMediaType ReadSampleConvertSampleToBgra32
flowchart TB
accTitle: L'aiguillage dans la fonction d'entrée
accDescr: Schéma de la structure de la fonction d'entrée : extraire un buffer contigu du sample, aiguiller vers la conversion NV12 si le subtype est NV12, vers la conversion YUY2 s'il s'agit de YUY2, et retourner une erreur dans les autres cas.
smp1["IMFSample"] --> cont1["Extraire un buffer contigu"]
cont1 -->|"NV12"| cnv1["Vers la conversion NV12"]
cont1 -->|"YUY2"| cnv2["Vers la conversion YUY2"]
cont1 -->|"autre"| err1["Retourner une erreur"]
Figure 17 : après avoir obtenu un buffer contigu à l’entrée, on aiguille vers la conversion propre au subtype et on tombe franchement en erreur pour le reste.
Le code appelant réel ressemble par exemple à ceci.
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 peut être traité comme top-down / 32bpp BGRA
5.10. Où placer « l’écriture de la conversion manuelle »
Le code vu jusqu’ici prend la forme où c’est l’application qui effectue la conversion après le Source Reader. C’est la formule la plus facile à comprendre.
Cependant, si vous voulez insérer la conversion à l’intérieur du pipeline Media Foundation, d’autres conceptions existent.
- Écrire votre propre
MFT - Utiliser le
Video Processor MFT/ XVP - Écrire un shader
NV12-> RGB côté GPU
Aller jusque-là change quelque peu le sujet, c’est pourquoi cet article s’est limité au code côté application. Il est toutefois utile de savoir qu’entre « tout confier à Media Foundation » et « tout faire dans l’application », il existe un point intermédiaire : le Video Processor MFT.
5.11. Vérifier que la conversion est correcte
Les accidents de couleur étant difficiles à voir, on vérifie séparément « ça marche » et « c’est correct ». Cela se fait en deux étapes.
Étape 1 : injecter des valeurs connues et les confronter au calcul à la main
Plutôt que de lancer d’emblée une video, il est plus fiable de passer des Y/U/V connus à ConvertLimitedYuvPixelToBgra. Ni fichier video ni Media Foundation ne sont nécessaires.
Voici, en limited range BT.601, les Y/U/V de quelques couleurs représentatives et les valeurs attendues lorsqu’on les injecte dans la formule de 5.6.
| Couleur | Y | U | V | R attendu | G | B |
|---|---|---|---|---|---|---|
| Noir | 16 | 128 | 128 | 0 | 0 | 0 |
| Blanc | 235 | 128 | 128 | 255 | 255 | 255 |
| Rouge | 81 | 90 | 240 | 254 | 0 | 0 |
| Bleu | 41 | 240 | 110 | 0 | 0 | 255 |
Pour le rouge par exemple, en injectant C = 81 - 16 = 65, D = 90 - 128 = -38 et E = 240 - 128 = 112 dans la formule, on obtient :
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
La sortie étant dans l’ordre BGRA, la suite d’octets est 00 00 FE FF.
Le point important ici est que le rouge vaut 254 et non 255. La raison n’est pas la précision des coefficients : c’est que les Y/U/V d’entrée sont déjà des entiers arrondis.
Si l’on convertit le rouge théorique (255, 0, 0) en limited range BT.601, on obtient Y = 16 + 219 × 0.299 = 81.481, U = 90.203, et V vaut exactement 240. Au moment de l’enregistrer comme échantillon 8 bits, cette partie décimale disparaît et Y devient 81. Les 0.481 perdus se traduisent, à la reconversion, par un manque de 0.481 × 1.164383 ≒ 0.56. 255 − 0.56 = 254.44 — c’est de là que vient le 254.44 ci-dessus. Même avec des coefficients de précision infinie, on resterait à 254.44 : l’arrondi à six décimales n’intervient qu’en dessous de la quatrième décimale et n’apparaît pas dans une sortie 8 bits.
La manière dont on repasse finalement en entier influe elle aussi sur le résultat. Le ClampToByte de 5.6. borne la valeur à [0, 255] puis tronque value + 0.5, autrement dit il arrondit au plus proche. Avec une simple troncature (static_cast<BYTE>(value)), ce rouge reste à 254, mais les valeurs proches d’une frontière — le B = 255.04 du bleu ou le R = 0.38 du rouge — se décalent de 1. Avant de comparer avec une autre implémentation, vérifiez laquelle des deux elle applique.
Autrement dit, si l’on tolère un écart de ±1 à 2, ce n’est pas « parce que la précision des coefficients diffère » : c’est pour deux raisons, « des décimales sont perdues à l’échantillonnage » et « la politique de passage à l’entier varie d’une implémentation à l’autre ». À l’inverse, tout écart que ces deux causes n’expliquent pas est un véritable défaut. Un rouge qui vaut 250, un rouge et un bleu inversés, des zones sombres qui remontent — pour ce genre d’écart, ne suspectez pas la précision des coefficients mais les hypothèses de la conversion (confusion entre BT.601 et BT.709, entre full range et limited range, inversion de U et de V, mauvaise lecture du stride). Si vous réglez cela comme « un problème de précision », vous passerez à côté d’un défaut réparable.
flowchart TB
accTitle: La frontière entre écart tolérable et véritable défaut
accDescr: Schéma de la frontière entre un écart de plus ou moins 1 à 2, qui s'explique par les décimales perdues à l'échantillonnage et par la politique de passage à l'entier, et un écart inexplicable par ces deux causes, qui constitue un véritable défaut et doit faire suspecter les hypothèses de la conversion.
dif1["Observer l'écart avec la valeur attendue"] -->|"écart de ±1 à 2"| exp1["S'explique par l'échantillonnage et le passage à l'entier"]
dif1 -->|"écart inexplicable"| bug1["Véritable défaut"]
bug1 --> prem1["Suspecter les hypothèses matrix, range, U/V, stride"]
Figure 18 : les petits écarts s’expliquent par l’échantillonnage et le passage à l’entier ; ce qui reste inexpliqué doit faire suspecter une hypothèse erronée.
Écrit sous forme de test, ce format suffit.
#include <cstdlib> // std::abs
// Vérifie que l'écart avec la valeur attendue reste dans la tolerance.
// On écrit dans l'attendu la « couleur théorique » (255, 0, 0 pour le rouge). La tolerance
// absorbe les décimales perdues à l'échantillonnage et les différences de politique de
// passage à l'entier. La précision des coefficients n'est pas en cause
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; // l'alpha est toujours opaque
}
// Utilisation (BT.601 limited range)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0); // noir
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // blanc
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0); // rouge (R=254 dans la formule)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255); // bleu
On peut faire la même chose avec BT.709. Comme les coefficients diffèrent, les valeurs de Y/U/V changent aussi : le rouge en BT.709 est par exemple Y=63, U=102, V=240. Faire passer les valeurs de 601 telles quelles dans la branche 709 décale les couleurs, donc écrire des lignes de test distinctes permet d’attraper sur-le-champ une confusion de matrix.
Dans les exemples GitHub, cette conversion d’un pixel est isolée dans un en-tête indépendant du système d’exploitation : ce test s’exécute donc même sans Windows.
Étape 2 : confronter les sorties du Pattern A et du Pattern B
Une fois le pixel unique validé, on passe à la frame entière. À partir du même instant de la même video,
- Pattern A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Pattern B (recevoir
NV12/YUY2et convertir soi-même)
on extrait une image par chemin et on les compare pixel par pixel.
Lecture de l'écart :
prendre |A.R - B.R|, |A.G - B.G|, |A.B - B.B| pour chaque pixel
en sortir le maximum et la proportion de pixels dépassant le seuil
N’attendez pas ici une correspondance exacte. Il y a deux raisons à cela.
- Le video processing du Source Reader effectue peut-être le chroma upsampling autrement qu’en nearest-neighbor. L’implémentation manuelle de 5.7. est une implémentation minimale qui réutilise le chroma partagé pour les 4 pixels : l’écart est donc d’autant plus visible sur les contours
- L’arrondi et la précision intermédiaire ne sont pas traités de la même façon
Ce qu’il faut donc regarder n’est pas « est-ce identique », mais la forme que prend l’écart.
| Écart observé | Ce qu’il faut suspecter |
|---|---|
| Les zones plates correspondent, l’écart n’apparaît qu’aux frontières de couleur | Différence de chroma upsampling. Attendu |
| Décalage uniforme sur toute l’image | Confusion de matrix (601 / 709) ou de range (16..235 / 0..255) |
| Bandes, décalage en biais | Stride figé arbitrairement. Voir 7.2. et 7.5. |
| Rouge et bleu inversés | Confusion entre BGRA et RGBA |
| Image entièrement transparente / toute noire | Le 4ᵉ byte n’a pas été rempli avec 0xFF. Voir 7.1. |
En regardant la « forme » de l’écart, on cerne assez bien ce qu’il faut suspecter. Un décalage uniforme sur toute l’image pointe vers la formule ou l’information de couleur ; un écart localisé, vers les indices ou le stride.
flowchart TB
accTitle: La vérification en deux étapes
accDescr: Schéma de la vérification en deux étapes : confronter d'abord au calcul à la main un seul pixel dont on connaît les Y/U/V, puis, une fois cela validé, confronter les frames produites par le Pattern A et le Pattern B au même instant de la même video et cibler la cause d'après la forme de l'écart.
st5["Étape 1 : confronter un pixel au calcul à la main"] --> st6["Étape 2 : confronter les frames des deux chemins"]
st6 --> shp1["Cibler la cause d'après la forme de l'écart"]
st5 -.-> osf1["Ni video ni Media Foundation nécessaires"]
Figure 19 : une fois la vérification d’un pixel passée, comparer les frames entières permet de cerner la cause de l’écart par sa forme.
6. Lequel choisir ?
En cas d’hésitation, le tableau suivant permet d’y voir beaucoup plus clair.
| Critère | Conversion automatique (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
Conversion manuelle |
|---|---|---|
| Rapidité de mise en œuvre | ◎ | △ |
| Extraction de quelques images fixes | ◎ | ○ |
| Grand volume de frames / temps réel | △ | ◎ |
| Contrôle explicite de la matrix / du range | △ | ◎ |
| Association avec GPU / D3D | △ | ○〜◎ |
Sortie autre que RGB32 |
△ | ◎ |
| Compréhension des principes | ○ | ◎ |
Pour une première implémentation, ce raisonnement simplifie les choses.
- Vous voulez d’abord que ça fonctionne -> conversion automatique
- Vous voulez assumer la responsabilité des couleurs et des performances -> conversion manuelle
En pratique, la séquence « vérifier d’abord l’image correcte avec la conversion automatique, puis la remplacer par le chemin manuel » est également très efficace. Si l’on prend tout en charge dès le départ, il devient difficile de savoir où l’image s’est cassée.
flowchart TB
accTitle: L'ordre de progression efficace en pratique
accDescr: Schéma de la progression pratique consistant à vérifier d'abord l'image correcte avec la conversion automatique avant de la remplacer par le chemin manuel, ce qui rend plus facile de savoir où l'image s'est cassée.
step1["Vérifier d'abord l'image correcte en conversion automatique"] --> step2["Remplacer ensuite par le chemin manuel"]
step2 --> good1["L'endroit de la casse est plus facile à isoler"]
allin1["Tout prendre en charge dès le départ"] -.-> lost1["Difficile de savoir où l'image s'est cassée"]
Figure 20 : en s’assurant d’abord d’une image de référence correcte avant de passer à l’implémentation manuelle, on isole plus facilement l’endroit de la casse.
7. Les pièges les plus fréquents en pratique
7.1. Confondre RGB32 avec du RGBA muni d’un alpha
En mémoire, RGB32 correspond à B, G, R, Alpha or Don't Care.
Si vous l’enregistrez tel quel en PNG en tant que BGRA, il arrive que le 4ᵉ byte soit à 0, rendant l’image transparente. Il est plus sûr d’y mettre 0xFF avant l’enregistrement.
7.2. Fixer le stride à width * bytesPerPixel
C’est un accident assez fréquent. Le sample buffer réel peut contenir du padding, donc le principe est d’utiliser l’actual stride pour se déplacer d’une row à l’autre.
7.3. Confondre MF_MT_DEFAULT_STRIDE et le pitch réel
MF_MT_DEFAULT_STRIDE désigne « le stride minimum lorsque ce format est représenté en mémoire contiguë ».
Pour l’actual pitch du sample buffer, privilégiez la valeur renvoyée par IMF2DBuffer::Lock2D.
(pitch est un autre nom pour stride. Comme indiqué en 3.2., cet article les emploie au même sens.)
7.4. Deviner silencieusement 601 / 709 sans regarder les métadonnées de couleur
Les accidents de couleur sont difficiles à voir. Ils ne provoquent pas non plus de crash. C’est bien ce qui les rend gênants.
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
Regardez au moins ces deux-là. Et l’état d’esprit à adopter est à peu près celui-ci : traiter comme une erreur toute valeur non prise en charge par votre code.
7.5. Découper le plan UV de NV12 avec width * height
Le plane offset est déterminé par le stride et la height réels. Pas par width * height.
Faire cela négligemment provoque des décalages de couleur ou des images corrompues.
flowchart TB
accTitle: Comment déterminer la limite de plan de NV12
accDescr: Schéma montrant que le début du plan UV de NV12 est déterminé par le produit du stride et de la height réels, et que le découper avec le produit de width et de height conduit à des décalages de couleur et à des images corrompues.
head1["Début du buffer (plan Y)"] -->|"avancer de stride × height"| uv2["Début du plan UV"]
wr1["Découper avec width × height"] -.-> brk1["Décalage de couleur, image corrompue"]
Figure 21 : la limite du plan UV se calcule avec stride × height, jamais avec width × height.
7.6. Traiter une video entrelacée en supposant qu’elle est progressive
Le manual sample de cet article suppose une video progressive. Si vous lisez du contenu interlaced tel quel comme un seul field, cela peut produire des artefacts en peigne (comb-like). Si un deinterlace est nécessaire, il est plus judicieux d’envisager le video processing automatique du Source Reader ou le Video Processor MFT.
7.7. Ignorer la qualité du chroma upsampling en 4:2:0
La conversion NV12 de cet article privilégie la clarté et utilise directement le shared chroma pour chaque pixel. Cela suffit selon les usages, mais si la qualité d’image est prioritaire, il vaut mieux pousser jusqu’à l’approche d’upconversion décrite dans la documentation sur les formats YUV recommandés.
8. Résumé
Lorsque vous convertissez du YUV en RGB avec Media Foundation, garder à l’esprit le cadre suivant vous évitera de vous perdre.
- Derrière le decoder, ce qui sort normalement, ce n’est pas du RGB, mais
NV12ouYUY2 - Pour la solution de facilité, demandez
RGB32viaMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING - Pour garder le contrôle, recevez
NV12/YUY2et convertissez vous-même vers BGRA - Sur le chemin manuel, maîtrisez le sampling / le range / la matrix / le stride avant de vous soucier de la formule
- Rester flou sur
BT.601/BT.709,16..235,4:2:0/4:2:2mène à des dérives de couleur ou des images cassées
YUV -> RGB peut sembler un peu rébarbatif au premier abord. Mais une fois que l’image suivante est bien ancrée dans l’esprit,
NV12partage U/V sur un block 2x2YUY2partage U/V sur 2 pixels horizontaux- on applique la matrix à ce U/V et à Y
tout devient beaucoup plus limpide. Ces mystérieuses suites d’octets aux couleurs improbables finissent par ressembler à de vrais pixels porteurs de sens.
flowchart TB
accTitle: L'image à garder en tête
accDescr: Schéma montrant qu'une fois ancrée l'image selon laquelle NV12 partage U/V sur un bloc 2x2, YUY2 sur 2 pixels horizontaux, et que l'on applique la matrix à ce U/V et à Y, les mystérieuses suites d'octets se lisent comme de vrais pixels.
im2["NV12 : U/V partagés sur un bloc 2x2"] --> ap1["Appliquer la matrix à ce U/V et à Y"]
im3["YUY2 : U/V partagés sur 2 pixels horizontaux"] --> ap1
ap1 --> see1["La suite d'octets se lit comme des pixels porteurs de sens"]
Figure 22 : une fois acquise l’image des unités de partage et de la matrix, les suites d’octets YUV se lisent sans peine.
9. Références
Code d’exemple de cet article
Articles KomuraSoft associés
- Qu’est-ce que Media Foundation - pourquoi on finit par voir le visage de COM et des API media de Windows
- Comment extraire une image fixe d’une video MP4 à un instant précis avec Media Foundation - une version en un seul fichier prête à coller dans un .cpp
Microsoft Learn
- Source Reader
- Using the Source Reader to Process Media Data
- MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- IMFSourceReader::SetCurrentMediaType
- Recommended 8-Bit YUV Formats for Video Rendering
- Extended Color Information
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- MF_MT_VIDEO_NOMINAL_RANGE attribute
- MFVideoTransferMatrix enumeration
- Video Processor MFT
- Uncompressed RGB Video Subtypes
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Comment incruster une image et du texte dans les images d'une vidéo MP4 avec Media Foundation
Comment incruster une image et du texte dans chaque image d'une vidéo MP4 avec Media Foundation pour produire un nouveau MP4, en structur...
Extraire une image fixe d'un MP4 à un instant précis avec Media Foundation
Comment récupérer, avec le Source Reader, la frame la plus proche d'un instant donné dans un MP4, corriger le stride et l'octet alpha du ...
Introduction à Media Foundation - Comprendre l'API à travers le prisme COM
Nous expliquons ce qu'est Media Foundation, ainsi que le vocabulaire de base des API multimédias Windows - COM, HRESULT, IMFSourceReader,...
Time Travel Debugging — Enregistrer et rembobiner les bogues qui ne se reproduisent pas dans les applications de longue durée
Un bogue qui n'apparaît qu'une fois par mois ne laisse dans un dump de plantage que le résultat. Enregistrez et rembobinez l'exécution av...
Pourquoi les arguments se cassent — Les règles des arguments de ligne de commande Windows
Windows passe à CreateProcess une seule chaîne que le destinataire découpe. Traite les règles de CommandLineToArgvW, du CRT et de .NET, A...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Ce sujet couvre Media Foundation, le Source Reader, l'enregistrement d'images et la conversion de trames vidéo — un thème d'implémentation de traitement multimédia Windows qui s'accorde bien avec notre service de développement d'applications Windows.
Conseil technique et revue de conception
Si vous souhaitez d'abord clarifier la répartition des responsabilités pour la conversion YUV / RGB, les espaces colorimétriques, le stride et la conception du chemin de conversion, ce sujet se prête bien à une mission de conseil technique / revue de conception.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Pourquoi le décodeur de Media Foundation produit-il du YUV plutôt que du RGB ?
- Parce que l'œil humain est plus sensible aux détails de luminosité qu'aux détails de couleur, une conception qui conserve Y (la composante liée à la luminosité) avec une grande précision et U/V (les composantes de différence de couleur) avec une précision plus grossière est efficace pour la video. C'est pourquoi, dans l'écosystème video de Windows, les trames non compressées issues du décodeur sont généralement dans un format de la famille YUV, comme NV12 ou YUY2. Dans le contexte de la video numérique, il est plus simple de considérer que YUV désigne en réalité Y'CbCr.
- Quel est le moyen le plus simple d'obtenir des trames RGB ?
- Il s'agit d'activer MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING sur IMFSourceReader et de demander MFVideoFormat_RGB32. C'est la solution la plus pratique pour extraire quelques images fixes ou générer des miniatures. Cependant, cette conversion automatique est un traitement logiciel qui n'est pas optimisé pour la lecture en temps réel ; si vous avez besoin d'un traitement à grande échelle ou d'un contrôle des couleurs, il vaut mieux recevoir les données en YUV et effectuer la conversion vous-même.
- Quels sont les points de vigilance lors d'une conversion YUV vers RGB écrite soi-même ?
- Ce n'est pas terminé une fois que l'on a multiplié par trois coefficients : le sous-échantillonnage (4:2:0 / 4:2:2), le range, la matrice et le stride entrent tous en jeu. En pratique, ce qui casse le plus souvent les couleurs, c'est de ne pas regarder MF_MT_YUV_MATRIX et MF_MT_VIDEO_NOMINAL_RANGE, et de supposer que le stride vaut width × bytesPerPixel. Le plus rapide est de commencer par bien comprendre la structure de NV12 et de YUY2.
- Quelle est la différence entre NV12 et YUY2 ?
- NV12 est un format 4:2:0 : le plan Y est suivi d'un plan UV où U et V alternent, et les 4 pixels d'un bloc 2x2 partagent une paire U/V. YUY2 est un format 4:2:2, dans lequel 2 pixels horizontaux partagent une paire U/V. Ce sont deux formats très courants en pratique, qui diffèrent par leur façon de sous-échantillonner la couleur.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.