在 Media Foundation 中将 YUV 转换为 RGB 的方法 - 从原理讲透 Source Reader 自动转换与手动转换
· 小村 豪 · Media Foundation, C++, Windows 开发, 视频处理, YUV
想从视频里截取一帧保存成 PNG,交给 WIC 或 GDI 处理,或者直接显示在界面上——这些场景下,应用程序想要的都是 RGB 像素数据。
但 Media Foundation 的 decoder 输出的帧,很常见的是 NV12 或 YUY2 这种 YUV 系格式。如果把这些原始字节直接当作图像处理,就会看到颜色损坏、出现条纹,甚至整张画面莫名发绿这种让人有点难过的结果。
之前写过的 Media Foundation 到底是什么 - 为什么处处能看到 COM 与 Windows 媒体 API 的影子 整理了整体概念,用 Media Foundation 从 MP4 视频指定时间点提取静态图像的方法 - 可直接粘贴进 .cpp 的单文件完整版 整理了静态图像的提取方法。这次要处理的,正是夹在中间的 YUV -> RGB 转换本身。
本文把下面 2 种模式分开整理:
- 模式 A:交给
IMFSourceReader自动转换到 RGB32 - 模式 B:接收
NV12/YUY2,自己转换成 RGB
目的不是记住 API 名称,而是让你能在脑中画出 Media Foundation 里 YUV 从哪里出现、又在哪里变成 RGB 的那条流程。
另外,本文出现的代码已经作为完整示例(模式 A / 模式 B 的 C++ 代码、CMake 配置、像素转换的测试)发布在 GitHub 上。
media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)
1. 先说结论
先把结论摆在前面,大致是这样:
- 只是抽取几张静态图像或生成缩略图 的话,启用
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING并请求MFVideoFormat_RGB32是最省事的做法 - 不过这个自动转换是 纯软件处理,并未针对实时播放做优化
- 如果要自己写转换,先把
NV12和YUY2的结构搞清楚是最快的路径 - YUV -> RGB 不是「乘上 3 个系数就完事」,实际上牵涉 色度子采样、range、matrix、stride
- Media Foundation 的文档里广泛使用
YUV这个词,但在数字视频语境下,把它当作实质上指的是 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"]
Decoder --> YUV["NV12 / YUY2 / YV12 等 YUV 帧"]
YUV -->|模式 A| SRVP["Source Reader 的 video processing"]
SRVP --> RGB1["RGB32"]
YUV -->|模式 B| App["自定义转换代码"]
App --> RGB2["BGRA / RGB"]
如果视频文件的内容是 H.264 或 HEVC 这类压缩格式,decoder 会先把它还原成 未压缩帧。这个未压缩帧未必是 RGB,实际上在 Windows 的视频处理链路里,YUV 系格式才是常态。
因此当应用程序想要 RGB 时,需要在下面两条路中选一条:
- 让 Media Foundation 一路转换到 RGB32
- 接收 YUV,自己写代码转成 RGB
本文讨论的正是这个分叉点。
3. 先理清 YUV 与 RGB 的关系
3.1. 名为 YUV,实际说的是 Y’CbCr
Windows 的 API 名称和文档广泛使用 YUV 这个词。不过在数字视频的语境下,把 U 读作 Cb、V 读作 Cr 基本没有问题。
粗略地说:
Y是偏亮度的成分U/V是色差成分RGB中每个像素直接持有 Red / Green / Blue
大致是这样的关系。
人眼对色彩细节的敏感度不如对亮度细节敏感,所以视频普遍采用 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 里,2x2 像素块中的 4 个像素共用一组 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 并不是单纯的「1 个像素对 1 个像素」的替换。首先要弄清楚 共用的 U/V 该分配给哪个像素。
3.3. YUV -> RGB 是「色彩空间转换 + 采样转换」
查看 Media Foundation 的 Extended Color Information 就会发现,严格意义上的色彩转换有相当多个阶段:inverse quantization、chroma upsampling、YUV -> RGB、transfer function、primaries 转换,一直到 quantization。
不过作为 8-bit SDR 的实务代码,先分成下面 3 层来理解会更容易:
- 还原采样 把 4:2:0 或 4:2:2 的 U/V 展开成每个像素都能取到的形式
- 还原 range 视频的 Y 通常使用 16..235、U/V 通常使用 16..240,需要把这个缩放还原回来
- 应用矩阵
用
BT.601或BT.709等系数转换为 RGB
也就是说,YUV -> RGB 转换在实务上要决定的是:
- 该像素的颜色应该参照哪一组 U/V
- 用什么系数把这组 Y/U/V 还原为 RGB
3.4. 粗略处理 BT.601 与 BT.709,颜色会慢慢跑偏
Media Foundation 的文档中说明,BT.601 用于 SDTV 及以下,BT.709 则优先用于超过 SD 的视频。
但如果在这里凭「分辨率大所以应该是 709 吧」这种感觉去 默默猜测,并不是好做法。颜色跑偏不会导致程序崩溃,所以很容易在没被发现的情况下就上线了。
Media Foundation 可以把色彩空间信息保存在 media type 的属性里,至少要检查下面这两项:
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
检查过这两项之后,只让自己代码支持的组合明确通过,之后才不容易悄悄出问题。
3.5. 首先要记住的是 BT.601 的 limited range 公式
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 中抽取 1 张静态图像
- 想生成几张缩略图
- 想把图像转成 RGB 后交给 WIC 处理
- 不是实时播放,批处理或工具用途就足够
Source Reader 提供了通过 MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING 执行 limited 的 YUV -> RGB32 video processing 的功能。
但正如 Microsoft Learn 中所说,这是 纯软件处理,并未针对 playback 做优化。如果要每秒处理几百帧,就不应该依赖它。
4.2. 要设置什么才能得到 RGB32
流程相当直接。
- 在传给
MFCreateSourceReaderFromURL的 attributes 中设置MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - 选择视频 stream
- 用
SetCurrentMediaType请求MFMediaType_Video/MFVideoFormat_RGB32 - 用
ReadSample读取 sample
仅凭这些,decoder 后面接入的 limited video processing 就会帮你完成 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,就能确认实际输出的 size 和 stride。
4.4. 这个方法的优点
这个方法的好处在于,能很快得到接近正确的画面。
- 不需要自己写 4:2:0 / 4:2:2 的展开逻辑
- matrix / deinterlace 的麻烦大部分都被隐藏起来
- 方便交给 WIC 或 GDI 处理
- 处理几帧的话已经足够实用
做静态图像提取类的工具时,从这里入手是相当自然的选择。
4.5. 但也有坑
这个自动转换具有下面这些特性:
| 项目 | 内容 |
|---|---|
| 转换目标 | 基本是 RGB32 |
| 实现方式 | 软件处理 |
| 适合的用途 | 少量帧、缩略图、离线处理 |
| 不适合的用途 | 基于 D3D 的实时渲染、大批量帧处理 |
| 相性不好的属性 | MF_SOURCE_READER_D3D_MANAGER、MF_READWRITE_DISABLE_CONVERTERS |
还有一点很重要,就是 RGB32 第 4 个 byte 的处理方式。Windows 的 RGB32 在内存中的排列是 Blue / Green / Red / Alpha or Don’t Care,并不是 ARGB32。如果要以 32bppBGRA 的形式交给 WIC,在保存前把第 4 个 byte 填成 0xFF 使其不透明 会更安全。
这一点在之前那篇静态图像提取的文章中也提到过,是容易踩的坑。
5. 模式 B:自己编写转换处理
5.1. 适合什么场景
自己实现转换适合下面这些场景:
- 需要处理大批量帧,想自己优化转换过程
- 想把
NV12直接送给 GPU 或 SIMD 处理 - 想明确控制
BT.601/BT.709/ range - 想要
RGB32以外的输出格式 - Source Reader 的 limited 自动转换已经不够用
可以说这是一种 用自己承担处理量与色彩责任,来换取自由度 的模式。
5.2. 手动转换的整体流程
步骤如下:
- 让 Source Reader 输出
NV12或YUY2 - 用
GetCurrentMediaType获取实际的 subtype 与属性 - 确认
MF_MT_FRAME_SIZE、MF_MT_DEFAULT_STRIDE、MF_MT_YUV_MATRIX、MF_MT_VIDEO_NOMINAL_RANGE - 从 sample 中取出 buffer 并 lock
- 求出每个像素需要参照的 Y/U/V
- 应用矩阵,写入 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;
}
这里特意做得比较 严格。Media Foundation 的枚举文档里虽然写着「Unknown 视为 BT.709」之类的说法,但实务上如果默默套用这种默认值,颜色跑偏会很难被察觉。至少在最初的实现里,遇到不支持的组合就直接报错 会更安全。
摄像头或 JPEG 系的 full-range 路径有时需要单独处理。这里没有默默地把两种情况一起接受,而是有意 明确收窄这段代码的适用前提。
5.5. 读取 buffer 要以 stride 为准
这一点也相当关键。
MF_MT_DEFAULT_STRIDE是 最小 stride- 实际的 sample buffer 有时会带有 包含 padding 的 actual stride
- 如果能用
IMF2DBuffer::Lock2D,就优先使用它
把 Microsoft Learn 的 Uncompressed Video Buffers 中给出的 helper 模式稍作整理,就变成下面这样。
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 / positive stride,但实际访问 buffer 时,直接使用 API 返回的 pitch 会更安全。如果在这里按 width 硬编码,之后很容易悄悄出错。
5.6. 把单个像素的转换公式写成代码
这里只处理 BT.601 和 BT.709 的 limited range,输出格式选用便于交给 WIC / GDI 的 BGRA32。
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
- BGRA 的第 4 个 byte 设为
255
5.7. 将 NV12 转换为 BGRA32
NV12 是 4:2:0 格式,2x2 像素块中的 4 个像素共用相同的 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 来处理。很多情况下视觉效果已经足够实用,但如果追求最高画质,按照 Microsoft Learn 的 YUV 文章所说,先做 4:2:0 -> 4:2:2 -> 4:4:4 的 upconversion,理论上会更干净。
5.8. 将 YUY2 转换为 BGRA32
YUY2 是 packed 的 4:2: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 排列,「每 2 个像素共用 U/V」的结构一目了然。这部分的 mental model 比 NV12 更容易建立。
5.9. 从 sample 调用的入口
最后,从 IMFSample 中取出连续的 buffer,再按 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 - 从
GetCurrentMediaType构造出DecodedFrameInfo 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 之后由应用程序自己转换,这是最容易理解的方式。
不过,如果想把转换嵌入 Media Foundation 的 pipeline 内部,还有其他设计:
- 编写自己的
MFT - 使用
Video Processor MFT/ XVP - 在 GPU 端编写
NV12-> RGB 的 shader
再往这个方向走,主题就有点不一样了,所以本文把重点放在应用程序端的代码上。不过,了解到在「交给 Media Foundation」与「全部自己处理」之间,还存在 Video Processor MFT 这个中间地带,会很有用。
6. 该选哪一种
犹豫不决时,下面这张表能帮你理清大部分问题。
| 视角 | 自动转换 (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
手动转换 |
|---|---|---|
| 实现速度 | ◎ | △ |
| 抽取几张静态图像 | ◎ | ○ |
| 大批量帧 / 实时 | △ | ◎ |
| 想明确控制 matrix / range | △ | ◎ |
| 想搭配 GPU / D3D | △ | ○〜◎ |
想要 RGB32 以外的输出 |
△ | ◎ |
| 理解原理 | ○ | ◎ |
作为第一个版本,可以这样考虑:
- 先让它跑起来 -> 自动转换
- 要为色彩或性能负责 -> 手动转换
实务上,「先用自动转换确认画面是对的,再换成 manual path」这种顺序也相当有效。一开始就把所有事情都背在自己身上,出问题时会很难判断是在哪个环节出的错。
7. 实务中容易踩的坑
7.1. 把 RGB32 当成带 alpha 的 RGBA
RGB32 在内存中的排列是 B, G, R, Alpha or Don't Care。如果直接把它当成 BGRA 存成 PNG,第 4 个 byte 有可能是 0,导致图像变透明。保存前把它填成 0xFF 会更安全。
7.2. 用 width * bytesPerPixel 硬算 stride
这是相当常见的失误。实际的 sample buffer 有可能带 padding,跨行移动时应该使用 actual stride,这才是正确做法。
7.3. 把 MF_MT_DEFAULT_STRIDE 和 actual pitch 混为一谈
MF_MT_DEFAULT_STRIDE 指的是「该格式用连续内存表示时的最小 stride」。sample buffer 的 actual pitch,应该优先使用 IMF2DBuffer::Lock2D 返回的值。
7.4. 不检查 color metadata 就默默猜测 601 / 709
颜色方面的问题不容易被发现,也不会导致崩溃,所以格外麻烦。
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
至少要检查一下这两项,并且抱着 自己代码不支持的值就直接报错 的态度去实现,会比较合适。
7.5. 用 width * height 来切分 NV12 的 UV plane
plane 的 offset 是由 实际的 stride 与 height 决定的,并不是 width * height。如果在这里草率处理,颜色会跑偏,图像也可能损坏。
7.6. 把 interlaced 视频当作 progressive 处理
本文的手动示例是以 progressive 为前提的。如果把 interlaced 视频直接当成 1 个 field 来读取,可能会出现梳状噪点。如果需要 deinterlace,把 Source Reader 的自动 video processing 或 Video Processor MFT 纳入考虑会更自然。
7.7. 忽视 4:2:0 的 chroma upsampling 质量
本文中 NV12 的转换以「易于理解」为优先,直接把共用的 chroma 用在每个像素上。视用途而定,这样通常已经够用,但如果要优先保证画质,还是应该参考 YUV 推荐格式资料中提到的 upconversion 思路。
8. 小结
在 Media Foundation 中把 YUV 转换为 RGB 时,先把下面这些整理装进脑子里,能减少不少迷茫。
- decoder 后面输出的通常不是 RGB,而是
NV12或YUY2 - 想省事就 通过
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING请求RGB32 - 想要控制就 接收
NV12/YUY2,自己转换成 BGRA - 在 manual path 上,先搞清楚 采样 / range / matrix / stride,公式反而不是最先要担心的
- 如果对
BT.601/BT.709、16..235、4:2:0/4:2:2含糊处理,就会出现色偏或画面损坏
YUV -> RGB 一开始确实有点难上手。但只要脑子里装进:
NV12是 2x2 像素共用 U/VYUY2是横向 2 个像素共用 U/V- 把这组 U/V 和 Y 一起应用矩阵
这幅画面,事情就会变得相当直接。那一堆看起来像宇宙乱码的字节,也会开始变成有意义的像素。
9. 参考资料
本文的示例代码
KomuraSoft 相关文章
- Media Foundation 到底是什么 - 为什么处处能看到 COM 与 Windows 媒体 API 的影子
- 用 Media Foundation 从 MP4 视频指定时间点提取静态图像的方法 - 可直接粘贴进 .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
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 Media Foundation 向 MP4 视频帧叠加图片与文字的方法
本文整理如何用 Media Foundation 向 MP4 视频的每一帧叠加图片与文字并生成新的 MP4:明确 Source Reader、绘制、色彩转换、Sink Writer 各自的职责分工,并给出可直接粘贴使用的单文件 C++ 示例。
用 Media Foundation 从 MP4 视频中提取指定时间点静止图像的方法
本文整理了使用 Source Reader 从 MP4 中取出接近指定时间点的帧,并处理好 stride 与 RGB32 的 alpha 后保存为 PNG 的实现步骤。
共享内存的陷阱与实务最佳实践
整理在实务中使用共享内存时的陷阱,以及涵盖同步、可见性、生命周期、ABI、权限等降低事故率的设计方法。
Media Foundation 入门:用 COM 视角理解 API
本文按照最先需要掌握的顺序,结合 COM、HRESULT、IMFSourceReader、MFT 等 Windows 媒体 API 的基础术语,整理 Media Foundation 到底是什么。
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
常见问题
汇总了咨询这一主题时常见的问题。
- Media Foundation 的解码器为什么输出 YUV 而不是 RGB?
- 因为人眼对亮度细节的敏感度高于对色彩细节的敏感度,所以视频格式普遍采用「Y(偏亮度的成分)保留精细,U/V(色差成分)适当粗化」的设计来节省带宽。因此在 Windows 的视频处理链路中,decoder 输出的未压缩帧通常是 NV12、YUY2 这类 YUV 系格式。另外,在数字视频的语境下,YUV 其实基本等同于 Y'CbCr,这样理解会更容易梳理。
- 最简单获得 RGB 帧的方法是什么?
- 在 IMFSourceReader 上启用 MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING,并请求 MFVideoFormat_RGB32。如果只是抽取几张静态图像或生成缩略图,这是最省事的做法。但这个自动转换是纯软件处理,并未针对实时播放做优化,如果要做大批量处理或者需要控制色彩细节,还是应该直接接收 YUV 并自己转换。
- 自己把 YUV 转换为 RGB 时要注意什么?
- 不是乘上 3 个系数就结束了,实际上会牵涉色度子采样(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 之后紧跟着 U、V 交替排列的 UV plane,2x2 像素块共用一组 U/V。YUY2 是 4:2:2 格式,横向相邻的 2 个像素共用一组 U/V。两者都是实务中常见的格式,区别在于色度的抽样方式不同。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。