Cómo convertir YUV a RGB en Media Foundation

· Actualizado el: · · Media Foundation, C++, Desarrollo en Windows, Procesamiento de video, YUV

Cuando quiere extraer un fotograma de un video y guardarlo como PNG, pasarlo a WIC o GDI, o mostrarlo en la interfaz de usuario, la aplicación necesita una secuencia de píxeles en RGB.

Sin embargo, los fotogramas que entrega el decoder de Media Foundation suelen estar, con bastante normalidad, en un formato de la familia YUV, como NV12 o YUY2. Si en este punto trata la secuencia de bytes cruda como si ya fuera una imagen, el resultado es un color roto, franjas, un tono verdoso extraño… una imagen un poco triste.

En el artículo anterior Media Foundation desde cero: entender la API desde el punto de vista de COM revisamos la visión general, y en Cómo extraer una imagen fija de un MP4 en un instante determinado con Media Foundation organizamos la extracción de imágenes fijas. Esta vez tratamos lo que ocurre en medio de ese proceso: la propia conversión de YUV a RGB.

En este artículo separamos y organizamos estos 2 patrones.

  • Patrón A: dejar que IMFSourceReader haga todo el camino automáticamente hasta RGB32
  • Patrón B: recibir NV12 / YUY2 y convertir a RGB usted mismo

El objetivo no es memorizar nombres de API. Es poder representarse mentalmente dónde aparece el YUV dentro de Media Foundation y en qué punto se convierte a RGB.

El código que aparece en este artículo se publica además como un conjunto de muestras completo (código C++ de los patrones A y B, configuración de CMake, pruebas de conversión de píxeles) en GitHub.

media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)

Requisitos previos para ejecutar el código

Si va a llevar el código de este artículo a su propio proyecto, esto es todo lo que necesita.

Elemento Requisito
Sistema operativo Windows 10 o posterior
Compilador MSVC de Visual Studio 2019 / 2022 (C++17)
SDK Windows SDK (encabezados y bibliotecas de importación de Media Foundation). Se incluye en la carga de trabajo “Desarrollo para escritorio con C++” de Visual Studio
Compilación Las muestras usan CMake 3.20 o posterior. También puede crear el proyecto de Visual Studio a mano

Hay 4 bibliotecas que enlazar. En el código del artículo se declaran con #pragma comment(lib, ...), pero especificarlas en la configuración del proyecto tiene el mismo efecto.

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

Tenga en cuenta que el código de este artículo asume que CoInitializeEx y MFStartup ya se han ejecutado. Solo la fórmula de conversión de 1 píxel (apartado 5.6.) es independiente del sistema operativo; por eso, en la muestra de GitHub está separada en un encabezado aparte, de modo que también se puede probar con g++ en Linux.

1. Conclusión inicial

Si adelantamos solo la conclusión, es esta.

  • Para extraer unas pocas imágenes fijas o generar miniaturas, lo más cómodo es habilitar MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING y solicitar MFVideoFormat_RGB32
  • Sin embargo, esta conversión automática se ejecuta por software y no está optimizada para reproducción en tiempo real
  • Si va a escribir su propia conversión, lo más rápido es entender bien NV12 y YUY2
  • YUV -> RGB no se resuelve solo “aplicando tres coeficientes”: en la práctica intervienen el submuestreo, el rango, la matriz y el stride
  • La documentación de Media Foundation usa ampliamente la palabra YUV, pero en video digital conviene leerla, en la práctica, como Y’CbCr
  • En la práctica, lo que más suele romper el color es no revisar MF_MT_YUV_MATRIX ni MF_MT_VIDEO_NOMINAL_RANGE, y asumir que el stride es width * bytesPerPixel

En resumen, la disyuntiva es esta: si quiere ahorrarse trabajo, deje que el Source Reader entregue RGB32. Si necesita procesar grandes volúmenes o controlar el color, reciba el YUV tal cual y conviértalo usted mismo. Esta es la elección.

2. Primero, véalo en un diagrama

Antes que nada, conviene ver en un diagrama qué ocurre dentro de Media Foundation; así se entiende más rápido.

Flujo de conversión de YUV a RGB en Media FoundationDiagrama que muestra cómo un archivo MP4/H.264/HEVC pasa por un decoder que produce fotogramas YUV (NV12/YUY2/YV12), y cómo estos se convierten a RGB mediante el patrón A (video processing del Source Reader) o el patrón B (código de conversión propio)Patrón APatrón BMP4 / H.264 / HEVCdecoderFotograma YUV: NV12 / YUY2 / YV12, etc.Video processing del Source ReaderRGB32Código de conversión propioBGRA / RGB

Si el contenido del archivo de video está en un formato comprimido como H.264 o HEVC, primero el decoder lo devuelve a un fotograma sin comprimir. Ese fotograma sin comprimir no tiene por qué ser RGB; de hecho, en el mundo de video de Windows lo normal es que sea de la familia YUV.

Por eso, cuando la aplicación necesita RGB, hay que elegir entre estas dos opciones.

  1. Hacer que el propio Media Foundation llegue hasta RGB32
  2. Recibir el YUV y convertirlo a RGB en su propio código

El tema de este artículo es exactamente ese punto de bifurcación.

3. Primero, entendamos la relación entre YUV y RGB

3.1. Aunque se diga YUV, en realidad se trata de Y’CbCr

Los nombres de las API y la documentación de Windows usan ampliamente la palabra YUV. Sin embargo, en el contexto del video digital, no hay mayor problema en leer U como Cb y V como Cr.

A grandes rasgos:

  • Y es el componente cercano al brillo
  • U / V son los componentes de crominancia (diferencia de color)
  • En RGB, cada píxel guarda directamente Red / Green / Blue

Esa es la relación.

El ojo humano es más sensible a la resolución del brillo que a la del color. Por eso, en video conviene un diseño que conserve Y con alta resolución y U/V con una resolución algo más baja. Esta es la razón por la que se usan tanto los formatos de la familia YUV.

3.2. 4:4:4 / 4:2:2 / 4:2:0: cuánto se diezma el color

Este es el punto clave para entender YUV.

Notación Significado Ejemplos representativos
4:4:4 Cada píxel tiene su propio Y/U/V AYUV, I444
4:2:2 2 píxeles en horizontal comparten U/V YUY2, UYVY, I422
4:2:0 Un bloque de 2x2 píxeles comparte U/V NV12, YV12, I420

Conviene ver primero la forma de los dos formatos que más aparecen en la práctica; eso facilita mucho las cosas.

Antes de seguir, fijemos un término. El stride (también llamado pitch) es la cantidad de bytes de una fila. No es el ancho de la imagen en sí, sino cuántos bytes hay que avanzar hasta el inicio de la siguiente fila, incluyendo el padding al final de la fila. En este artículo usamos stride y pitch como sinónimos. Microsoft Learn también usa ambos términos, así que no hace falta distinguirlos.

En los siguientes diagramas, W es el width, H el height y S el stride. Lo importante es que S >= W, pero no necesariamente S == W.

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

  <----------- S bytes ------------>
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (padding)           |    |
 | Y Y Y Y Y | (padding)           |    | Y plane
 | Y Y Y Y Y | (padding)           |    | S * H bytes
 | Y Y Y Y Y | (padding)           |    |
 +-----------+---------------------+  --+  <- límite de plane = S * H desde el inicio
 | U V U V U | (padding)           |    |
 | U V U V U | (padding)           |    | UV plane
 +-----------+---------------------+  --+  altura: H / 2 filas

  Y de la fila y    : yPlane  + S * y
  UV de la fila y   : uvPlane + S * (y / 2)
  Inicio de UV plane: scanline0 + S * H

En NV12, un bloque de 2x2 (4 píxeles) comparte un par de U/V. Y existe para cada píxel. El plane UV usa el mismo stride que el plane Y, pero tiene la mitad de filas. Por eso el límite entre planes es S * H, no W * H (lo retomamos en el apartado 7.5.).

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

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

  Inicio de la fila y : scanline0 + S * y
  2 píxeles = 4 bytes (Y, U, Y, V)
  Solo hay 1 plane (al ser packed, no hay límite entre planes)

En YUY2, 2 píxeles en horizontal comparten un par de U/V. Y0 y Y1 son distintos, pero U0 y V0 se comparten. Al ser packed no hace falta calcular el límite entre planes, pero para moverse entre filas también hay que usar el stride.

En este punto ya se ve que YUV -> RGB no es una simple sustitución píxel a píxel. Primero hay que decidir a qué píxel, y cómo, se asigna el U/V compartido.

3.3. YUV -> RGB es “conversión de espacio de color + conversión de muestreo”

Si consulta Extended Color Information de Media Foundation, verá que la conversión de color estricta tiene bastantes etapas: inverse quantization, chroma upsampling, YUV -> RGB, transfer function, conversión de primaries y quantization.

Sin embargo, para un primer código práctico en 8 bits SDR, es más fácil entenderlo dividiéndolo en estas 3 capas.

  1. Revertir el submuestreo Expandir el U/V de 4:2:0 o 4:2:2 a una forma en la que cada píxel pueda referenciarlo
  2. Revertir el rango En video, Y suele usar 16..235 y U/V 16..240, así que hay que revertir ese escalado
  3. Aplicar la matriz Convertir a RGB con coeficientes como BT.601 o BT.709

Es decir, en la práctica, la conversión YUV -> RGB es el proceso que decide:

  • qué U/V corresponde al color de ese píxel
  • con qué coeficientes se revierte ese Y/U/V a RGB

3.4. Tratar BT.601 y BT.709 sin cuidado descoloca el color poco a poco

La documentación de Media Foundation explica la relación así: BT.601 se usa para SDTV y resoluciones inferiores, mientras que BT.709 se prioriza para video que supera el SD.

Sin embargo, no conviene suponer en silencio que “como la resolución es grande, será 709”. Un desajuste de color no provoca un fallo, así que es fácil que pase a producción sin que nadie se dé cuenta.

Media Foundation puede llevar la información del espacio de color como atributos del media type. Como mínimo, hay que revisar estos dos:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Revisar estos dos y dejar pasar de forma explícita solo las combinaciones que su código soporta reduce el riesgo de un accidente silencioso más adelante.

3.5. La fórmula que hay que memorizar primero: la versión de rango limitado de BT.601

La fórmula representativa de BT.601 en 8 bits es esta.

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)

En BT.709 cambian los coeficientes; también aparecerán más adelante en el código.

Lo importante aquí no es memorizar los coeficientes, sino la estructura: a Y se le resta el nivel de negro 16, y U/V se miran centrados en 128.

4. Patrón A: dejar que Media Foundation convierta automáticamente

4.1. Cuándo conviene usarlo

Este método conviene, por ejemplo, en situaciones como estas.

  • Quiere extraer una sola imagen fija de un MP4
  • Quiere generar unas pocas miniaturas
  • Quiere convertir a una imagen RGB y pasarla a WIC
  • No necesita reproducción en tiempo real; le basta con un uso de tipo batch o herramienta

Source Reader tiene la capacidad de realizar un video processing limitado de YUV -> RGB32 usando MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING.

Sin embargo, tal como indica Microsoft Learn, se trata de un procesamiento por software y no está optimizado para reproducción. Si necesita procesar cientos de fotogramas por segundo, apoyarse en esto no es lo más adecuado.

4.2. Qué configurar para obtener RGB32

El flujo es bastante directo.

  1. Establecer MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE en los attributes que se pasan a MFCreateSourceReaderFromURL
  2. Seleccionar el stream de video
  3. Solicitar MFMediaType_Video / MFVideoFormat_RGB32 con SetCurrentMediaType
  4. Leer el sample con ReadSample

Con esto basta: el video processing limitado que se coloca detrás del decoder se encarga de la conversión YUV -> RGB32.

4.3. Código

El siguiente código asume que ya se ejecutaron CoInitializeEx y MFStartup. En su forma mínima, tiene más o menos este aspecto.

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

Después de esto, si llama a GetCurrentMediaType puede comprobar el size y el stride reales de la salida.

4.4. Puntos fuertes de este método

La ventaja de este método es que, sencillamente, permite llegar rápido a una imagen correcta.

  • No hace falta escribir usted mismo la expansión de 4:2:0 / 4:2:2
  • Oculta buena parte de la complicación de matrix / deinterlace
  • Es fácil de pasar a WIC o GDI
  • Para procesar unos pocos fotogramas, es perfectamente práctico

Para herramientas de extracción de imágenes fijas, empezar por aquí es bastante natural.

4.5. Pero también tiene trampas

Esta conversión automática tiene las siguientes características.

Elemento Contenido
Destino de la conversión Por lo general, RGB32
Implementación Procesamiento por software
Usos apropiados Pocos fotogramas, miniaturas, procesamiento offline
Usos no apropiados Rendering en tiempo real basado en D3D, procesamiento de grandes volúmenes de fotogramas
Atributos que combinan mal MF_SOURCE_READER_D3D_MANAGER, MF_READWRITE_DISABLE_CONVERTERS

Y hay otro punto importante: cómo tratar el 4.º byte de RGB32. En Windows, RGB32 está en memoria en el orden Blue / Green / Red / Alpha or Don’t Care. No es ARGB32. Si lo pasa a WIC como 32bppBGRA, es más seguro rellenar el 4.º byte con 0xFF para hacerlo opaco.

Este es un punto en el que también es fácil tropezar, como se mencionó en el artículo anterior sobre extracción de imágenes fijas.

5. Patrón B: escribir la conversión usted mismo

5.1. Cuándo conviene usarlo

La conversión manual conviene, por ejemplo, en casos como estos.

  • Procesa un gran volumen de fotogramas y quiere optimizar la conversión usted mismo
  • Quiere enviar el NV12 tal cual a la GPU o a SIMD
  • Quiere manejar explícitamente BT.601 / BT.709 / el rango
  • Quiere generar un formato de salida distinto de RGB32
  • La conversión automática limitada del Source Reader no es suficiente

Se puede decir que es el patrón en el que, a cambio de asumir usted mismo la responsabilidad del volumen de procesamiento y del color, gana libertad.

5.2. Flujo completo de la conversión manual

Los pasos son los siguientes.

  1. Configurar la salida del Source Reader como NV12 o YUY2
  2. Obtener el subtype real y los atributos con GetCurrentMediaType
  3. Comprobar MF_MT_FRAME_SIZE, MF_MT_DEFAULT_STRIDE, MF_MT_YUV_MATRIX y MF_MT_VIDEO_NOMINAL_RANGE
  4. Extraer el buffer del sample y bloquearlo (lock)
  5. Calcular el Y/U/V que referencia cada píxel
  6. Aplicar la matriz y escribir en BGRA

El código de este artículo se limita a 8 bits SDR / progressive / NV12 o YUY2 / rango limitado. Acotar las premisas aquí no es un atajo perezoso, sino algo importante. Si la conversión YUV se implementa para “aceptar todo, por si acaso”, es fácil que rompa el color en silencio.

5.3. Primero, declare explícitamente el media type de salida

Primero, le decimos al Source Reader “quiero que entregues el YUV tal cual”. Aquí también asumimos que CoInitializeEx / MFStartup ya se ejecutaron.

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

Aquí, en subtype se pasa MFVideoFormat_NV12 o MFVideoFormat_YUY2.

Hay que tener cuidado: no está garantizado que el subtype solicitado se acepte tal cual. Lo que realmente se obtiene se comprueba con GetCurrentMediaType.

5.4. Antes de convertir, acepte solo la información de color soportada

En la conversión manual, primero se obtiene del media type la información mínima necesaria. La muestra de este artículo solo acepta NV12 / YUY2, y además solo deja pasar matrix BT.601 o BT.709, y rango MFNominalRange_16_235.

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

Aquí somos estrictos a propósito. La documentación del enum de Media Foundation menciona cosas como “tratar Unknown como BT.709”, pero en la práctica, si se redondea esto en silencio, un desajuste de color se vuelve difícil de detectar. Al menos en la primera implementación, es más seguro convertir en error las combinaciones no soportadas.

¿Cuándo se devuelve Unknown?

Puede que le parezca demasiado estricto, así que aquí van los caminos por los que puede aparecer Unknown. En general, son casos en los que “el video original no lleva información de color”.

  • El VUI de H.264 / HEVC no incluye información de color. Según la norma, cuando colour_description_present_flag es 0, matrix_coefficients se trata como “no especificado”. Si esta información falta al pasar por el decoder, la matrix que llega a las etapas posteriores también queda sin especificar
  • YUV crudo procedente de un dispositivo de captura o de un contenedor antiguo. Son rutas que no llevan una descripción del espacio de color
  • A veces ni siquiera está presente el atributo MF_MT_YUV_MATRIX. En ese caso, GetUINT32 no devuelve ningún valor y falla con MF_E_ATTRIBUTENOTFOUND (en el código anterior, esto se rechaza directamente con FAILED(hr))

Lo importante aquí es que Unknown no significa “se sabe que es BT.709”, sino “no se sabe”. Si aplica 709 a un material de resolución SD el color se desajusta, y lo mismo ocurre a la inversa.

A partir de aquí, la política se divide en dos.

  • Rechazar estrictamente (la política de este artículo): devolver un error como “no soportado” y dejar que el nivel superior decida que “este material no se admite”. Es más seguro declarar abiertamente que no se puede procesar que dejar que el color se desajuste en silencio
  • Fijar un valor por defecto y dejarlo pasar: si de verdad necesita aceptarlo, registre en el log qué supuesto se aplicó cuando era Unknown, y deje explícito que “se decidió entre 601/709 según la resolución”

En cualquier caso, lo que hay que evitar es redondear en silencio. Un desajuste de color no provoca un fallo, así que puede llegar a producción sin que nadie lo note.

En cámaras o en el mundo JPEG, a veces conviene tratar por separado las rutas que llevan full-range. Aquí, en lugar de mezclarlas en silencio, la política es acotar de forma explícita las premisas que acepta este código.

5.5. Lea el buffer confiando en el stride

Esto también es bastante importante.

  • MF_MT_DEFAULT_STRIDE es el stride mínimo
  • El sample buffer real puede tener un actual stride que incluye padding
  • Si se puede usar IMF2DBuffer::Lock2D, dele prioridad

Si adaptamos para que sea más fácil de usar el patrón helper de Uncompressed Video Buffers de Microsoft Learn, queda así.

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 definición recomendada de surface para YUV usa top-left / stride positivo, pero para el acceso real al buffer es más seguro usar tal cual el stride (= pitch) que devuelve la API. Si aquí se fija en función de width, más adelante se rompe en silencio.

5.6. Convierta la fórmula de 1 píxel en código

Aquí solo tratamos el rango limitado de BT.601 y BT.709. La salida es BGRA32, fácil de pasar a WIC o GDI.

inline BYTE ClampToByte(double value)
{
    if (value <= 0.0) return 0;
    if (value >= 255.0) return 255;
    return static_cast<BYTE>(value + 0.5);
}

HRESULT ConvertLimitedYuvPixelToBgra(
    BYTE y,
    BYTE u,
    BYTE v,
    MFVideoTransferMatrix matrix,
    BYTE* dstPixel)
{
    if (!dstPixel) return E_POINTER;

    const double c = static_cast<double>(y) - 16.0;
    const double d = static_cast<double>(u) - 128.0;
    const double e = static_cast<double>(v) - 128.0;

    double r = 0.0;
    double g = 0.0;
    double b = 0.0;

    switch (matrix)
    {
    case MFVideoTransferMatrix_BT601:
        r = 1.164383 * c + 1.596027 * e;
        g = 1.164383 * c - 0.391762 * d - 0.812968 * e;
        b = 1.164383 * c + 2.017232 * d;
        break;

    case MFVideoTransferMatrix_BT709:
        r = 1.164383 * c + 1.792741 * e;
        g = 1.164383 * c - 0.213249 * d - 0.532909 * e;
        b = 1.164383 * c + 2.112402 * d;
        break;

    default:
        return MF_E_INVALIDMEDIATYPE;
    }

    dstPixel[0] = ClampToByte(b);
    dstPixel[1] = ClampToByte(g);
    dstPixel[2] = ClampToByte(r);
    dstPixel[3] = 255;

    return S_OK;
}

Lo que se hace aquí es sencillo.

  • Restar 16 a Y
  • Restar 128 a U / V
  • Aplicar los coeficientes de cada matrix
  • Recortar (clip) el resultado a 0..255
  • Poner el 4.º byte de BGRA en 255

5.7. Convertir NV12 a BGRA32

NV12 es 4:2:0, así que un bloque de 2x2 (4 píxeles) comparte el mismo U/V. Como implementación mínima, lo más claro es usar directamente ese chroma compartido en los 4 píxeles.

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;

    // El inicio del UV plane está a "stride × height" bytes desde el comienzo.
    // Ojo: no es width × height (véase el diagrama del apartado 3.2.)
    const BYTE* uvPlane =
        scanline0 + static_cast<size_t>(actualStride) * info.height;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        // El desplazamiento entre filas siempre se hace en unidades de stride
        const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;

        // Al ser 4:2:0, el UV comparte 1 fila cada 2 filas verticales -> y / 2
        // El UV plane usa el mismo stride que el Y plane
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);

        // La salida es BGRA compacto, sin padding, así que 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];

            // En el UV plane, [U, V] se alternan.
            // Como cada 2 píxeles en horizontal comparten un par, primero se obtiene
            // con (x / 2) "qué par es", y como 1 par = 2 bytes, se multiplica por 2
            // para obtener la posición en bytes. +0 es U, +1 es 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;
}

Este código interpreta el chroma upsampling de forma similar a nearest-neighbor. Visualmente suele ser suficientemente práctico, pero si busca la máxima calidad de imagen, un diseño que primero realice el upconversion 4:2:0 -> 4:2:2 -> 4:4:4, como se describe en el artículo de YUV de Microsoft Learn, es teóricamente más limpio.

5.8. Convertir YUY2 a BGRA32

YUY2 es un 4:2:2 packed. Como solo comparte un par de U/V cada 2 píxeles, es un poco más fácil de leer 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;
}

Los bytes de YUY2 se ordenan como Y0 U Y1 V, así que se ve directamente la estructura de “reutilizar U/V cada 2 píxeles”. Por eso, es más fácil construir un modelo mental que con NV12.

5.9. Punto de entrada al llamar desde un sample

Por último, si extrae el buffer contiguo de IMFSample y bifurca según el subtype, resulta más fácil de usar.

HRESULT ConvertSampleToBgra32(
    IMFSample* sample,
    const DecodedFrameInfo& info,
    std::vector<BYTE>& dstBgra)
{
    if (!sample) return E_POINTER;

    ComPtr<IMFMediaBuffer> buffer;
    HRESULT hr = sample->ConvertToContiguousBuffer(&buffer);
    if (FAILED(hr)) return hr;

    if (info.subtype == MFVideoFormat_NV12)
    {
        return ConvertNv12ToBgra32(buffer.Get(), info, dstBgra);
    }

    if (info.subtype == MFVideoFormat_YUY2)
    {
        return ConvertYuy2ToBgra32(buffer.Get(), info, dstBgra);
    }

    return MF_E_INVALIDMEDIATYPE;
}

Con esto, la parte previa queda así:

  • Crear el reader
  • Solicitar NV12 o YUY2
  • Construir DecodedFrameInfo a partir de GetCurrentMediaType
  • ReadSample
  • ConvertSampleToBgra32

Ese es el flujo que se puede lograr.

El lado de la llamada real queda, por ejemplo, así.

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 se puede tratar como top-down / 32bpp BGRA

5.10. Dónde ubicar la conversión manual dentro del pipeline

El código hasta aquí tiene la forma en la que la aplicación convierte después del Source Reader. Es la forma más fácil de entender.

Sin embargo, si quiere insertarse dentro del propio pipeline de Media Foundation, hay otros diseños posibles.

  • Escribir su propio MFT
  • Usar Video Processor MFT / XVP
  • Escribir un shader NV12 -> RGB en el lado de la GPU

Llegar hasta aquí cambia un poco de tema, así que en este artículo nos limitamos al código del lado de la aplicación. Aun así, conviene saber que entre “dejarlo todo en manos de Media Foundation” y “hacerlo todo en la aplicación” existe un punto intermedio: el Video Processor MFT.

5.11. Cómo verificar que la conversión es correcta

Los accidentes de color son difíciles de ver a simple vista, así que conviene separar “funciona” de “es correcto” y comprobarlos por separado. El orden es el siguiente, en 2 pasos.

Primer paso: comparar con un cálculo manual usando valores conocidos

Es más fiable pasar valores de Y/U/V conocidos a ConvertLimitedYuvPixelToBgra que lanzar un video directamente. No hacen falta ni un archivo de video ni Media Foundation.

En el rango limitado de BT.601, el Y/U/V de colores representativos y el valor esperado al introducirlos en la fórmula del apartado 5.6. son estos.

Color Y U V R esperado G B
Negro 16 128 128 0 0 0
Blanco 235 128 128 255 255 255
Rojo 81 90 240 254 0 0
Azul 41 240 110 0 0 255

Por ejemplo, para el rojo: C = 81 - 16 = 65, D = 90 - 128 = -38, E = 240 - 128 = 112. Al introducirlos en la fórmula:

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

El resultado es este. Como la salida está en orden BGRA, la secuencia de bytes es 00 00 FE FF.

Aquí es importante que el rojo dé 254 y no 255. El motivo no es la precisión de los coeficientes, sino que los valores de entrada Y/U/V ya son enteros redondeados.

Si convierte el rojo teórico (255, 0, 0) al rango limitado de BT.601, obtiene Y = 16 + 219 × 0.299 = 81.481, U = 90.203 y V da exactamente 240. En el momento de guardarlo como muestra de 8 bits, esa fracción desaparece y queda Y = 81. La fracción perdida, 0.481, se convierte al revertir en una merma de 0.481 × 1.164383 ≒ 0.56. 255 − 0.56 = 254.44: de ahí sale el 254.44 anterior. Aunque los coeficientes tuvieran precisión infinita, seguiría dando 254.44; el redondeo a 6 decimales solo afecta a partir de la 4.ª cifra decimal, algo que no se manifiesta en una salida de 8 bits.

Cómo se redondea al entero final también influye en el resultado. El ClampToByte del apartado 5.6. recorta a [0, 255] y luego trunca value + 0.5, es decir, redondea al más cercano. Con un truncamiento simple (static_cast<BYTE>(value)), este rojo sigue dando 254, pero en valores cercanos al límite, como B = 255.04 en el azul o R = 0.38 en el rojo, el resultado se desvía en 1. Antes de comparar con otra implementación, confirme cuál de los dos usa.

Es decir, el motivo para tolerar una diferencia de ±1 o 2 no es “que la precisión de los coeficientes difiera”, sino estos dos: “la fracción se pierde en el muestreo” y “la política de redondeo a entero varía según la implementación”. Dicho de otro modo, una diferencia que no se explica por estos dos motivos es un fallo real. Que el rojo dé 250, que el rojo y el azul se intercambien, que solo las zonas oscuras se aclaren: ante diferencias así, sospeche de las premisas de la conversión (confundir BT.601 con BT.709, confundir full range con limited range, intercambiar U y V, leer mal el stride), no de la precisión de los coeficientes. Si se despacha esto como “un problema de precisión”, se pasan por alto fallos que sí se pueden corregir.

Si lo escribe como prueba, con esta forma basta.

#include <cstdlib>  // std::abs

// Comprueba si la diferencia con el valor esperado está dentro de tolerance.
// En el valor esperado se escribe "el color teórico" (para el rojo, 255, 0, 0).
// La tolerancia absorbe la fracción perdida en el muestreo y la diferencia
// en la política de redondeo. La precisión de los coeficientes no es el motivo
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;  // el alfa siempre debe ser opaco
}

// Uso (BT.601, rango limitado)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0);        // negro
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // blanco
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0);       // rojo (en la fórmula da R=254)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255);      // azul

Con BT.709 se puede hacer lo mismo. Como los coeficientes cambian, también cambian los valores de Y/U/V. Por ejemplo, el rojo en BT.709 es Y=63, U=102, V=240. Si pasa los valores de 601 tal cual por la rama de 709, el color se desajusta, así que si separa las pruebas por filas, puede detectar en el acto una confusión de matrix.

En la muestra de GitHub, solo esta conversión de 1 píxel está separada en un encabezado independiente del sistema operativo, así que esta prueba funciona incluso sin Windows.

Segundo paso: comparar la salida del patrón A con la del patrón B

Una vez que el píxel individual coincide, el siguiente paso es el fotograma completo. A partir del mismo instante del mismo video:

  1. Patrón A (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. Patrón B (recibir NV12 / YUY2 y convertir manualmente)

extraiga una imagen por cada una de las 2 rutas y compárelas píxel a píxel.

Cómo leer la diferencia:
  Tome |A.R - B.R|, |A.G - B.G|, |A.B - B.B| de cada píxel
  Calcule el valor máximo y el porcentaje de píxeles que superan el umbral

No espere una coincidencia exacta. Hay 2 motivos.

  • Es posible que el video processing del Source Reader realice el chroma upsampling con un método distinto de nearest-neighbor. La implementación manual del apartado 5.7. es una versión mínima que usa el chroma compartido tal cual en los 4 píxeles, así que la diferencia se nota más en los bordes
  • El redondeo y la precisión intermedia se manejan de forma distinta

Por eso, lo que hay que observar no es “si coinciden”, sino el patrón de cómo aparece la diferencia.

Diferencia observada Qué sospechar
Las zonas planas coinciden; la diferencia aparece solo en los bordes de color Diferencia en el chroma upsampling. Es lo esperado
Toda la imagen se desvía de forma uniforme Confusión de matrix (601 / 709) o de range (16..235 / 0..255)
Aparecen franjas o un desplazamiento diagonal Stride fijado a mano. Véanse 7.2. y 7.5.
El rojo y el azul se intercambian Confusión entre BGRA y RGBA
Todo se ve transparente o completamente negro No se rellenó el 4.º byte con 0xFF. Véase 7.1.

Si observa la “forma” de la diferencia, puede acotar bastante dónde buscar el problema. Si el desajuste es uniforme en toda la imagen, sospeche de la fórmula o de la información de color; si es localizado, sospeche de los índices o del stride.

6. Cuál elegir

Cuando dude, esta tabla ayuda bastante a decidir.

Criterio Conversión automática (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) Conversión manual
Rapidez de implementación
Extraer unas pocas imágenes fijas
Grandes volúmenes de fotogramas / tiempo real
Controlar explícitamente matrix / range
Integrarse con GPU / D3D ○〜◎
Necesitar una salida distinta de RGB32
Comprender los fundamentos

Para el primer proyecto, es fácil decidir pensando así.

  • Quiere que funcione cuanto antes -> conversión automática
  • Quiere asumir la responsabilidad del color y del rendimiento -> conversión manual

En la práctica, también funciona bastante bien el orden “primero confirmar la imagen correcta con la conversión automática, y después sustituirla por la ruta manual”. Si carga con todo desde el principio, resulta difícil saber en qué punto se rompió la imagen.

7. Trampas habituales en la práctica

7.1. Suponer que RGB32 es RGBA con alfa

RGB32 está en memoria como B, G, R, Alpha or Don't Care. Si lo convierte tal cual a PNG como BGRA, es posible que el 4.º byte sea 0 y quede transparente. Es más seguro escribir 0xFF antes de guardar.

7.2. Fijar el stride como width * bytesPerPixel

Es un accidente bastante habitual. El sample buffer real puede llevar padding, así que la regla es usar el actual stride para moverse entre filas.

7.3. Confundir MF_MT_DEFAULT_STRIDE con el pitch real

MF_MT_DEFAULT_STRIDE es “el stride mínimo al representar ese format en memoria contigua”. Para el actual pitch del sample buffer, dele prioridad al valor que devuelve IMF2DBuffer::Lock2D. (pitch es sinónimo de stride. Como se mencionó en el apartado 3.2., en este artículo se usan con el mismo sentido.)

7.4. Suponer BT.601/BT.709 sin mirar los metadatos de color

Los accidentes de color son difíciles de ver. Tampoco provocan un fallo. Por eso son molestos.

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

Al menos, revíselos. Y conviene tener la actitud de convertir en error los valores que su código no soporta.

7.5. Cortar el UV plane de NV12 con width * height

El offset del plane se determina por el stride real y el height, no por width * height. Si se hace esto sin cuidado, el color se desajusta o la imagen se rompe.

7.6. Procesar video entrelazado asumiendo que es progresivo

La muestra manual de este artículo asume video progresivo. Si lee un video entrelazado tal cual como si fuera 1 field, puede aparecer un ruido en forma de peine. Si necesita deinterlace, lo más directo es plantearse el video processing automático del Source Reader o el Video Processor MFT.

7.7. Ignorar la calidad del chroma upsampling en 4:2:0

La conversión de NV12 de este artículo prioriza la claridad y usa el chroma compartido tal cual en cada píxel. Para muchos usos es suficiente, pero si prioriza la calidad de imagen, conviene seguir también el enfoque de upconversion descrito en el documento de formatos recomendados de YUV.

8. Resumen

Al convertir de YUV a RGB en Media Foundation, tener claros estos puntos reduce bastante la confusión.

  • Detrás del decoder, lo normal es que salga NV12 o YUY2, no RGB
  • Si quiere ahorrarse trabajo, solicite RGB32 con MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
  • Si quiere controlarlo, reciba NV12 / YUY2 y conviértalo usted mismo a BGRA
  • En la ruta manual, antes que la fórmula, domine el sampling / range / matrix / stride
  • Si deja ambiguos BT.601 / BT.709, 16..235 o 4:2:0 / 4:2:2, obtendrá un color desajustado o una imagen rota

YUV -> RGB resulta un poco difícil de abordar al principio. Pero una vez que se le graba en la cabeza esta imagen:

  • NV12 comparte U/V en bloques de 2x2
  • YUY2 comparte U/V cada 2 píxeles en horizontal
  • se aplica la matrix a ese U/V y a Y

todo se vuelve bastante directo. Esa secuencia de bytes de colores extraterrestres empieza a verse como píxeles con sentido de verdad.

9. Referencias

Código de ejemplo de este artículo

Artículos relacionados de KomuraSoft

Microsoft Learn

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Desarrollo de aplicaciones para Windows

El tema trata la implementación del procesamiento multimedia en Windows, incluidos Media Foundation, Source Reader, el guardado de imágenes y la conversión de fotogramas de video, por lo que encaja bien con el desarrollo de aplicaciones Windows.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Por qué el decoder de Media Foundation entrega YUV en lugar de RGB?
Porque el ojo humano es más sensible a la resolución de brillo que a la de color, y en video conviene un diseño que conserve Y (el componente cercano al brillo) con alta resolución y U/V (los componentes de crominancia) con resolución más baja. Por eso, en el mundo de video de Windows, los fotogramas sin comprimir que salen del decoder suelen estar en un formato de la familia YUV, como NV12 o YUY2. En el contexto del video digital conviene leer YUV como si, en la práctica, significara Y'CbCr.
¿Cuál es la forma más sencilla de obtener un fotograma RGB?
Habilitar MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING en IMFSourceReader y solicitar MFVideoFormat_RGB32. Es la opción más cómoda para extraer unas pocas imágenes fijas o generar miniaturas. Sin embargo, esta conversión automática se ejecuta por software y no está optimizada para reproducción en tiempo real, así que si necesita procesar grandes volúmenes o controlar el color, conviene recibir el YUV tal cual y convertirlo usted mismo.
¿Qué hay que tener en cuenta al convertir YUV a RGB por su cuenta?
No basta con aplicar tres coeficientes: entran en juego el submuestreo (4:2:0 / 4:2:2), el rango, la matriz y el stride. En la práctica, lo que más suele romper el color es no revisar MF_MT_YUV_MATRIX y MF_MT_VIDEO_NOMINAL_RANGE, y asumir que el stride es width × bytesPerPixel. Lo más rápido es entender bien primero la estructura de NV12 y YUY2.
¿En qué se diferencian NV12 y YUY2?
NV12 es un formato 4:2:0: tras el plane de Y viene un plane de UV donde U y V se alternan, y cada bloque de 2x2 (4 píxeles) comparte un par U/V. YUY2 es un formato 4:2:2 en el que cada 2 píxeles en horizontal comparten U/V. Ambos son formatos muy habituales en la práctica, y la diferencia está en cómo se diezma (submuestrea) el color.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog