Cómo convertir YUV a RGB en Media Foundation
· Actualizado el: · Go Komura · 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
IMFSourceReaderhaga todo el camino automáticamente hasta RGB32 - Patrón B: recibir
NV12/YUY2y 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.libmfreadwrite.libmfuuid.libole32.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_PROCESSINGy solicitarMFVideoFormat_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
NV12yYUY2 - 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_MATRIXniMF_MT_VIDEO_NOMINAL_RANGE, y asumir que el stride eswidth * 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.
flowchart LR
accTitle: Flujo de conversión de YUV a RGB en Media Foundation
accDescr: Diagrama 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)
File["MP4 / H.264 / HEVC"] --> Decoder["decoder"]
Decoder --> YUV["Fotograma YUV: NV12 / YUY2 / YV12, etc."]
YUV -->|Patrón A| SRVP["Video processing del Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|Patrón B| App["Código de conversión propio"]
App --> RGB2["BGRA / 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.
- Hacer que el propio Media Foundation llegue hasta RGB32
- 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:
Yes el componente cercano al brilloU/Vson 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.
- 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
- Revertir el rango En video, Y suele usar 16..235 y U/V 16..240, así que hay que revertir ese escalado
- Aplicar la matriz
Convertir a RGB con coeficientes como
BT.601oBT.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_MATRIXMF_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.
- Establecer
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUEen los attributes que se pasan aMFCreateSourceReaderFromURL - Seleccionar el stream de video
- Solicitar
MFMediaType_Video/MFVideoFormat_RGB32conSetCurrentMediaType - 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,
×tamp,
ppSample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (*ppSample == nullptr) return MF_E_INVALID_STREAM_DATA;
if (pTimestamp100ns) *pTimestamp100ns = timestamp;
return S_OK;
}
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
NV12tal 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.
- Configurar la salida del Source Reader como
NV12oYUY2 - Obtener el subtype real y los atributos con
GetCurrentMediaType - Comprobar
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIXyMF_MT_VIDEO_NOMINAL_RANGE - Extraer el buffer del sample y bloquearlo (lock)
- Calcular el Y/U/V que referencia cada píxel
- 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_flages 0,matrix_coefficientsse 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,GetUINT32no devuelve ningún valor y falla conMF_E_ATTRIBUTENOTFOUND(en el código anterior, esto se rechaza directamente conFAILED(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_STRIDEes 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
NV12oYUY2 - Construir
DecodedFrameInfoa partir deGetCurrentMediaType ReadSampleConvertSampleToBgra32
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,
¤tType);
if (FAILED(hr)) return hr;
DecodedFrameInfo info;
hr = GetStrictDecodedFrameInfo(currentType.Get(), &info);
if (FAILED(hr)) return hr;
DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> sample;
hr = reader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&flags,
×tamp,
&sample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (!sample) return MF_E_INVALID_STREAM_DATA;
std::vector<BYTE> bgra;
hr = ConvertSampleToBgra32(sample.Get(), info, bgra);
if (FAILED(hr)) return hr;
// bgra 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:
- Patrón A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Patrón B (recibir
NV12/YUY2y 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_MATRIXMF_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
NV12oYUY2, no RGB - Si quiere ahorrarse trabajo, solicite
RGB32conMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING - Si quiere controlarlo, reciba
NV12/YUY2y 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..235o4: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:
NV12comparte U/V en bloques de 2x2YUY2comparte 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
- Media Foundation desde cero: entender la API desde el punto de vista de COM
- Cómo extraer una imagen fija de un MP4 en un instante determinado con Media Foundation
Microsoft Learn
- Source Reader
- Using the Source Reader to Process Media Data
- MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- IMFSourceReader::SetCurrentMediaType
- Recommended 8-Bit YUV Formats for Video Rendering
- Extended Color Information
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- MF_MT_VIDEO_NOMINAL_RANGE attribute
- MFVideoTransferMatrix enumeration
- Video Processor MFT
- Uncompressed RGB Video Subtypes
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo grabar imágenes y texto en los fotogramas de un MP4 con Media Foundation
Con Media Foundation, cómo grabar imágenes y texto en cada fotograma de un MP4 repartiendo el trabajo entre Source Reader, el dibujado, l...
Cómo extraer una imagen fija de un MP4 en un instante específico con Media Foundation
Extraemos con Source Reader el fotograma más cercano a un instante de un MP4, ajustamos el stride y el alpha de RGB32, y lo guardamos com...
Introducción a Media Foundation - Cómo entender la API desde la perspectiva de COM
Explicamos qué es Media Foundation junto con los términos básicos de su API multimedia en Windows —COM, HRESULT, IMFSourceReader, MFT— en...
Trampas de la memoria compartida y buenas prácticas para producción
Analizamos las trampas de usar memoria compartida en producción y el diseño que reduce la tasa de incidentes: sincronización, visibilidad...
Cómo invocar una DLL nativa de C# Native AOT desde C/C++
Publicar una biblioteca de C# como DLL nativa con Native AOT e invocar sus puntos UnmanagedCallersOnly desde C/C++: casos de uso, patrone...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
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.
Consultoría técnica y revisión de diseño
Si desea ordenar primero el reparto de responsabilidades de la conversión YUV / RGB, el espacio de color, el stride y el diseño de la ruta de conversión, este es un tema fácil de abordar como consultoría técnica o revisión de diseño.
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.