在 Media Foundation 中把 YUV 畫面轉成 RGB 的方法 - 從原理整理 Source Reader 自動轉換與自行轉換
想從影片抽一張畫面存成 PNG、丟給 WIC 或 GDI,或是顯示到 UI 上。這些場景下,應用程式端都會想要 RGB 的像素陣列。
可是從 Media Foundation 的解碼器出來的畫面,其實常常是 NV12 或 YUY2 這種 YUV 系列格式。這時候把那一堆原始 bytes 當成影像直接處理,就會看到色彩壞掉、出現條紋、甚至整張畫面帶點奇怪的綠意,有點令人心酸的結果。
之前寫過的 Media Foundation 是什麼 - 為什麼看得到 COM 與 Windows 媒體 API 的影子 整理過整體概念,而 用 Media Foundation 從 MP4 影片指定時間點抽出靜止畫面的方法 - 可直接貼進 .cpp 的單檔完整版 則整理了靜止畫面抽取。這次要處理的,正是那中間的 YUV -> RGB 轉換本身。
本文把下列 2 種做法分開整理:
- 做法 A:交給
IMFSourceReader自動轉到 RGB32 - 做法 B:自己接住
NV12/YUY2,自行轉成 RGB
目的不是背 API 名稱。重點是 能在腦中畫出 Media Foundation 中哪裡冒出 YUV、哪裡變成 RGB 的那條流程。
1. 先說結論
先把結論整理好,大致如下。
- 抽幾張靜止畫面或做縮圖 的話,啟用
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING、要求MFVideoFormat_RGB32是最省事的做法 - 不過這個自動轉換是 純軟體處理,並沒有針對即時播放做最佳化
- 要自己寫轉換,先把
NV12與YUY2搞懂 是最短路徑 - YUV -> RGB 不是「乘上 3 個係數就搞定」,實際上會牽涉 色度次取樣、range、矩陣、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 這類壓縮格式,解碼器會先把它還原成 未壓縮畫面。這裡的未壓縮畫面不一定是 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 |
實務中最常見的 2 種,先記住它們的形狀會輕鬆很多。
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 個 pixel 換 1 個 pixel」那麼單純。
首先必須想清楚 共用的 U/V 該怎麼分配給哪個 pixel。
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 擴展成每個 pixel 都查得到的形式 - 把 range 還原
影片的 Y 通常是 16..235、U/V 通常是 16..240,要把這個縮放還原 - 套用矩陣
以BT.601或BT.709等係數轉成 RGB
也就是說,YUV -> RGB 轉換在實務上就是要決定:
- 該 pixel 的色彩對應到哪個 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 張靜止畫面
- 想做幾張縮圖
- 想把影像交給 WIC 當成 RGB 影像處理
- 不是即時播放,是批次或工具用途
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 |
| 實作 | 純軟體處理 |
| 適用情境 | 少量 frame、縮圖、離線處理 |
| 不適用情境 | 以 D3D 為基礎的即時 rendering、大量 frame 處理 |
| 相性差的屬性 | 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. 什麼情境合適
自行轉換適合以下情境:
- 要處理大量 frame,想自己做最佳化
- 想直接把
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
- 找出每個 pixel 要用的 Y/U/V
- 套用矩陣、寫到 BGRA
本文的程式碼把範圍縮成 8-bit SDR / progressive / NV12 or 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,而且 只允許矩陣是 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。
Media Foundation 的 enum 文件雖然寫著「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 pattern 稍作整理就變成下面這樣。
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. 把 1 pixel 的轉換式寫成程式碼
這邊只處理 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 - 乘上對應矩陣的係數
- 結果 clip 到 0..255
- BGRA 的第 4 個 byte 設
255
5.7. 把 NV12 轉成 BGRA32
NV12 是 4:2:0,2x2 區塊的 4 個 pixel 共用相同的 U/V。
最底限的實作中,就直接把那組共用 chroma 用在 4 個 pixel 上會最清楚。
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 個 pixel 共用一組 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 的 bytes 排成 Y0 U Y1 V,「每 2 個 pixel 共用 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) |
自行轉換 |
|---|---|---|
| 實作速度 | ◎ | △ |
| 抽幾張靜止畫面 | ◎ | ○ |
| 大量 frame / 即時 | △ | ◎ |
| 想明確控制矩陣 / 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,跨 row 移動要用 actual stride 才是正確做法。
7.3. 搞混 MF_MT_DEFAULT_STRIDE 與 actual pitch
MF_MT_DEFAULT_STRIDE 是「該 format 在連續記憶體裡表示時的最小 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 video 當 progressive 處理
本文的 manual 範例是以 progressive 為前提。把 interlaced 當成 1 field 直接讀,可能會出現梳狀雜訊。
需要 deinterlace 時,把 Source Reader 的自動 video processing 或 Video Processor MFT 納入視野比較自然。
7.7. 忽略 4:2:0 的 chroma upsampling 品質
本文的 NV12 轉換以「方便理解」為主,直接把 shared chroma 套用在每個 pixel。用途不同時這樣就夠,若要畫質優先,還是得看 YUV 推薦格式文件裡的 upconversion 思路。
8. 小結
在 Media Foundation 中把 YUV 轉成 RGB,先抓住下列整理能減少很多迷路。
- decoder 後面跑出來的常常不是 RGB,而是
NV12或YUY2 - 想省事就用
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING要求RGB32 - 想控制就接下
NV12/YUY2自己轉成 BGRA - manual path 先抓住 取樣 / range / 矩陣 / stride,再看公式
BT.601/BT.709、16..235、4:2:0/4:2:2含糊帶過,就會出現色偏或畫面壞掉
YUV -> RGB 一開始有點難上手。
但只要腦中裝進:
NV12是 2x2 共用 U/VYUY2是每 2 個 pixel 共用 U/V- 把 U/V 和 Y 套上矩陣
這個畫面,其他的都會變得自然。那一串像是宇宙亂碼的 bytes,就會變成有意義的像素。
9. 參考資料
合同会社小村ソフト 的相關文章
- 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 影片每一幀的方法 - 整理 Source Reader / 繪製 / 色彩轉換 / Sink Writer 與可直接貼進 .cpp 的單檔完整版
整理用 Media Foundation 把圖片與文字烙印到 MP4 每一幀的整體流程,把 Source Reader 解碼、GDI+ 繪製、BGRA 與 NV12 的色彩轉換、Sink Writer 重新編碼四段拆開來看,並附上可直接貼進 Visual Studio 主控...
用 Media Foundation 從 MP4 影片指定時間點抽出靜止畫面的方法 - 可直接貼進 .cpp 的單檔完整版
整理用 Media Foundation 的 Source Reader 從 MP4 抽取最靠近指定時間靜止畫面的做法,涵蓋 SetCurrentPosition 非精準 seek、ReadSample 可能回 NULL、stride 與影像上下方向、RGB32 第 4 b...
Media Foundation 是什麼 - COM 和 Windows 媒體 API 的臉浮現的原因
整理 Media Foundation 為何讓人覺得像 COM:其本體是媒體處理平台,但 source、transform、sink、callback 之間的邊界以 COM 介面表現,因此 HRESULT、GUID、apartment 自然浮現,並指出 Source Rea...
使用共享記憶體時的陷阱與最佳實踐 - 先整理同步、可見性、壽命、ABI、安全性
整理在同一機器內以共享記憶體交換大型資料時的陷阱與設計要點。把 control plane 和 data plane 分離、縮小並行模型、用固定寬度整數和標頭設計 ABI、以 offset 取代指標、明示 commit protocol、為當機復原放入 generation...
用 Native AOT 把 C# 做成原生 DLL 的方法 - 用 UnmanagedCallersOnly 從 C/C++ 呼叫
從現有 C/C++ 應用程式以 in-process 方式呼叫 C# 邏輯時,本文示範以 Native AOT 將類別庫發佈為原生 DLL,並用 UnmanagedCallersOnly 公開 cdecl 進入點。透過 handle、錯誤碼與扁平 C ABI 設計交界面,整...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。