Cómo convertir YUV a RGB en Media Foundation

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

Historial de revisiones (1 actualizaciones, última el 3 Sep 2026)

Registro de los cambios realizados en este artículo. Cuando se archivó una versión previa, sigue siendo legible mediante un enlace permanente con DOI.

Se restauraron 21 de las 22 figuras del artículo que faltaban en la traducción, junto con sus leyendas. Los grafos son idénticos a los japoneses; solo se han traducido las etiquetas. Leer la versión anterior a esta actualización (DOI: 10.5281/zenodo.22173472)
Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.22173471)

Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.

Go Komura (2026). Cómo convertir YUV a RGB en Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173471 https://comcomponent.com/es/blog/media-foundation-yuv-to-rgb-conversion-patterns/

DOI (última versión)
10.5281/zenodo.22173471
DOI (esta versión)
10.5281/zenodo.22279098

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.

La disyuntiva de este artículoDiagrama que muestra la disyuntiva que trata este artículo: si quiere ahorrarse trabajo, deje que el Source Reader entregue RGB32, y si necesita procesar grandes volúmenes o controlar el color, reciba el YUV tal cual y conviértalo usted mismo.Ahorrar trabajoGrandes volúmenes / control del colorQuiero fotogramas en RGBPatrón A: que el Source Reader entregue RGB32Patrón B: recibir YUV y convertir manualmente

Figura 1: La disyuntiva es el patrón A si prioriza ahorrar trabajo, y el patrón B si asume la responsabilidad del volumen de procesamiento y del color.

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

Figura 2: El decoder entrega fotogramas YUV, y a partir de ahí el camino hacia RGB se bifurca en el patrón A y el patrón B.

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.

Por qué se usan los formatos de la familia YUVDiagrama que muestra que, como el ojo humano es más sensible a la resolución del brillo que a la del color, conviene un diseño que conserve Y con alta resolución y U/V con resolución más baja, y que esta es la razón por la que se usan tanto los formatos de la familia YUV.El ojo humano es sensible al brilloConservar Y con alta resolución y U/V con resolución bajaRazón por la que se usan los formatos de la familia YUV

Figura 3: El diseño de la familia YUV conserva Y con alta resolución y U/V con resolución baja, ajustándose a la sensibilidad del ojo al brillo.

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.

Comparación de la unidad de compartición en NV12 y YUY2Diagrama que muestra que en NV12 un bloque de 2x2 (4 píxeles) comparte un par de U/V y en YUY2 lo comparten 2 píxeles en horizontal, por lo que primero hay que decidir a qué píxel se asigna el U/V compartido.NV12 (4:2:0)4 píxeles de un bloque 2x2 comparten un par U/VYUY2 (4:2:2)2 píxeles en horizontal comparten un par U/VDecidir a qué píxel y cómo se asigna

Figura 4: En ambos formatos el U/V está compartido, así que la conversión no se resuelve sustituyendo píxel a píxel.

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 consiste en decidir dos cosas:

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

Ese es el proceso.

Las 3 capas que conviene dominar en el código prácticoDiagrama que muestra que en el código práctico de 8 bits SDR conviene dividir la conversión de YUV a RGB en tres capas: revertir el submuestreo, revertir el rango y aplicar la matriz.Fotograma YUVRevertir el submuestreoRevertir el rango (16..235, etc.)Aplicar la matriz (601 / 709)RGB

Figura 5: La conversión no son tres coeficientes, sino tres capas: submuestreo, rango y matriz.

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.

Cómo evitar suponer el espacio de colorDiagrama que muestra que suponer en silencio 601 o 709 a partir de la resolución hace que el desajuste de color llegue a producción sin que nadie lo note, y que conviene revisar los atributos de matrix y de range y dejar pasar de forma explícita solo las combinaciones soportadas.Suponer en silencio a partir de la resoluciónEl desajuste de color no provoca un falloLlega a producción sin que nadie lo noteRevisar los atributos matrix y rangeDejar pasar solo las combinaciones soportadasSe evita el accidente silencioso

Figura 6: No suponga el espacio de color: revise los atributos y deje pasar de forma explícita solo las combinaciones que soporta.

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.

Estructura de la fórmula de conversiónDiagrama que muestra la estructura de la fórmula de conversión: restar a Y el nivel de negro 16, mirar U y V centrados en 128, aplicar los coeficientes de cada matrix y recortar el resultado a 0..255.Restar 16 a Y (nivel de negro)Aplicar los coeficientes de la matrixRestar 128 a U / V (centro)Recortar (clip) a 0..255

Figura 7: Lo que hay que memorizar no son los coeficientes, sino la estructura de la fórmula: el nivel de negro 16 y el centro 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.

Para qué sirve y para qué no la conversión automáticaDiagrama que muestra que la conversión automática del Source Reader es un procesamiento por software no optimizado para reproducción, adecuado para usos de tipo batch como imágenes fijas o miniaturas, y en el que no conviene apoyarse para procesar cientos de fotogramas por segundo.AdecuadoNo apoyarseConversión automática del Source ReaderProcesamiento por softwareImágenes fijas, miniaturas, batchCientos de fotogramas por segundo en tiempo real

Figura 8: La conversión automática es procesamiento por software, así que conviene reservarla para herramientas que traten pocos fotogramas.

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.

Los 4 pasos para habilitar la conversión automáticaDiagrama que muestra los 4 pasos de la conversión automática: habilitar el video processing en los attributes al crear el Reader, seleccionar el stream de video, solicitar RGB32 y leer con ReadSample.Habilitar el video processing en los attributesSeleccionar el stream de videoSolicitar RGB32Leer con ReadSampleSe convierte a RGB32 detrás del decoder

Figura 9: Basta con seguir los 4 pasos: el video processing que se coloca detrás del decoder llega hasta 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.

Tratamiento del 4.º byte de RGB32Diagrama que muestra que en el RGB32 de Windows el 4.º byte que sigue a B, G y R puede ser alpha o don't care, por lo que es más seguro rellenarlo con 0xFF para hacerlo opaco antes de pasarlo a WIC como 32bppBGRA.Rellenar con 0xFFPasarlo tal cualOrden en memoria de RGB323 bytes: B, G, REl 4.º byte es alpha o don't careSe puede pasar a WIC como 32bppBGRAPuede quedar transparente

Figura 10: El 4.º byte no está definido, así que hágalo opaco con 0xFF antes de pasarlo a WIC.

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.

Contrapartida de la conversión manualDiagrama que muestra que la conversión manual es el patrón en el que, a cambio de asumir uno mismo la responsabilidad del volumen de procesamiento y del color, se gana libertad para optimizar, conectar con GPU o SIMD, controlar explícitamente matrix y range y elegir el formato de salida.Elegir la conversión manualAsumir la responsabilidad del volumen de procesamiento y del colorLibertad de optimización, GPU / SIMD y formato de salidaControl explícito de matrix / range

Figura 11: La conversión manual es la opción que, a cambio de responsabilidad, gana rendimiento y libertad de color.

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.

Flujo completo de la conversión manualDiagrama que muestra los pasos de la conversión manual: fijar la salida en NV12 o YUY2, comprobar el media type y los atributos reales, bloquear el buffer, calcular el Y/U/V de cada píxel y aplicar la matriz para escribir en BGRA.Solicitar NV12 / YUY2Comprobar el subtype y los atributos realesBloquear (lock) el bufferCalcular el Y/U/V de cada píxelAplicar la matriz y escribir en BGRAAcotar las premisas rompe menos el color

Figura 12: La conversión manual avanza en el orden solicitar, comprobar, bloquear, referenciar y convertir, y cuanto más se acoten las premisas, más segura resulta.

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.

Separar lo solicitado de la salida realDiagrama que muestra que el subtype solicitado con SetCurrentMediaType no siempre se acepta tal cual, por lo que hay que comprobar con GetCurrentMediaType qué se obtiene realmente.No siempre se acepta tal cualSolicitar un subtypeSalida realComprobar con GetCurrentMediaTypeEscribir el resto del proceso con el valor comprobado

Figura 13: La solicitud es solo una solicitud: confirme siempre la salida real con GetCurrentMediaType antes de usarla.

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.

Qué hacer cuando la matrix es UnknownDiagrama que muestra que Unknown no significa que se sepa que es BT.709, sino que no se sabe, y que la política se divide entre rechazar estrictamente con un error o dejarlo pasar registrando en el log el supuesto aplicado, evitando siempre redondear en silencio.Política de este artículoSi hay que aceptarloEsto es lo único que hay que evitarmatrix Unknown = no se sabeRechazar estrictamente con un errorDejarlo pasar registrando el supuesto en el logRedondear en silencio

Figura 14: Unknown significa “no se sabe”, así que elija entre rechazarlo o aceptarlo con registro en el log, pero nunca lo redondee en silencio.

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.

Prioridad del strideDiagrama que muestra que MF_MT_DEFAULT_STRIDE es el stride mínimo y que el buffer real puede tener un stride con padding, por lo que si se puede usar Lock2D hay que dar prioridad a su valor y evitar fijar el stride en función de width.Máxima prioridad si está disponiblefallbackSe rompe en silencioactual stride que devuelve Lock2DValor que se usa para acceder al bufferMF_MT_DEFAULT_STRIDE (valor mínimo)Valor fijado a partir de width

Figura 15: Para moverse entre filas, priorice el stride real que devuelve Lock2D y no lo fije a partir de width.

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.

Los dos diseños de chroma upsamplingDiagrama que muestra que la implementación mínima usa el chroma compartido tal cual en los 4 píxeles, con una interpretación cercana a nearest-neighbor que visualmente suele bastar, mientras que para la máxima calidad de imagen es teóricamente más limpio convertir después de hacer el upconversion de 4:2:0 a 4:2:2 y a 4:4:4.Implementación mínima: usar el chroma compartido tal cualVisualmente suele ser suficientemente prácticoConvertir tras el upconversionTeóricamente más limpio, prioriza la calidadOrden 4:2:0 → 4:2:2 → 4:4:4

Figura 16: Cuándo usar la implementación mínima con el chroma compartido y cuándo un diseño que hace el upconversion por etapas.

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.

Bifurcación en la función de entradaDiagrama que muestra la estructura de la función de entrada: extraer el buffer contiguo del sample y bifurcar hacia la conversión de NV12 o la de YUY2 según el subtype, devolviendo un error en cualquier otro caso.NV12YUY2Cualquier otro casoIMFSampleExtraer el buffer contiguoA la conversión para NV12A la conversión para YUY2Devolver un error

Figura 17: En la entrada se pasa a un buffer contiguo y se bifurca según el subtype; lo no soportado cae directamente en un error.

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.

Dónde está la línea entre diferencia tolerable y fallo realDiagrama que muestra que una diferencia de más o menos 1 o 2 se explica por la fracción perdida en el muestreo y por la política de redondeo a entero, y que una diferencia que no se explica así es un fallo real ante el que hay que sospechar de las premisas de la conversión.Diferencia de ±1 o 2Diferencia sin explicaciónObservar la diferencia con el valor esperadoSe explica por la fracción del muestreo y la política de redondeoFallo realSospechar de las premisas: matrix, range, U/V, stride

Figura 18: Las diferencias pequeñas se explican por el muestreo y el redondeo, y las que no, apuntan a una confusión en las premisas.

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.

La verificación en 2 pasosDiagrama que muestra la verificación en dos pasos: primero pasar un solo píxel con Y/U/V conocidos y compararlo con el cálculo manual, y después comparar los fotogramas de las dos rutas tomados del mismo instante del mismo video para acotar, por la forma de la diferencia, dónde está el problema.Primer paso: comparar 1 píxel con el cálculo manualSegundo paso: comparar los fotogramas de las 2 rutasAcotar dónde buscar según la forma de la diferenciaNo hacen falta ni video ni Media Foundation

Figura 19: Si primero supera la verificación de 1 píxel y después compara el fotograma completo, la forma de la diferencia acota la causa.

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.

Orden de trabajo que funciona en la prácticaDiagrama que muestra que confirmar primero la imagen correcta con la conversión automática y sustituirla después por la ruta manual facilita saber en qué punto se rompió la imagen.Confirmar primero la imagen correcta con la conversión automáticaSustituirla después por la ruta manualEs más fácil aislar dónde se rompióCargar con todo desde el principioCuesta saber en qué punto se rompió

Figura 20: Si primero asegura la imagen correcta y después pasa a la implementación propia, aislar el punto de ruptura resulta mucho más sencillo.

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.

Cómo calcular el límite de plane en NV12Diagrama que muestra que el inicio del plane UV de NV12 se determina por el producto del stride real y el height, y que cortarlo por el producto de width y height provoca desajustes de color y una imagen rota.Avanzar stride × heightInicio del buffer (plane Y)Inicio del plane UVCortar por width × heightDesajuste de color e imagen rota

Figura 21: El límite del plane UV se obtiene con stride × height, nunca con width × height.

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.

La imagen que conviene tener en la cabezaDiagrama que muestra que, cuando se graba la imagen de que NV12 comparte U/V en bloques de 2x2, de que YUY2 los comparte cada 2 píxeles en horizontal y de que a ese U/V y a Y se les aplica la matrix, la misteriosa secuencia de bytes empieza a verse como píxeles con sentido.NV12: comparte U/V en bloques de 2x2Aplicar la matrix a ese U/V y a YYUY2: comparte U/V cada 2 píxeles en horizontalLa secuencia de bytes se ve como píxeles con sentido

Figura 22: Con la imagen de la unidad de compartición y de la matrix, la secuencia de bytes de YUV se lee sin dificultad.

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