Как преобразовать YUV в RGB с помощью Media Foundation
· Го Комура · Media Foundation, C++, Разработка Windows, Обработка видео, YUV
Извлечь кадр из видео и сохранить его как PNG, передать в WIC или GDI, либо вывести в UI. В таких сценариях приложению нужен массив пикселей в формате RGB.
Однако кадры, которые выдаёт decoder Media Foundation, чаще всего представлены в YUV-форматах вроде NV12 или YUY2. Если трактовать этот сырой поток байтов напрямую как изображение, получится немного грустная картинка: цвета ломаются, появляются полосы, изображение приобретает странный зеленоватый оттенок.
В более ранней статье «Что такое Media Foundation - почему за COM и медиа-API Windows начинает проступать одно и то же лицо» мы разобрали общую картину, а в статье «Как извлечь статичное изображение из MP4 в заданный момент времени с помощью Media Foundation - однофайловая версия, которую можно вставить прямо в .cpp» — извлечение статичных кадров. В этот раз мы разберём то, что находится между этими темами: само преобразование YUV -> RGB.
В этой статье мы отдельно разберём следующие два паттерна.
- Паттерн A: доверить
IMFSourceReaderдовести кадры до RGB32 автоматически - Паттерн B: получать
NV12/YUY2и конвертировать в RGB самостоятельно
Цель — не запомнить названия API. Цель — суметь мысленно представить, в каком месте Media Foundation появляется YUV и где он превращается в RGB.
Код, который встречается в этой статье, опубликован на GitHub в виде полного набора примеров (C++ код для паттернов A и B, конфигурация CMake, тесты преобразования пикселей).
media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)
1. Сначала — вывод
Сначала — только выводы.
- Для извлечения нескольких статичных кадров или генерации миниатюр проще всего включить
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGи запроситьMFVideoFormat_RGB32 - Однако это автоматическое преобразование выполняется программно (software) и не оптимизировано для воспроизведения в реальном времени
- Если вы пишете собственное преобразование, кратчайший путь — сначала как следует разобраться в
NV12иYUY2 - YUV -> RGB — это не «умножить на три коэффициента и готово»: на практике здесь задействованы субдискретизация, range, matrix и stride
- В документации Media Foundation широко используется термин
YUV, но применительно к цифровому video проще воспринимать его как фактическое обозначение Y’CbCr - На практике цвет чаще всего портят из-за того, что не смотрят на
MF_MT_YUV_MATRIXиMF_MT_VIDEO_NOMINAL_RANGE, а также из-за предположения, что stride равенwidth * bytesPerPixel
Итого: если хочется сделать всё проще — пусть Source Reader сам выдаёт RGB32. Если нужна массовая обработка или контроль над цветом — принимайте кадры в YUV и конвертируйте сами. Выбор из этих двух вариантов.
2. Сначала — посмотрим на схему
Сначала быстрее будет посмотреть на схему того, что происходит внутри Media Foundation.
flowchart LR
File["MP4 / H.264 / HEVC"] --> Decoder["декодер"]
Decoder --> YUV["YUV-кадры: NV12 / YUY2 / YV12 и т.д."]
YUV -->|Паттерн A| SRVP["video processing в Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|Паттерн B| App["Собственный код преобразования"]
App --> RGB2["BGRA / RGB"]
Если содержимое видеофайла представлено в сжатом формате вроде H.264 или HEVC, decoder сначала возвращает его к несжатым кадрам. Эти несжатые кадры не обязательно оказываются RGB. Более того, в видеоподсистеме Windows YUV-форматы — это норма.
Поэтому, когда приложению нужен RGB, выбирают один из двух вариантов.
- Довести кадры до RGB32 силами самой Media Foundation
- Получить YUV и превратить его в RGB собственным кодом
Тема этой статьи — как раз эта развилка.
3. Сначала разберёмся с соотношением YUV и RGB
3.1. Хотя говорят YUV, на самом деле речь о Y’CbCr
Названия API и документация Windows широко используют термин YUV. Однако в контексте цифрового video можно почти без проблем читать U как Cb, а V как Cr.
Упрощённо соотношение таково:
Y— компонент, близкий к яркостиU/V— цветоразностные компонентыRGB— каждый пиксель напрямую хранит значения Red / Green / Blue
Человеческий глаз чувствительнее к деталям яркости, чем к деталям цвета. Поэтому в video выгодно хранить Y подробно, а U/V — несколько более грубо. Именно поэтому YUV-форматы получили такое широкое распространение.
3.2. 4:4:4 / 4:2:2 / 4:2:0 — это «насколько прорежен цвет»
Это ключевой момент для понимания YUV.
| Обозначение | Значение | Типичные примеры |
|---|---|---|
| 4:4:4 | Каждый pixel хранит собственные Y/U/V | AYUV, I444 |
| 4:2:2 | 2 pixel по горизонтали используют общую пару U/V | YUY2, UYVY, I422 |
| 4:2:0 | Блок 2x2 pixel использует общую пару U/V | NV12, YV12, I420 |
Стоит сразу посмотреть на структуру двух форматов, которые чаще всего встречаются на практике — это заметно облегчает понимание.
NV12 (4:2:0, planar)
Y plane
Y Y Y Y
Y Y Y Y
Y Y Y Y
Y Y Y Y
UV plane
U V U V
U V U V
В NV12 4 пикселя блока 2x2 используют общую пару U/V. Y присутствует для каждого пикселя отдельно.
YUY2 (4:2:2, packed)
bytes:
Y0 U0 Y1 V0 Y2 U2 Y3 V2 ...
В YUY2 2 пикселя по горизонтали используют общую пару U/V. Y0 и Y1 — разные значения, а U0 и V0 общие.
Уже на этом этапе видно, что YUV -> RGB — не простая замена «один пиксель на один пиксель». Сначала нужно продумать, как распределить общую пару U/V между пикселями.
3.3. YUV -> RGB — это «преобразование цветового пространства + преобразование сэмплирования»
Если посмотреть на Extended Color Information в Media Foundation, видно, что строго правильное цветовое преобразование состоит из немалого числа этапов: inverse quantization, chroma upsampling, YUV -> RGB, transfer function, преобразование primaries и, наконец, quantization.
Но если рассматривать это как практический код для 8-bit SDR, для начала проще разбить процесс на следующие три уровня.
- Отменить субдискретизацию.
Развернуть
U/Vиз 4:2:0 или 4:2:2 так, чтобы на них мог сослаться каждый pixel - Отменить range.
Yв video обычно использует диапазон 16..235, аU/V— 16..240, поэтому нужно отменить это масштабирование - Применить matrix.
Преобразовать в RGB с коэффициентами вроде
BT.601илиBT.709
То есть на практике преобразование YUV -> RGB — это процесс, в котором решается:
- какая пара U/V соответствует цвету данного pixel
- с какими коэффициентами превратить это Y/U/V обратно в RGB
3.4. Если небрежно обращаться с BT.601 и BT.709, цвет постепенно уходит в сторону
В документации Media Foundation описывается соотношение, при котором BT.601 предпочтителен для SDTV и ниже, а BT.709 — для video выше SD.
Однако молча предполагать «раз разрешение большое, значит это 709» — не лучшая идея. Сдвиг цвета не приводит к сбою, поэтому легко остаётся незамеченным и попадает в продакшен.
В Media Foundation информация о цветовом пространстве может храниться в атрибутах media type. Как минимум стоит проверить эти два:
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
Если проверять эти два атрибута и явно пропускать только те комбинации, которые поддерживает ваш код, риск тихих проблем в будущем заметно снижается.
3.5. Первая формула, которую стоит запомнить — limited range версия BT.601
Типичная формула для 8-bit BT.601 выглядит так.
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)
Для BT.709 коэффициенты меняются — мы приведём их и в коде позже.
Здесь важнее не «зазубрить коэффициенты», а понять структуру: из Y вычитается чёрный уровень 16, а U/V рассматриваются относительно центра 128.
4. Паттерн A: доверить преобразование Media Foundation
4.1. Когда этот способ подходит
Этот способ подходит, например, в следующих ситуациях:
- нужно извлечь один статичный кадр из MP4
- нужно создать несколько миниатюр
- нужно получить RGB-изображение и передать его в WIC
- подходит пакетная обработка или инструмент, а не воспроизведение в реальном времени
У Source Reader есть функция, которая с помощью MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING выполняет ограниченную video processing для YUV -> RGB32.
Однако, как указано и в Microsoft Learn, это программная (software) обработка, не оптимизированная для playback. Если нужно обрабатывать сотни кадров в секунду, опираться на этот механизм не совсем правильно.
4.2. Что нужно настроить, чтобы получить RGB32
Последовательность довольно прямолинейна.
- В
attributes, передаваемых вMFCreateSourceReaderFromURL, установитьMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - Выбрать видео stream
- Запросить
MFMediaType_Video/MFVideoFormat_RGB32черезSetCurrentMediaType - Прочитать sample через
ReadSample
Это всё — и ограниченная video processing, встроенная сразу после decoder, сама выполнит преобразование YUV -> RGB32.
4.3. Код
Приведённый ниже код предполагает, что CoInitializeEx и MFStartup уже выполнены. В минимальной конфигурации это выглядит примерно так.
#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;
}
После этого можно вызвать GetCurrentMediaType, чтобы узнать реальный размер вывода и stride.
4.4. Сильные стороны этого способа
Главное достоинство этого способа — он позволяет быстро получить корректную картинку.
- не нужно самостоятельно писать раскрытие 4:2:0 / 4:2:2
- значительная часть хлопот с matrix / deinterlace скрыта от вас
- результат легко передать в WIC или GDI
- вполне практичен для обработки нескольких кадров
Для инструментов извлечения статичных изображений совершенно естественно начинать именно отсюда.
4.5. Но есть и подводные камни
У этого автоматического преобразования есть следующие особенности.
| Параметр | Значение |
|---|---|
| Целевой формат | Как правило RGB32 |
| Реализация | Программная (software) обработка |
| Подходящее применение | небольшое число frame, миниатюры, offline-обработка |
| Неподходящее применение | real-time rendering на базе D3D, обработка большого числа frame |
| Плохо совместимые атрибуты | MF_SOURCE_READER_D3D_MANAGER, MF_READWRITE_DISABLE_CONVERTERS |
И ещё один важный момент — обращение с 4-м байтом RGB32. В памяти Windows RGB32 хранится в порядке Blue / Green / Red / Alpha or Don’t Care. Это не ARGB32. Если вы передаёте данные в WIC как 32bppBGRA, безопаснее заполнить 4-й байт значением 0xFF, сделав его непрозрачным.
Об этом мы уже упоминали в предыдущей статье про извлечение статичных изображений как о месте, где легко ошибиться.
5. Паттерн B: написать преобразование самостоятельно
5.1. Когда этот способ подходит
Собственное преобразование подходит, например, в таких случаях:
- нужно обрабатывать большое число frame, и вы хотите оптимизировать преобразование самостоятельно
- нужно передавать
NV12напрямую в GPU или SIMD-код - нужно явно управлять
BT.601/BT.709/ range - нужен выходной формат, отличный от
RGB32 - ограниченного автоматического преобразования Source Reader недостаточно
Можно сказать, это паттерн, при котором вы берёте на себя ответственность за объём обработки и цвет в обмен на свободу.
5.2. Общий поток собственного преобразования
Последовательность такая.
- Настроить вывод Source Reader на
NV12илиYUY2 - Получить реальный subtype и атрибуты через
GetCurrentMediaType - Проверить
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIX,MF_MT_VIDEO_NOMINAL_RANGE - Извлечь buffer из sample и заблокировать его (lock)
- Определить, какие Y/U/V соответствуют каждому pixel
- Применить matrix и записать результат в BGRA
Код в этой статье ограничен случаем 8-bit SDR / progressive / NV12 или YUY2 / limited range. Сужение допущений здесь — не халтура, а, наоборот, важный шаг: если реализовать YUV-преобразование как «принимаем всё подряд», цвет легко портится незаметно.
5.3. Сначала явно задаём выходной media type
Сначала сообщаем Source Reader: «выдавай YUV как есть». Здесь тоже предполагается, что CoInitializeEx / MFStartup уже выполнены.
#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;
}
Сюда в качестве subtype передаётся MFVideoFormat_NV12 или MFVideoFormat_YUY2.
Стоит учитывать: запрошенный subtype не обязательно принимается как есть. То, что реально получилось на выходе, проверяется через GetCurrentMediaType.
5.4. Перед преобразованием принимаем только поддерживаемую информацию о цвете
При собственном преобразовании сначала извлекаем из media type минимально необходимую информацию.
В примере этой статьи принимаются только NV12 / YUY2, а из matrix пропускаются только BT.601 или BT.709, из range — только 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;
}
Здесь мы намеренно используем strict-подход.
В документации по enum Media Foundation встречаются формулировки вроде «Unknown трактуется как BT.709», но если молча округлять такие случаи на практике, сдвиг цвета становится труднее заметить. По крайней мере в первой реализации безопаснее считать ошибкой любую неподдерживаемую комбинацию.
Для камер и источников на базе JPEG иногда хочется отдельно обрабатывать пути с full-range. Здесь мы не смешиваем их молча, а придерживаемся политики явного сужения допущений, которые принимает этот код.
5.5. Читаем buffer, доверяя значению stride
Это тоже очень важный момент.
MF_MT_DEFAULT_STRIDE— это минимальный stride- у реального sample buffer может быть actual stride с учётом padding
- если доступен
IMF2DBuffer::Lock2D, отдавайте предпочтение ему
Если взять паттерн-helper из Uncompressed Video Buffers на Microsoft Learn и сделать его сразу пригодным для использования, получится следующее.
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;
};
Рекомендуемое определение YUV surface подразумевает top-left / положительный stride, но для реального доступа к buffer безопаснее использовать pitch, возвращённый API, как есть. Если здесь жёстко закладываться на width, позже всё тихо сломается.
5.6. Переводим формулу преобразования одного pixel в код
Здесь рассматриваются только limited-range варианты BT.601 и BT.709. Выходной формат — BGRA32, который удобно передавать в WIC или 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;
}
Здесь выполняется простая последовательность действий:
- из
Yвычитается 16 - из
U/Vвычитается 128 - применяются коэффициенты соответствующего matrix
- результат ограничивается (clip) диапазоном 0..255
- 4-й байт BGRA устанавливается в
255
5.7. Преобразование NV12 в BGRA32
NV12 — формат 4:2:0, поэтому 4 пикселя блока 2x2 используют одну и ту же пару U/V.
В минимальной реализации проще всего использовать эту общую chroma напрямую для всех 4 пикселей.
HRESULT ConvertNv12ToBgra32(
IMFMediaBuffer* buffer,
const DecodedFrameInfo& info,
std::vector<BYTE>& dstBgra)
{
if (!buffer) return E_POINTER;
if (info.subtype != MFVideoFormat_NV12) return MF_E_INVALIDMEDIATYPE;
if ((info.width & 1u) != 0 || (info.height & 1u) != 0)
{
return MF_E_INVALIDMEDIATYPE;
}
dstBgra.resize(static_cast<size_t>(info.width) * info.height * 4);
BufferLock lock(buffer);
BYTE* scanline0 = nullptr;
LONG actualStride = 0;
HRESULT hr = lock.Lock(
info.defaultStride,
info.height,
&scanline0,
&actualStride);
if (FAILED(hr)) return hr;
if (actualStride <= 0)
{
lock.Unlock();
return MF_E_INVALIDMEDIATYPE;
}
const BYTE* yPlane = scanline0;
const BYTE* uvPlane =
scanline0 + static_cast<size_t>(actualStride) * info.height;
for (UINT32 y = 0; y < info.height; ++y)
{
const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;
const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);
BYTE* dstRow =
dstBgra.data() + static_cast<size_t>(info.width) * 4 * y;
for (UINT32 x = 0; x < info.width; ++x)
{
const BYTE Y = yRow[x];
const BYTE U = uvRow[(x / 2) * 2 + 0];
const BYTE V = uvRow[(x / 2) * 2 + 1];
hr = ConvertLimitedYuvPixelToBgra(
Y,
U,
V,
info.matrix,
dstRow + static_cast<size_t>(x) * 4);
if (FAILED(hr))
{
lock.Unlock();
return hr;
}
}
}
lock.Unlock();
return S_OK;
}
Этот код трактует chroma upsampling в духе nearest-neighbor. Визуально этого часто вполне достаточно, но если стремиться к максимальному качеству, теоретически более аккуратным будет решение, при котором сначала выполняется upconversion 4:2:0 -> 4:2:2 -> 4:4:4, как описано в статье Microsoft Learn о YUV.
5.8. Преобразование YUY2 в BGRA32
YUY2 — это packed-формат 4:2:2.
Пара U/V используется всего двумя пикселями, поэтому читать такой код немного проще, чем для 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;
}
Байты YUY2 идут в порядке Y0 U Y1 V, поэтому структура «переиспользуем U/V на каждые 2 пикселя» видна напрямую.
Благодаря этому мысленную модель для YUY2 строить проще, чем для NV12.
5.9. Точка входа при вызове из sample
Наконец, если извлечь непрерывный buffer из IMFSample и разветвиться по subtype, использовать код становится удобно.
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;
}
Теперь предшествующие шаги выстраиваются в такую последовательность:
- создать reader
- запросить
NV12илиYUY2 - сформировать
DecodedFrameInfoизGetCurrentMediaType ReadSampleConvertSampleToBgra32
Код на стороне вызывающей стороны выглядит, например, так.
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 можно рассматривать как top-down / 32bpp BGRA
5.10. Где размещать «собственное преобразование»
Код, рассмотренный выше, представляет собой форму, при которой преобразование выполняет само приложение после Source Reader. Это самый понятный вариант.
Однако если хочется встроить преобразование прямо в pipeline Media Foundation, существуют и другие подходы.
- написать собственный
MFT - использовать
Video Processor MFT/ XVP - написать shader
NV12-> RGB на стороне GPU
Если зайти настолько далеко, тема несколько меняется, поэтому в этой статье мы ограничились кодом на стороне приложения. Но полезно знать, что между «доверить всё Media Foundation» и «сделать всё в приложении» существует промежуточная точка — Video Processor MFT.
6. Какой вариант выбрать
Если сомневаетесь, следующая таблица хорошо помогает разобраться.
| Критерий | Автоматическое преобразование (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
Собственное преобразование |
|---|---|---|
| Скорость реализации | ◎ | △ |
| Извлечение нескольких статичных кадров | ◎ | ○ |
| Большое число frame / real-time | △ | ◎ |
| Явный контроль matrix / range | △ | ◎ |
| Совмещение с GPU / D3D | △ | ○〜◎ |
Нужен вывод, отличный от RGB32 |
△ | ◎ |
| Понимание принципов | ○ | ◎ |
Для первой реализации проще рассуждать так:
- хочется сначала просто запустить -> автоматическое преобразование
- хочется взять на себя ответственность за цвет и производительность -> собственное преобразование
На практике вполне эффективен и такой порядок: «сначала убедиться в корректности картинки с помощью автоматического преобразования, а затем заменить его на manual path». Если с самого начала брать на себя всё сразу, становится трудно понять, в каком месте картинка сломалась.
7. Подводные камни, в которые легко попасть на практике
7.1. Считать RGB32 форматом RGBA с alpha
В памяти RGB32 хранится как B, G, R, Alpha or Don't Care.
Если сохранить это напрямую как BGRA в PNG, 4-й байт может оказаться равным 0, и изображение станет прозрачным. Безопаснее перед сохранением записать в этот байт 0xFF.
7.2. Жёстко задавать stride через width * bytesPerPixel
Это довольно распространённая ошибка. В реальном sample buffer может присутствовать padding, поэтому правило таково: для перехода между row нужно использовать actual stride.
7.3. Путать MF_MT_DEFAULT_STRIDE с actual pitch
MF_MT_DEFAULT_STRIDE — это «минимальный stride при представлении данного format в непрерывной памяти».
Для actual pitch у sample buffer стоит отдавать приоритет значению, которое возвращает IMF2DBuffer::Lock2D.
7.4. Молча угадывать 601 / 709, не глядя на color metadata
Проблемы с цветом трудно заметить визуально. Они также не приводят к сбою. Именно поэтому они и коварны.
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
Проверяйте хотя бы эти два атрибута. И правильный настрой — считать ошибкой любое значение, которое не поддерживает ваш код.
7.5. Вычислять UV plane в NV12 через width * height
Смещение plane определяется реальными stride и height, а не width * height.
Если сделать это небрежно, цвет сместится или изображение будет повреждено.
7.6. Обрабатывать interlaced video как progressive
Manual-пример в этой статье рассчитан на progressive. Если читать interlaced-видео как один field напрямую, может появиться гребенчатый шум.
Если нужен deinterlace, разумнее рассмотреть автоматическую video processing в Source Reader или Video Processor MFT.
7.7. Игнорировать качество chroma upsampling для 4:2:0
Преобразование NV12 в этой статье ради простоты использует общую chroma напрямую для каждого пикселя. Для некоторых сценариев этого достаточно, но если приоритет — качество изображения, стоит разобраться и с подходом upconversion, описанным в документации по рекомендованным YUV-форматам.
8. Итог
При преобразовании YUV в RGB в Media Foundation стоит держать в голове следующую схему — тогда заблудиться будет намного сложнее.
- сразу после decoder обычно появляется не RGB, а
NV12илиYUY2 - если хочется проще — запросите
RGB32черезMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING - если хочется контроля — получайте
NV12/YUY2и конвертируйте в BGRA сами - на manual path важнее формулы разобраться с sampling / range / matrix / stride
- неопределённость с
BT.601/BT.709,16..235,4:2:0/4:2:2приводит к сдвигу цвета или испорченной картинке
YUV -> RGB поначалу выглядит немного пугающе. Но стоит один раз уложить в голове картину:
NV12— U/V общие на блок 2x2YUY2— U/V общие на 2 пикселя по горизонтали- к этим U/V и Y применяется matrix
и всё становится вполне понятным. Загадочная последовательность байт «космического» цвета начинает выглядеть как вполне осмысленные пиксели.
9. Справочные материалы
Код примеров к этой статье
Статьи по теме от KomuraSoft
- Что такое Media Foundation - почему за COM и медиа-API Windows начинает проступать одно и то же лицо
- Как извлечь статичное изображение из MP4 в заданный момент времени с помощью Media Foundation - однофайловая версия, которую можно вставить прямо в .cpp
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
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Как встроить изображение и текст в кадры MP4 с помощью Media Foundation
В статье разбирается, как средствами Media Foundation встроить изображение и текст в каждый кадр MP4-видео и получить новый MP4: распреде...
Как извлечь статичное изображение из MP4 по заданному времени с помощью Media Foundation
Как с помощью Source Reader извлечь из MP4 кадр, ближайший к заданному времени, привести в порядок stride и alpha-байт RGB32 и сохранить ...
Введение в Media Foundation — понимание API через призму COM
Разбираем, что такое Media Foundation, вместе с базовой терминологией Windows-медиа-API — COM, HRESULT, IMFSourceReader, MFT — в том поря...
Разделяемая память: подводные камни и практические рекомендации
Разбираем подводные камни практического использования разделяемой памяти и проектирование, снижающее частоту сбоев: синхронизация, видимо...
Как вызвать C# Native AOT DLL из C/C++
Разбираем, как публиковать библиотеку классов C# в виде нативной DLL через Native AOT и вызывать точки входа UnmanagedCallersOnly из C/C+...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Тема связана с реализацией обработки медиаданных в Windows — Media Foundation, Source Reader, сохранение изображений, преобразование видеокадров, — поэтому хорошо сочетается с разработкой Windows-приложений.
Технические консультации и ревью дизайна
Если нужно заранее продумать распределение ответственности между YUV/RGB-преобразованием, цветовое пространство, stride и маршрут преобразования, эта тема хорошо подходит для технической консультации и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Почему декодер Media Foundation выдаёт YUV, а не RGB?
- Человеческий глаз чувствительнее к деталям яркости, чем к деталям цвета, поэтому в video выгодно хранить Y (компонент, близкий к яркости) подробно, а U/V (цветоразностные компоненты) — более грубо. Именно поэтому в видеоподсистеме Windows несжатые кадры, которые отдаёт decoder, обычно представлены в YUV-форматах вроде NV12 или YUY2. Отметим, что в контексте цифрового video термин YUV фактически означает Y'CbCr — так проще воспринимать материал.
- Как проще всего получить кадры в RGB?
- Самый простой способ — включить MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING в IMFSourceReader и запросить MFVideoFormat_RGB32. Для извлечения нескольких статичных кадров или генерации миниатюр это самый лёгкий путь. Однако это автоматическое преобразование выполняется программно (software) и не оптимизировано для воспроизведения в реальном времени, поэтому для массовой обработки или когда нужен контроль над цветом лучше получать кадры в YUV и конвертировать их самостоятельно.
- На что обратить внимание при самостоятельном преобразовании YUV в RGB?
- Дело не сводится к умножению на три коэффициента — в игру вступают субдискретизация (4:2:0 / 4:2:2), range, matrix и stride. На практике цвет чаще всего портят два момента: игнорирование MF_MT_YUV_MATRIX и MF_MT_VIDEO_NOMINAL_RANGE, а также предположение, что stride равен width × bytesPerPixel. Кратчайший путь — сначала как следует разобраться в структуре NV12 и YUY2.
- В чём разница между NV12 и YUY2?
- NV12 — это формат 4:2:0: за Y plane следует UV plane, в котором U и V чередуются, а 4 пикселя блока 2x2 используют общую пару U/V. YUY2 — формат 4:2:2, где пару U/V используют 2 пикселя по горизонтали. Оба формата часто встречаются на практике и различаются способом прореживания цвета (субдискретизацией).
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки