Comment convertir du YUV en RGB avec Media Foundation

· Mis à jour le: · · Media Foundation, C++, Développement Windows, Traitement vidéo, YUV

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)

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.

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

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.

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.

NV12 (4:2:0, planar)

Y plane
Y Y Y Y
Y Y Y Y
Y Y Y Y
Y Y Y Y

UV plane
U V U V
U V U V

Dans NV12, les 4 pixels d’un block 2x2 partagent une paire U/V. Y existe pour chaque pixel individuellement.

YUY2 (4:2:2, packed)

bytes:
Y0 U0 Y1 V0   Y2 U2 Y3 V2   ...

Dans YUY2, 2 pixels horizontaux partagent une paire U/V. Y0 et Y1 sont distincts, mais U0 et V0 sont partagés.

À 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.

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

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.

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.

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é.

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.

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.

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é.

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.

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.

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.

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 pitch renvoyé par l’API. Si vous le figez arbitrairement sur la base de width ici, cela finira par casser silencieusement.

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;
    const BYTE* uvPlane =
        scanline0 + static_cast<size_t>(actualStride) * info.height;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);
        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];
            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.

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

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.

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.

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.

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.

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.

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