Comment convertir du YUV en RGB avec Media Foundation

· Mis à jour le: · · 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 IMFSourceReader amener automatiquement les frames jusqu’à RGB32
  • Pattern B : recevoir NV12 / YUY2 et 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.lib
  • mfreadwrite.lib
  • mfuuid.lib
  • ole32.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_PROCESSING et de demander MFVideoFormat_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 NV12 et YUY2 en 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_MATRIX et MF_MT_VIDEO_NOMINAL_RANGE, et de supposer que le stride vaut width * 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.

Le choix binaire de cet articleSché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.solution de facilitétraitement massif / contrôle des couleursIl faut des frames RGBPattern A : laisser le Source Reader produire du RGB32Pattern 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.

Pattern APattern BMP4 / H.264 / HEVCdecoderTrames YUV telles que NV12 / YUY2 / YV12Video processing du Source ReaderRGB32Votre propre code de conversionBGRA / 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.

  1. Laisser Media Foundation amener les frames jusqu’à RGB32
  2. 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 :

  • Y est la composante liée à la luminosité
  • U / V sont les composantes de différence de couleur
  • RGB correspond 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.

Pourquoi les formats de la famille YUV sont utilisésSché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.L'œil humain est sensible à la luminositéGarder Y finement et U/V grossièrementRaison 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.

Comparaison des unités de partage de NV12 et YUY2Sché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é.NV12 (4:2:0)4 pixels en 2x2 partagent une paire U/VYUY2 (4:2:2)2 pixels horizontaux partagent une paire U/VDécider comment les assigner à chaque pixel

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.

  1. 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
  2. Annuler le range Le Y de la video utilise normalement 16..235, et U/V 16..240, donc on annule cette mise à l’échelle
  3. Appliquer la matrix Convertir vers RGB à l’aide de coefficients tels que BT.601 ou BT.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
Les trois couches à retenir pour du code pratiqueSché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.Frame YUVAnnuler le sous-échantillonnageAnnuler le range (16..235, etc.)Appliquer la matrix (601 / 709)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_MATRIX
  • MF_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.

Le cheminement pour ne pas deviner l'espace colorimétriqueSché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.Deviner silencieusement d'après la résolutionUne dérive de couleur ne provoque pas de crashElle passe en production sans être remarquéeRegarder les attributs matrix et rangeNe laisser passer que les combinaisons prises en chargeLes 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.

La structure de la formule de conversionSché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.Soustraire 16 à Y (niveau de noir)Multiplier par les coefficients de la matrixSoustraire 128 à U / V (centre)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é.

Ce à quoi la conversion automatique est adaptée ou nonSché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.adapténe pas s'appuyer dessusConversion automatique du Source ReaderTraitement logiciel (software)Images fixes, miniatures, batchTemps 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.

  1. Dans les attributes passés à MFCreateSourceReaderFromURL, définir MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. Sélectionner le stream video
  3. Demander MFMediaType_Video / MFVideoFormat_RGB32 via SetCurrentMediaType
  4. 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.

Les quatre étapes pour activer la conversion automatiqueSché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.Activer le video processing dans les attributesSélectionner le stream videoDemander RGB32Lire avec ReadSampleLa 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,
        &timestamp,
        ppSample);

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

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

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.

La gestion du 4e octet de RGB32Sché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.remplir avec 0xFFpasser tel quelDisposition mémoire de RGB32Les 3 octets B, G, RLe 4ᵉ octet est alpha ou don't carePeut être passé à WIC en 32bppBGRAPeut 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 NV12 tel 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é.

Le compromis de la conversion manuelleSché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.Choisir la conversion manuelleAssumer soi-même le débit et la responsabilité des couleursLiberté d'optimisation, GPU / SIMD, format de sortieContrô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.

  1. Configurer la sortie du Source Reader en NV12 ou YUY2
  2. Récupérer le subtype et les attributs réels via GetCurrentMediaType
  3. Vérifier MF_MT_FRAME_SIZE, MF_MT_DEFAULT_STRIDE, MF_MT_YUV_MATRIX et MF_MT_VIDEO_NOMINAL_RANGE
  4. Extraire le buffer du sample et le lock
  5. Déterminer le Y/U/V que référence chaque pixel
  6. 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.

Le flux global de la conversion manuelleSché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.Demander NV12 / YUY2Vérifier le subtype et les attributs réelsLock le bufferDéterminer le Y/U/V de chaque pixelAppliquer la matrix et écrire en BGRARestreindre 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.

Distinguer la demande de la sortie réelleSché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.pas garanti d'être accepté tel quelDemander un subtypeLa sortie réelleVérifier avec GetCurrentMediaTypeÉ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_flag vaut 0, matrix_coefficients est 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_MATRIX lui-même soit absent. Dans ce cas, GetUINT32 ne renvoie pas de valeur et échoue avec MF_E_ATTRIBUTENOTFOUND (le code ci-dessus le rejette directement via FAILED(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.

Les branches lorsque matrix vaut UnknownSché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.politique de cet articles'il faut laisser passerà éviter absolumentmatrix à Unknown = on ne sait pasRejeter strictement avec une erreurLaisser passer en journalisant l'hypothèseArrondir 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_STRIDE est le stride minimum
  • Le sample buffer réel peut avoir un actual stride incluant du padding
  • Si IMF2DBuffer::Lock2D est 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.

L'ordre de priorité pour le strideSché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.priorité absolue si disponiblefallbackcasse silencieusementL'actual stride renvoyé par Lock2DValeur utilisée pour l'accès au bufferMF_MT_DEFAULT_STRIDE (valeur minimale)Valeur figée à partir de width

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.

Deux conceptions du chroma upsamplingSché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.Implémentation minimale : réutiliser le chroma partagéSouvent tout à fait exploitable visuellementConvertir après upconversionThéoriquement plus propre, priorité à la qualité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 NV12 ou YUY2
  • construire un DecodedFrameInfo à partir de GetCurrentMediaType
  • ReadSample
  • ConvertSampleToBgra32
L'aiguillage dans la fonction d'entréeSché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.NV12YUY2autreIMFSampleExtraire un buffer contiguVers la conversion NV12Vers la conversion YUY2Retourner 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,
    &currentType);
if (FAILED(hr)) return hr;

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

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

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

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

// bgra 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.

La frontière entre écart tolérable et véritable défautSché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.écart de ±1 à 2écart inexplicableObserver l'écart avec la valeur attendueS'explique par l'échantillonnage et le passage à l'entierVéritable défautSuspecter 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,

  1. Pattern A (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. Pattern B (recevoir NV12 / YUY2 et 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.

La vérification en deux étapesSché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.Étape 1 : confronter un pixel au calcul à la mainÉtape 2 : confronter les frames des deux cheminsCibler la cause d'après la forme de l'écartNi 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.

L'ordre de progression efficace en pratiqueSché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.Vérifier d'abord l'image correcte en conversion automatiqueRemplacer ensuite par le chemin manuelL'endroit de la casse est plus facile à isolerTout prendre en charge dès le départDifficile 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_MATRIX
  • MF_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.

Comment déterminer la limite de plan de NV12Sché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.avancer de stride × heightDébut du buffer (plan Y)Début du plan UVDécouper avec width × heightDé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 NV12 ou YUY2
  • Pour la solution de facilité, demandez RGB32 via MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
  • Pour garder le contrôle, recevez NV12 / YUY2 et 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:2 mè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,

  • NV12 partage U/V sur un block 2x2
  • YUY2 partage 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.

L'image à garder en têteSché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.NV12 : U/V partagés sur un bloc 2x2Appliquer la matrix à ce U/V et à YYUY2 : U/V partagés sur 2 pixels horizontauxLa 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

Microsoft Learn

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog