Media FoundationでYUVをRGBに変換する方法

· 更新日: · · Media Foundation, C++, Windows開発, 動画処理, YUV

更新履歴(5件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
検証手順で、赤が255ではなく254になる理由を「係数を小数6桁に丸めているため」と説明していたのを直しました。これは誤りで、無限精度の係数で計算しても254.44になります。原因は入力のY/U/Vが既に丸められた整数であることで、理屈上の赤のYは81.481、これを81として保存した時点で失われる0.481が、戻すときに0.481×1.164383≒0.56の目減りになります。あわせて、最終的な整数化の方針(この記事の`ClampToByte`は四捨五入)も結果を左右することを明記し、許容誤差の理由を「標本化で端数が落ちている」「整数化の方針が実装ごとに違う」の2点に整理しました。この2つで説明が付かない差は本物の不具合で、係数の精度ではなく変換の前提を疑うべきことも追記しています。
正しく変換できたかを確かめる節を新設しました。記事内の式から手計算した既知値の表(係数を小数6桁に丸めているため赤は255ではなく254になることを明示しています)と、差の出かたから原因を引く表を載せています。実測値は書いていません。レイアウト図にstrideとplane境界を描き足し、strideとpitchが同義であることを初出で断り、前提環境の表を追加しました。
本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589644)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Media FoundationでYUVをRGBに変換する方法」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589644 https://staging.comcomponent.com/blog/2026/03/15/002-media-foundation-yuv-to-rgb-conversion-patterns/

DOI(最新版)
10.5281/zenodo.21589644
DOI(この版)
10.5281/zenodo.21732675

動画からフレームを抜いて PNG に保存したい、WIC や GDI に渡したい、あるいは UI 上へ出したい。そういう場面では、アプリ側は RGB の画素列を欲しがります。

ところが、Media Foundation の decoder から出てくるフレームは、かなり普通に NV12YUY2 のような YUV 系フォーマットです。ここで生のバイト列をそのまま画像だと思って扱うと、色が壊れる、縞になる、妙に緑がかる、という少し悲しい絵になります。

以前書いた Media Foundation入門 - COM視点でAPIを理解する では全体像を、Media FoundationでMP4の指定時刻から静止画を切り出す方法 では静止画抽出を整理しました。今回はその途中にある 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)

動かすための前提環境

この記事のコードを自分のプロジェクトへ持っていくなら、必要なのは次だけです。

項目 前提
OS Windows 10 以降
コンパイラ Visual Studio 2019 / 2022 の MSVC(C++17)
SDK Windows SDK(Media Foundation のヘッダーとインポートライブラリ)。Visual Studio の「C++ によるデスクトップ開発」ワークロードに含まれます
ビルド サンプルは CMake 3.20 以降。Visual Studio のプロジェクトを手で作っても構いません

リンクするライブラリは 4 つです。記事のコードでは #pragma comment(lib, ...) で書いていますが、プロジェクト設定側で指定しても同じです。

  • mfplat.lib
  • mfreadwrite.lib
  • mfuuid.lib
  • ole32.lib

なお、この記事のコードは CoInitializeExMFStartup が済んでいる前提です。1 pixel の変換式(5.6.)だけは OS 非依存なので、GitHub のサンプルでは別ヘッダーへ切り出してあり、Linux 上の g++ でもテストできるようにしています。

1. まず結論

先に結論だけまとめると、こうです。

  • 数枚の静止画抽出やサムネイル生成なら、MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING を有効にして MFVideoFormat_RGB32 を要求するのがいちばん楽です
  • ただしこの自動変換は software 処理 で、リアルタイム再生向けには最適化されていません
  • 自前変換を書くなら、まずは NV12YUY2 をきちんと理解する のが最短です
  • YUV -> RGB は「係数 3 本かければ終わり」ではなく、実際には サブサンプリング、range、matrix、stride が絡みます
  • Media Foundation のドキュメントでは広く YUV という言葉を使いますが、デジタル video では実質的に Y’CbCr を指していると思って読むと整理しやすいです
  • 実務で色を壊しやすいのは、MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE を見ないこと、そして stride を width * bytesPerPixel だと思い込むこと です

要するに、楽をしたいなら Source Reader に RGB32 を出させる大量処理や色の制御まで欲しいなら YUV のまま受けて自分で変換する。この二択です。

この記事の知識マップ

この記事は、Media Foundationのdecoderから出る非圧縮フレームがRGBではなくNV12やYUY2のようなY’CbCr系であることを起点に、YUV→RGB変換の2つの経路を整理している。Source ReaderでMF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGを有効にしてRGB32を要求する自動変換は手軽だがsoftware処理でリアルタイム再生には向かず、大量処理や色の制御が必要ならNV12/YUY2を自分で受けてBGRAへ変換する。自前変換ではchroma subsamplingの復元に加え、IMFMediaTypeが持つMF_MT_YUV_MATRIX(BT.601/BT.709)とMF_MT_VIDEO_NOMINAL_RANGEを見て係数とrangeを正しく選び、strideをwidth×bytesPerPixelで決め打ちせずIMF2DBuffer::Lock2Dが返す実際の値を使う必要がある。

Media FoundationでのYUV→RGB変換の知識マップMedia Foundationのdecoderが出すNV12/YUY2などYCbCr系フレームをRGBへ変換する2つの経路と、chroma subsampling・matrix・nominal range・strideという実装上の落とし穴の関係を示す図前提とするで構成できるで構成できる利用する利用する利用する利用する利用する実装を担う利用する推奨される対応前提とするで確認できる推奨される対応利用する利用するで構成できる前提とする利用する利用する利用する利用する利用するYUV→RGB変換NV12ピクセル形式YUY2ピクセル形式クロマサブサンプリング(4:4:4/4:2:2/4:2:0)MF_MT_YUV_MATRIX属性MF_MT_VIDEO_NOMINAL_RANGE属性Y'CbCrIMFSourceReader(Source Reader)MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGMFVideoFormat_RGB32Video Processor MFTstride(画像の行送りバイト数)IMF2DBuffer::Lock2DIMFMediaTypeIMFAttributesBT.601BT.709Media Foundation

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全23件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. まず絵で見る

最初に、Media Foundation の中で何が起きているかを図で見たほうが話が速いです。

パターンAパターンBMP4 / H.264 / HEVCdecoderNV12 / YUY2 / YV12 などの YUV フレームSource Reader の video processingRGB32自前の変換コードBGRA / RGB

動画ファイルの中身が H.264 や HEVC のような圧縮形式なら、まず decoder がそれを 非圧縮フレームへ戻します。この非圧縮フレームが RGB とは限りません。むしろ、Windows の video 系では YUV 系が普通です。

なので、アプリが RGB を欲しがるときは、次のどちらかを選びます。

  1. Media Foundation 側に RGB32 まで持っていかせる
  2. YUV を受けて、自分のコードで RGB にする

この記事の話は、まさにこの分岐点です。

3. YUV と RGB の関係を先に整理する

3.1. YUV と言いつつ、実際には Y’CbCr の話

Windows の API 名やドキュメントは広く YUV という言葉を使います。ただ、デジタル video の文脈では、UCbVCr と読んでほぼ問題ありません。

ざっくり言うと、

  • 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

実務でよく出る 2 つだけ、まず形を見ておくとかなり楽です。

ここで先に 1 つ用語を固めます。stride(別名 pitch)は、1 行分のバイト数です。画像の横幅そのものではなく、行末の padding を含めた「次の行の先頭まで何バイト進むか」を表します。この記事では stridepitch を同じ意味で使います。Microsoft Learn 側も両方の言い方が出てくるので、読み分けの必要はありません。

以下の図では、W を width、H を height、S を stride とします。S >= W であって、S == W とは限らないのが要点です。

NV12 (4:2:0, planar) / width = W, height = H, stride = S

  <----------- S バイト ----------->
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (padding)           |    |
 | Y Y Y Y Y | (padding)           |    | Y plane
 | Y Y Y Y Y | (padding)           |    | S * H バイト
 | Y Y Y Y Y | (padding)           |    |
 +-----------+---------------------+  --+  <- plane 境界 = 先頭から S * H
 | U V U V U | (padding)           |    |
 | U V U V U | (padding)           |    | UV plane
 +-----------+---------------------+  --+  高さは H / 2 行

  行 y の Y     : yPlane  + S * y
  行 y の UV    : uvPlane + S * (y / 2)
  UV plane 先頭 : scanline0 + S * H

NV12 では、2x2 block の 4 画素が 1 組の U/V を共有します。Y は各 pixel ごとにあります。 UV plane は Y plane と同じ stride を使いますが、行数は半分です。だから plane 境界は S * H であって、W * H ではありません(7.5. で改めて扱います)。

YUY2 (4:2:2, packed) / width = W, height = H, stride = S

  <-------------- S バイト ---------------->
  <------- W * 2 バイト ------->
 +-----------------------------+----------+
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (padding)|   行 0
 | Y0 U0 Y1 V0  Y2 U2 Y3 V2 …  | (padding)|   行 1
 +-----------------------------+----------+

  行 y の先頭   : scanline0 + S * y
  2 pixel = 4 バイト (Y, U, Y, V)
  plane は 1 つだけ (packed なので境界がない)

YUY2 では、横 2 画素が 1 組の U/V を共有します。Y0Y1 は別ですが、U0V0 は共有です。 packed なので plane 境界の計算は不要ですが、行の移動にはやはり stride を使います

この時点で見えてくるのは、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 層に分けると理解しやすいです。

  1. サブサンプリングを戻す 4:2:0 や 4:2:2 の U/V を、各 pixel が参照できる形に広げる
  2. range を戻す video の Y はふつう 16..235、U/V は 16..240 を使うので、そのスケーリングを戻す
  3. matrix をかける BT.601BT.709 などの係数で RGB へ変換する

つまり、YUV -> RGB 変換とは実務的には、

  • どの U/V がその pixel の色なのか
  • その Y/U/V をどの係数で RGB に戻すのか

を決める処理です。

3.4. BT.601 と BT.709 を雑に扱うと、じわっと色がズレる

Media Foundation のドキュメントでは、BT.601 は SDTV とそれ以下、BT.709 は SD を超える video で優先される関係として説明されています。

ただ、ここで「解像度が大きいから 709 だろう」と 黙って推測する のはあまりよくありません。色ずれはクラッシュしないので、気付かないまま運用に乗りやすいからです。

Media Foundation では色空間情報をメディアタイプ属性で持てます。最低でも次の 2 つは見ます。

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

この 2 つを見て、自分のコードが対応している組み合わせだけを明示的に通す ほうが、あとで静かに事故りにくいです。

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 を使って YUV -> RGB32 の limited な video processing をさせる機能があります。

ただし Microsoft Learn にもある通り、これは software 処理 で、playback 向けに最適化されていません。何百枚も毎秒処理したいなら、ここに寄りかかるのは少し違います。

4.2. 何を設定すると RGB32 が出てくるのか

流れはかなり素直です。

  1. MFCreateSourceReaderFromURL に渡す attributes で MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. 動画 stream を選ぶ
  3. SetCurrentMediaTypeMFMediaType_Video / MFVideoFormat_RGB32 を要求する
  4. ReadSample で sample を読む

これだけで、decoder の後ろに入る limited な video processing が YUV -> RGB32 をやってくれます。

4.3. コード

以下のコードは、CoInitializeExMFStartup が済んでいる前提です。最小構成だと、だいたいこんな形です。

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <wrl/client.h>

#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")

using Microsoft::WRL::ComPtr;

HRESULT CreateSourceReaderWithAutoRgb(
    const wchar_t* path,
    IMFSourceReader** ppReader)
{
    if (!path || !ppReader) return E_POINTER;
    *ppReader = nullptr;

    ComPtr<IMFAttributes> attrs;
    HRESULT hr = MFCreateAttributes(&attrs, 2);
    if (FAILED(hr)) return hr;

    hr = attrs->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
    if (FAILED(hr)) return hr;

    hr = MFCreateSourceReaderFromURL(path, attrs.Get(), ppReader);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr)) return hr;

    ComPtr<IMFMediaType> outType;
    hr = MFCreateMediaType(&outType);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        outType.Get());
    if (FAILED(hr)) return hr;

    return S_OK;
}

HRESULT ReadOneRgb32Sample(
    IMFSourceReader* reader,
    IMFSample** ppSample,
    LONGLONG* pTimestamp100ns)
{
    if (!reader || !ppSample) return E_POINTER;
    *ppSample = nullptr;
    if (pTimestamp100ns) *pTimestamp100ns = 0;

    DWORD streamIndex = 0;
    DWORD flags = 0;
    LONGLONG timestamp = 0;

    HRESULT hr = reader->ReadSample(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        0,
        &streamIndex,
        &flags,
        &timestamp,
        ppSample);

    if (FAILED(hr)) return hr;
    if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
    if (*ppSample == nullptr) return MF_E_INVALID_STREAM_DATA;

    if (pTimestamp100ns) *pTimestamp100ns = timestamp;
    return S_OK;
}

このあと GetCurrentMediaType を呼べば、実際の出力 size や stride を確認できます。

4.4. この方法の強み

この方法の良さは、とにかく 早く正しい絵に近づける ことです。

  • 4:2:0 / 4:2:2 の展開を自分で書かなくてよい
  • matrix / deinterlace の面倒をかなり隠せる
  • WIC や GDI に渡しやすい
  • 数フレーム処理なら十分に実用的

静止画抽出系のツールでは、まずここから入るのがかなり自然です。

4.5. ただし落とし穴もある

この自動変換には、次の性質があります。

項目 内容
変換先 基本は RGB32
実装 software 処理
向いている用途 少数 frame、サムネイル、オフライン処理
向いていない用途 D3D ベースの real-time rendering、大量 frame 処理
相性の悪い属性 MF_SOURCE_READER_D3D_MANAGERMF_READWRITE_DISABLE_CONVERTERS

そしてもう 1 つ大事なのが、RGB32 の 4 byte 目の扱いです。 Windows の RGB32 は、メモリ上では Blue / Green / Red / Alpha or Don’t Care の並びです。ARGB32 ではありません。WIC へ 32bppBGRA として渡すなら、4 byte 目を 0xFF で埋めて不透明にする ほうが安全です。

ここは前回の静止画抽出記事でも踏みやすい点として触れました。

5. パターンB: 自分で変換処理を書く

5.1. どんなときに向いているか

自前変換が向いているのは、たとえばこんなケースです。

  • 大量 frame を処理するので、変換を自分で最適化したい
  • NV12 のまま GPU や SIMD へ流したい
  • BT.601 / BT.709 / range を明示的に扱いたい
  • RGB32 以外の出力フォーマットを作りたい
  • Source Reader の limited な自動変換では足りない

処理量や色の責任を自分で持つ代わりに、自由度を取りに行く パターンと言えます。

5.2. 自前変換の全体フロー

手順は次の通りです。

  1. Source Reader の出力を NV12YUY2 にする
  2. GetCurrentMediaType で実際の subtype と属性を取る
  3. MF_MT_FRAME_SIZEMF_MT_DEFAULT_STRIDEMF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE を確認する
  4. sample から buffer を取り出して lock する
  5. 各 pixel が参照する Y/U/V を求める
  6. matrix をかけて 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_NV12MFVideoFormat_YUY2 を渡します。

注意したいのは、要求した subtype がそのまま通るとは限らない ことです。実際に何が出てくるかは、GetCurrentMediaType で確認します。

5.4. 変換前に、対応している色情報だけを受け入れる

自前変換では、まずメディアタイプから最低限の情報を取ります。 この記事のサンプルでは、NV12 / YUY2 だけを受け入れ、かつ matrix は BT.601BT.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 ドキュメントには「UnknownBT.709 として扱う」といった記述もありますが、実務ではここを黙って丸めると、色ずれが気付きにくくなります。少なくとも最初の実装では、対応していない組み合わせはエラーにする ほうが安全です。

Unknown はどんなときに返ってくるのか

「厳しすぎないか」と思うかもしれないので、Unknown が出る経路を挙げておきます。だいたい 「元の映像が色情報を持っていない」 ケースです。

  • H.264 / HEVC の VUI に色情報が入っていない。規格上、colour_description_present_flag が 0 のとき matrix_coefficients は「未指定」として扱われます。この情報が無いまま decoder を通れば、下流に伝わる matrix も未指定になります
  • キャプチャデバイスや古いコンテナから来た生の YUV。色空間の記述を持たない経路です
  • そもそも MF_MT_YUV_MATRIX 属性自体が付いていないこともあります。この場合 GetUINT32 は値を返さず MF_E_ATTRIBUTENOTFOUND で失敗します(上のコードでは FAILED(hr) でそのまま弾いています)

ここで大事なのは、Unknown は「BT.709 だと分かっている」ではなく「分からない」 だという点です。SD 解像度の素材に 709 を当てれば色はずれますし、その逆もずれます。

そのうえで方針は 2 つに分かれます。

  • strict に弾く(この記事の方針): 対応外としてエラーを返し、上位で「この素材は非対応」と判断させる。色が静かにずれるより、扱えないと言い切るほうが安全です
  • 既定値を決めて通す: どうしても通す必要があるなら、Unknown のときにどう仮定したかを ログに残す。そのうえで「解像度で 601 / 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 access は API が返してきた stride(= pitch)をそのまま使う ほうが安全です。ここで width ベースに決め打ちすると、あとで静かに壊れます。

5.6. 1 pixel の変換式をコードにする

ここでは BT.601BT.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 ごとの係数をかける
  • 結果を 0..255 に clip する
  • BGRA の 4 byte 目は 255 にする

5.7. NV12 を BGRA32 へ変換する

NV12 は 4:2:0 なので、2x2 block の 4 pixel が同じ U/V を共有します。 最小実装としては、その shared 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;

    // UV plane の先頭は「stride × height」だけ進んだ位置。
    // width × height ではないことに注意 (3.2. の図を参照)
    const BYTE* uvPlane =
        scanline0 + static_cast<size_t>(actualStride) * info.height;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        // 行の移動は必ず stride 単位
        const BYTE* yRow = yPlane + static_cast<size_t>(actualStride) * y;

        // 4:2:0 なので UV は縦 2 行で 1 行を共有する -> y / 2
        // UV plane も Y plane と同じ stride を使う
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);

        // 出力側は padding なしの詰めた BGRA なので 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];

            // UV plane は [U, V] が交互に並ぶ。
            // 横 2 pixel で 1 組を共有するので、まず (x / 2) で「何組目か」を出し、
            // 1 組 = 2 バイトなので * 2 でバイト位置に直す。+0 が U、+1 が 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;
}

このコードは、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 で 1 組の 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 を使い回す」という構造がそのまま見えます。 この分、NV12 より mental model が作りやすいです。

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 を作る
  • NV12YUY2 を要求する
  • GetCurrentMediaType から DecodedFrameInfo を作る
  • ReadSample
  • ConvertSampleToBgra32

という流れにできます。

実際の呼び出し側は、たとえば次のようになります。

ComPtr<IMFMediaType> currentType;
HRESULT hr = reader->GetCurrentMediaType(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    &currentType);
if (FAILED(hr)) return hr;

DecodedFrameInfo info;
hr = GetStrictDecodedFrameInfo(currentType.Get(), &info);
if (FAILED(hr)) return hr;

DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> sample;

hr = reader->ReadSample(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    0,
    nullptr,
    &flags,
    &timestamp,
    &sample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (!sample) return MF_E_INVALID_STREAM_DATA;

std::vector<BYTE> bgra;
hr = ConvertSampleToBgra32(sample.Get(), info, bgra);
if (FAILED(hr)) return hr;

// bgra は top-down / 32bpp BGRA として扱える

5.10. 「自前変換を書く」ときの置き場所

ここまでのコードは、Source Reader のあとでアプリ側が変換する 形です。これがいちばん分かりやすいです。

ただ、Media Foundation のパイプラインの中へ差し込みたいなら、別の設計もあります。

  • 独自の MFT を書く
  • Video Processor MFT / XVP を使う
  • GPU 側で NV12 -> RGB shader を書く

このあたりまで行くとテーマが少し変わるので、今回はアプリ側コードに絞りました。 ただ、「Media Foundation に任せる」と「全部アプリでやる」の間に Video Processor MFT という中間地点がある、ということは知っておくと便利です。

5.11. 正しく変換できたかを確かめる

色の事故は目に見えにくいので、「動いた」と「正しい」を分けて確認します。順番は次の 2 段です。

第 1 段: 既知の値を入れて手計算と突き合わせる

いきなり動画を流すより、ConvertLimitedYuvPixelToBgra に既知の Y/U/V を渡すほうが確実です。動画ファイルも Media Foundation も要りません。

BT.601 の limited range で、代表的な色の Y/U/V と、5.6. の式に入れたときの期待値はこうなります。

Y U V 期待される R G B
16 128 128 0 0 0
235 128 128 255 255 255
81 90 240 254 0 0
41 240 110 0 0 255

たとえば赤なら、C = 81 - 16 = 65D = 90 - 128 = -38E = 240 - 128 = 112 を式に入れて、

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

となります。出力は BGRA 順なので、バイト列としては 00 00 FE FF です。

ここで 赤が 255 ではなく 254 になっている点が大事です。理由は係数の精度ではありません。入力の Y/U/V が、既に丸められた整数だからです。

理屈上の赤 (255, 0, 0)BT.601 の limited range へ落とすと、Y = 16 + 219 × 0.299 = 81.481U = 90.203V はちょうど 240 になります。8bit の標本として保存する時点で、この端数が消えて Y = 81 になります。失われた 0.481 が、戻すときに 0.481 × 1.164383 ≒ 0.56 の目減りになります。255 − 0.56 = 254.44 ── 上の 254.44 はここから出た数字です。係数を無限精度にしても 254.44 のままで、6 桁への丸めが効いてくるのは小数点以下 4 桁より下、8bit の出力には現れません。

最後にどう整数へ落とすかも、結果を左右します。5.6. の ClampToByte[0, 255] に収めたうえで value + 0.5 を切り捨てる、つまり四捨五入です。単純な切り捨て(static_cast<BYTE>(value))にすると、この赤は同じ 254 ですが、青の B = 255.04 や赤の R = 0.38 のように境界に寄った値で 1 ずれます。他の実装と突き合わせる前に、相手がどちらなのかを確かめてください。

つまり、±1〜2 の差を許容する理由は「係数の精度が違うから」ではなく、「標本化で端数が落ちているから」と「整数化の方針が実装ごとに違うから」の 2 つです。逆に言うと、この 2 つで説明が付かない差は本物の不具合です。赤が 250 になる、赤と青が入れ替わる、暗部だけ浮く ── こうした差は、係数の精度ではなく変換の前提(BT.601BT.709 の取り違え、full range と limited range の取り違え、UV の入れ替え、stride の読み違い)を疑ってください。ここを「精度の問題」で片付けると、直せる不具合を見逃します。

テストとして書くなら、この形で十分です。

#include <cstdlib>  // std::abs

// 期待値との差が tolerance 以内かを見る。
// 期待値には「理屈上の色」(赤なら 255, 0, 0) を書く。標本化で落ちた端数と、
// 整数化の方針の違いを tolerance で吸収する。係数の精度は理由ではない
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;  // alpha は必ず不透明
}

// 使い方 (BT.601 limited range)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0);      // 黒
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // 白
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0);     // 赤 (式に入れると R=254)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255);    // 青

BT.709 でも同じことができます。係数が違うので Y/U/V の値も変わります。たとえば BT.709 の赤は Y=63, U=102, V=240 です。601 の値をそのまま 709 の分岐に流すと色がずれるので、行を分けてテストしておくと、matrix の取り違えをその場で捕まえられます。

GitHub のサンプルでは、この 1 pixel 変換だけを OS 非依存のヘッダーに切り出してあるので、Windows がなくてもこのテストは動きます。

第 2 段: パターンA とパターンB の出力を突き合わせる

1 pixel が合ったら、次はフレーム全体です。同じ動画の同じ時刻から、

  1. パターンA(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32
  2. パターンB(NV12 / YUY2 を受けて自前変換)

の 2 経路で 1 枚ずつ取り出し、pixel ごとに比較します。

差分の見方:
  各 pixel の |A.R - B.R|, |A.G - B.G|, |A.B - B.B| を取る
  最大値と、閾値を超えた pixel の割合を出す

ここで 完全一致は期待しないでください。 理由は 2 つあります。

  • Source Reader 側の video processing が、chroma upsampling を nearest-neighbor 以外の方法で行っている可能性があります。5.7. の自前実装は共有 chroma をそのまま 4 pixel に使う最小実装なので、エッジ部分ほど差が出ます
  • 丸めと中間精度の扱いが違います

なので見るべきは「一致するか」ではなく、差の出かたのパターンです。

見えた差 疑うところ
平坦な部分は一致、色の境界だけ差が出る chroma upsampling の違い。想定内
全体が一様にずれる matrix (601 / 709) か range (16..235 / 0..255) の取り違え
縞になる、斜めにずれる stride の決め打ち。7.2. と 7.5.
赤と青が入れ替わる BGRA と RGBA の取り違え
全体が透明 / 真っ黒に見える 4 byte 目を 0xFF で埋めていない。7.1.

差分の「形」を見れば、どこを疑えばよいかはかなり絞れます。全体が一様にずれているなら式か色情報、局所的なら添え字か stride です。

6. どちらを選ぶべきか

迷ったときは、次の表でかなり整理できます。

観点 自動変換 (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) 自前変換
実装の速さ
数枚の静止画抽出
大量 frame / real-time
matrix / range を明示制御したい
GPU / D3D と組みたい ○〜◎
RGB32 以外の出力が欲しい
原理の理解

最初の 1 本としては、こう考えると楽です。

  • まず動かしたい -> 自動変換
  • 色や性能の責任を持ちたい -> 自前変換

実務では、「まず自動変換で正しい絵を確認し、そのあと 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 が返す値を優先します。 (pitchstride の別名です。3.2. で触れたとおり、この記事では同じ意味で使っています。)

7.4. color metadata を見ずに 601 / 709 を黙って推測する

色の事故は目に見えにくいです。クラッシュもしません。だからやっかいです。

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

は、少なくとも見ましょう。 そして、自分のコードが対応していない値はエラーにする くらいの気持ちでちょうどいいです。

7.5. NV12 の UV plane を width * height で切ってしまう

plane offset は、実際の stride と height で決まります。width * height ではありません。 ここを雑にやると、色がずれたり、画像が壊れたりします。

7.6. interlaced video を progressive 前提で処理する

この記事の manual sample は 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 ではなく NV12YUY2 が普通に出る
  • 楽をしたいなら MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGRGB32 を要求する
  • 制御したいなら NV12 / YUY2 を受けて自分で BGRA へ変換する
  • manual path では、式より前に sampling / range / matrix / stride を押さえる
  • BT.601 / BT.70916..2354:2:0 / 4:2:2 を曖昧にすると、色ずれや壊れた絵になる

YUV -> RGB は、最初は少し取っつきにくいです。 でも一度、

  • NV12 は 2x2 で U/V を共有
  • YUY2 は横 2 pixel で U/V を共有
  • その U/V と Y に matrix をかける

という像が頭に入ると、かなり素直になります。宇宙色の謎バイト列が、ちゃんと意味のある画素に見えてきます。

9. 参考資料

この記事のサンプルコード

KomuraSoft の関連記事

Microsoft Learn

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Media FoundationのデコーダーはなぜRGBではなくYUVを出すのですか?
人間の目は色の細かさより明るさの細かさに敏感なため、videoではY(明るさ寄りの成分)を細かく、U/V(色差成分)を粗く持つ設計が効くからです。そのためWindowsのvideo系では、decoderから出る非圧縮フレームはNV12やYUY2のようなYUV系フォーマットが普通です。なお、デジタルvideoの文脈ではYUVは実質的にY'CbCrを指していると読むと整理しやすいです。
一番簡単にRGBフレームを得る方法は何ですか?
IMFSourceReaderでMF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGを有効にし、MFVideoFormat_RGB32を要求する方法です。数枚の静止画抽出やサムネイル生成ならこれがいちばん楽です。ただしこの自動変換はsoftware処理で、リアルタイム再生向けには最適化されていないため、大量処理や色の制御まで欲しい場合は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ブロックの4画素が1組のU/Vを共有します。YUY2は4:2:2のフォーマットで、横方向に2画素がU/Vを共有します。どちらも実務でよく出るフォーマットで、色の間引き方(サブサンプリング)が異なります。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る