كيفيّة تحويل إطارات YUV إلى RGB باستخدام Media Foundation - أنماط التحويل التلقائيّ في Source Reader والتحويل اليدويّ

· · Media Foundation, C++, تطوير Windows, معالجة الفيديو, YUV

عندما يريد تطبيق ما حفظ إطار بصيغة PNG، أو تمرير صورة إلى WIC أو GDI، أو عرض محتوى فيديو في واجهة المستخدم، فإنّ ما يحتاجه عادةً هو مخزّن بكسل بصيغة RGB.

لكنّ الإطارات التي تخرج من decoder في Media Foundation تكون في الغالب من عائلة صيغ YUV مثل NV12 أو YUY2. فإذا عُومِلت تلك البايتات الخامّ كما لو كانت بكسلات RGB جاهزة، فإنّ النتيجة عادةً ما تكون صورة معطوبة: ألوان خاطئة، أو خطوط، أو مسحة خضراء مريبة.

المقال السابق ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد غطّى الشكل الأوسع لـ Media Foundation.
كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++ ركّز على استخراج الصور الثابتة.
هذا المقال يقع بينهما ويركّز على تحويل YUV إلى RGB بحدّ ذاته.

ثمّة نمطان عمليّان:

  • النمط A: السماح لـ IMFSourceReader بتسليم RGB32 مباشرةً
  • النمط B: تلقّي NV12 أو YUY2 والتحويل إلى RGB في كود التطبيق

الهدف هنا ليس حفظ أسماء الـ APIs. الهدف هو جعل التدفّق واضحاً بما يكفي لتتمكّن من تخيّل أين يظهر YUV في Media Foundation وأين يدخل RGB إلى المشهد.

1. النسخة المختصرة

يمكن تلخيص الخلاصة العمليّة كالتالي:

  • بالنسبة لـ استخراج عدد قليل من الإطارات أو توليد الصور المصغّرة، فإنّ تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING وطلب MFVideoFormat_RGB32 هو المسار الأسهل
  • ذلك التحويل التلقائيّ هو معالجة برمجيّة، ولذلك ليس إجابةً ممتازة للتشغيل في الوقت الحقيقيّ أو للتحويل بإنتاجيّة عالية
  • إن كنت ستكتب التحويل بنفسك، فإنّ فهم NV12 و YUY2 بشكل صحيح هو أقصر طريق
  • تحويل YUV إلى RGB ليس فقط «تطبيق ثلاثة معاملات». فهو في الواقع يتضمّن أيضاً subsampling و nominal range و color matrix و stride
  • في وثائق Media Foundation، كلمة YUV تعني فعليّاً في الغالب Y’CbCr بمصطلحات الفيديو الرقميّ العمليّة
  • أكثر أخطاء الألوان شيوعاً تأتي من تجاهل MF_MT_YUV_MATRIX و MF_MT_VIDEO_NOMINAL_RANGE، أو من افتراض أنّ stride يساوي دائماً width * bytesPerPixel

التقسيم بسيط إذن: إذا كانت الراحة هي الأهمّ، فاطلب من Source Reader أن يعطيك RGB32. إذا كان التحكّم أو الحجم أو مسؤوليّة الألوان أهمّ، فتلقّ YUV وحوّله بنفسك.

2. الصورة أوّلاً

من الأسهل البدء بصورة لمكان حدوث التحويل.

flowchart LR
    File["MP4 / H.264 / HEVC"] --> Decoder["decoder"]
    Decoder --> YUV["NV12 / YUY2 / YV12 and other YUV frames"]
    YUV -->|Pattern A| SRVP["Source Reader video processing"]
    SRVP --> RGB1["RGB32"]
    YUV -->|Pattern B| App["Application-side conversion code"]
    App --> RGB2["BGRA / RGB"]

إذا كان الملفّ المصدر فيديو مضغوطاً مثل H.264 أو HEVC، فإنّ الـ decoder ينتج أوّلاً إطاراً غير مضغوط. ذلك الإطار لا يكون في الغالب بصيغة RGB. في خطوط أنابيب فيديو Windows، فإنّ صيغ YUV هي الحالة العاديّة.

ولذلك عندما يريد التطبيق RGB، فإنّه يختار عادةً أحد المسارين:

  1. السماح لـ Media Foundation بنقل الإطار إلى RGB32 بالكامل
  2. تلقّي YUV والتحويل إلى RGB يدويّاً

ذلك الاختيار هو الموضوع الحقيقيّ لهذا المقال.

3. ترتيب علاقة YUV و RGB أوّلاً

3.1. كلمة “YUV” تعني عمليّاً Y’CbCr في الغالب

تستخدم Windows APIs والوثائق كلمة YUV على نطاق واسع، لكن في العمل العمليّ على الفيديو الرقميّ، فإنّ قراءة U بمعنى Cb و V بمعنى Cr تكون قريبة بما يكفي للحفاظ على نموذجك الذهنيّ مستقيماً.

بشكل تقريبيّ:

  • Y يحمل معلومات شبيهة بالسطوع
  • U و V يحملان معلومات فروق الألوان
  • RGB يخزّن الأحمر والأخضر والأزرق مباشرةً لكلّ بكسل

عين الإنسان أكثر حساسيّة للتفاصيل في السطوع منها للتفاصيل في chroma. ولذلك تحتفظ صيغ الفيديو غالباً بـ Y بتفاصيل أعلى وتقلّل دقّة U و V.

3.2. تصف 4:4:4 و 4:2:2 و 4:2:0 كيفيّة مشاركة chroma

هذه هي الفكرة الأساسيّة التي تجعل الصيغ قابلة للقراءة.

الترميز المعنى أمثلة شائعة
4:4:4 كلّ بكسل لديه بيانات Y/U/V كاملة AYUV، I444
4:2:2 كلّ بكسلين أفقيّين يتشاركان chroma YUY2، UYVY، I422
4:2:0 كتلة 2x2 تتشارك chroma 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 من البكسلات تتشارك زوج U/V واحد.

YUY2 (4:2:2, packed)

bytes:
Y0 U0 Y1 V0   Y2 U2 Y3 V2   ...

في YUY2، فإنّ بكسلين أفقيّين يتشاركان زوج U/V واحد.

ذلك يفسّر بالفعل لماذا لا يكون تحويل YUV إلى RGB استبدالاً بسيطاً بكسلاً ببكسل.
عليك أوّلاً أن تقرّر أيّ قيم U/V تنتمي إلى أيّ بكسل.

3.3. تحويل YUV إلى RGB هو عمل sampling وعمل color-space معاً

إذا نظرت إلى Extended Color Information، فإنّ خطّ أنابيب الألوان الكامل قد يتضمّن inverse quantization و chroma upsampling وتحويل YUV إلى RGB ومعالجة دالّة النقل وتحويل primaries و output quantization.

بالنسبة لـ كود التطبيقات العمليّ بـ 8 بت SDR، فإنّ نموذجاً ذهنيّاً مبسّطاً من ثلاثة أجزاء يكفي:

  1. استعادة chroma sampling
    توسيع chroma 4:2:0 أو 4:2:2 بحيث يمكن لكلّ بكسل استخدامه
  2. استعادة nominal range
    تفسير قيم video-range بشكل صحيح
  3. تطبيق الـ matrix
    استخدام BT.601 أو BT.709 أو مجموعة معاملات صحيحة أخرى

بعبارة أخرى، يجيب كود تحويل YUV إلى RGB العمليّ على سؤالين:

  • أيّ قيم U/V تنتمي إلى هذا البكسل؟
  • أيّ matrix يجب أن يحوّل ثلاثيّ Y/U/V هذا إلى RGB؟

3.4. التخمين الصامت لـ BT.601 مقابل BT.709 هو خطأ ألوان ينتظر الحدوث

تشرح وثائق Media Foundation BT.601 و BT.709 بطريقة معقولة، لكنّ عادة «هي على الأرجح 709 لأنّ الدقّة أعلى» تظلّ عادةً محفوفةً بالمخاطر. أخطاء الألوان لا تتسبّب بانهيار البرنامج، ولذلك تتسلّل في الغالب إلى الإنتاج بصمت.

كحدّ أدنى، افحص:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

الاستراتيجيّة الأكثر أماناً للتنفيذ المبكّر هي قبول التركيبات التي يدعمها الكود صراحةً فقط.

3.5. صيغة بدء جيّدة هي تحويل BT.601 limited-range

تبدو صيغة BT.601 تمثيليّة بـ 8 بت كالتالي:

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 أو GDI
  • العبء عمليّ شبيه بأداة، لا تشغيل في الوقت الحقيقيّ

يدعم Source Reader معالجة فيديو محدودة عند تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING، ويمكن لتلك المعالجة أن تنتج RGB32 من مسار فكّ تشفير YUV.

وثائق Microsoft نفسها صريحة في أنّ هذا معالجة برمجيّة و غير مُحسَّن للتشغيل. إذن هو مريح، لكنّه ليس الإجابة الشاملة.

4.2. ما الذي ينتج RGB32

التدفّق مباشر:

  1. إنشاء سمات Source Reader مع MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. تحديد مجرى الفيديو
  3. طلب MFMediaType_Video + MFVideoFormat_RGB32
  4. استدعاء ReadSample

ذلك يجعل مرحلة معالجة الفيديو المحدودة خلف Source Reader تقوم بتحويل YUV إلى RGB.

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,
        &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 لفحص الحجم و stride الفعليّين للإخراج.

4.4. لماذا هذا المسار جذّاب

قوّة هذا النهج بسيطة: فهو يوصلك إلى صورة تبدو صحيحة بسرعة.

  • لست بحاجة إلى كتابة توسيع 4:2:0 أو 4:2:2 بنفسك
  • يبقى الكثير من تفاصيل الصيغة مخفيّاً
  • الإخراج أسهل في تمريره إلى WIC أو GDI
  • بالنسبة لعدد قليل من الإطارات، يكون عمليّاً تماماً في الغالب

بالنسبة لأدوات استخراج الصور الثابتة، يكون هذا في الغالب نقطة الدخول الأهدأ.

4.5. لكنّه لا يزال يحتوي فخاخاً

لهذا المسار التلقائيّ بعض الخصائص المهمّة:

البند المعنى
صيغة الإخراج عادةً RGB32
التنفيذ معالجة برمجيّة
جيّد لـ عدد قليل من الإطارات، الصور المصغّرة، التحويل غير المتّصل
غير مثاليّ لـ العرض المتمحور حول D3D، معالجة الإطارات بإنتاجيّة عالية
محرج مع MF_SOURCE_READER_D3D_MANAGER، MF_READWRITE_DISABLE_CONVERTERS

ثمّة تفصيل آخر مهمّ جدّاً في الممارسة: البايت الرابع من RGB32.
صيغة Windows RGB32 مرتّبة في الذاكرة كـ Blue / Green / Red / Alpha or Don’t Care. ذلك ليس ضماناً لـ «ARGB32 جاهز». إذا مُرّر هذا البايت الرابع إلى مرمّز PNG على أنّه alpha، يمكن أن تصبح الصورة شفّافة بشكل غير مقصود. ملؤه بـ 0xFF قبل الكتابة هو عادةً الحركة الأكثر أماناً.

5. النمط B: اكتب التحويل بنفسك

5.1. أين يلائم التحويل اليدويّ

يكون التحويل اليدويّ ملائماً أكثر عندما:

  • تحتاج إلى معالجة عدد كبير من الإطارات وتريد التحكّم في الأداء
  • تريد إبقاء NV12 على مسار GPU أو SIMD لأطول فترة ممكنة
  • تريد تحكّماً صريحاً في BT.601 أو BT.709 أو nominal range
  • تحتاج إلى مخرجات غير RGB32
  • مسار التحويل التلقائيّ المحدود ليس كافياً

بعبارة أخرى، يقايض هذا المسار الراحة بالتحكّم.

5.2. التدفّق العامّ للتحويل اليدويّ

الخطوات هي:

  1. ضبط Source Reader لإخراج NV12 أو YUY2
  2. فحص الـ media type الفعليّ بـ GetCurrentMediaType
  3. قراءة MF_MT_FRAME_SIZE و MF_MT_DEFAULT_STRIDE و MF_MT_YUV_MATRIX و MF_MT_VIDEO_NOMINAL_RANGE
  4. قفل sample buffer
  5. تحديد قيم Y/U/V التي ينبغي أن يستخدمها كلّ بكسل
  6. تطبيق الـ matrix وكتابة إخراج BGRA

يضيّق الكود في هذا المقال النطاق عمداً إلى 8 بت SDR، فيديو progressive، NV12 أو YUY2، و limited range. هذا ليس كسلاً. إنّه الطريقة الأكثر أماناً لتجنّب ألوان غير صحيحة بصمت.

5.3. اطلب نوع إخراج YUV أوّلاً

الخطوة الأولى هي إخبار Source Reader بإبقاء مسار YUV ظاهراً.

#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;
}

مرّر MFVideoFormat_NV12 أو MFVideoFormat_YUY2 كـ subtype.
ولا تفترض أنّ النوع الفرعيّ المطلوب هو بالضبط ما ستحصل عليه. تأكّد دائماً من النوع الفعليّ بـ GetCurrentMediaType.

5.4. اقبل فقط بيانات الألوان الوصفيّة التي تدعمها صراحةً

يقبل الكود أدناه فقط، عمداً:

  • NV12 أو YUY2
  • BT.601 أو BT.709
  • 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 بعض التفسيرات الاحتياطيّة، لكنّ تقريب البيانات الوصفيّة المجهولة بصمت إلى افتراض افتراضيّ هو من أسهل الطرق لشحن أخطاء ألوان دقيقة.

5.5. اقرأ الـ buffers باستخدام stride، لا الافتراضات

هذا الجزء مهمّ جدّاً:

  • MF_MT_DEFAULT_STRIDE هو الحدّ الأدنى من stride المنطقيّ
  • يمكن لـ sample buffer الفعليّ أن يتضمّن حشواً
  • إذا كان IMF2DBuffer::Lock2D متاحاً، فضّله

هذا المساعد هو تعديل عمليّ لنمط Uncompressed Video Buffers:

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;
};

القاعدة الآمنة بسيطة: ثق بالـ pitch الذي يعيده الـ API، لا بتخمين قائم على العرض.

5.6. ترميز الصيغة لكلّ بكسل

يعالج المثال أدناه فقط limited-range BT.601 و BT.709، ويكتب إخراج 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;
}

البنية مباشرة:

  • اطرح 16 من Y
  • اطرح 128 من U و V
  • طبّق الـ matrix
  • اقصص إلى نطاق البايت
  • اجبر alpha على 255

5.7. تحويل NV12 إلى BGRA32

NV12 هو 4:2:0، ولذا فإنّ كلّ كتلة 2x2 تتشارك زوج U/V واحد.

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;
}

يستخدم هذا تفسيراً شبيهاً بـ nearest-neighbor لمشاركة chroma. ذلك جيّد بما يكفي في الغالب للأدوات وللعديد من مسارات التطبيقات، لكنّ upsampling بجودة أعلى هو خيار تصميم منفصل.

5.8. تحويل YUY2 إلى BGRA32

YUY2 هو packed 4:2:2، ولذلك يتشارك بكسلان أفقيّان زوج U/V واحد.

#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;
}

لأنّ البايتات مرتّبة كـ Y0 U Y1 V، فإنّ نمط chroma المشترك يظهر مباشرةً في الكود.

5.9. نقطة دخول عمليّة من IMFSample

بعد أن يصبح الـ sample متجاوراً، فإنّ التوزيع بحسب النوع الفرعيّ يبقي الجانب المُستدعِي بسيطاً.

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;
}

يصبح التدفّق المحيط حينها:

  • إنشاء القارئ
  • طلب NV12 أو YUY2
  • بناء DecodedFrameInfo من GetCurrentMediaType
  • استدعاء 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 can now be treated as top-down 32bpp BGRA

5.10. أين يقع التحويل اليدويّ معماريّاً

كلّ الكود أعلاه يحوّل بعد Source Reader، داخل التطبيق. ذلك هو المكان الأوضح للبدء.

ثمّة متغيّرات أكثر تقدّماً أيضاً:

  • كتابة MFT مخصّص
  • استخدام Video Processor MFT / XVP
  • تحويل NV12 إلى RGB على GPU بـ shaders

تلك خيارات حقيقيّة، لكنّها تنقل الموضوع من «كيف نفهم تحويل YUV إلى RGB في Media Foundation» إلى «أين نضع مرحلة التحويل في خطّ أنابيب وسائط أكبر».

6. أيّ مسار ينبغي أن تختار؟

عادةً ما يجعل هذا الجدول القرار أسهل:

المنظور التحويل التلقائيّ (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) التحويل اليدويّ
سرعة التنفيذ
استخراج بضع صور ثابتة
دفعات كبيرة / أعباء وقت حقيقيّ
تحكّم صريح بـ matrix / range
التكامل مع D3D / GPU ○ إلى ◎
صيغ إخراج غير RGB32
تعلّم النموذج الأساسيّ

كاستراتيجيّة أولى عمليّة:

  • إذا كنت تريد شيئاً يعمل بسرعة، فاستخدم التحويل التلقائيّ
  • إذا كنت تريد التحكّم الكامل في الألوان والأداء، فاستخدم التحويل اليدويّ

من المعقول جدّاً أيضاً استخدام المسار التلقائيّ أوّلاً كخطّ أساسيّ للصحّة، ثمّ استبداله بمسار يدويّ لاحقاً.

7. الفخاخ الشائعة في الممارسة

7.1. افتراض أنّ RGB32 يعني alpha صالحاً بالكامل

RGB32 في الذاكرة هو B, G, R, Alpha or Don't Care.
إذا كان البايت الرابع صفراً وسلّمته مباشرةً إلى مسار PNG يعامله كـ alpha، يمكن أن تصبح الصورة شفّافة. ملؤه بـ 0xFF هو الافتراضيّ الأكثر أماناً.

7.2. افتراض أنّ stride يساوي width * bytesPerPixel

هذا أحد أكثر الأخطاء شيوعاً.
يمكن لـ sample buffers الحقيقيّة أن تتضمّن حشواً. تحرّك دائماً بين الصفوف باستخدام stride الفعليّ.

7.3. الخلط بين MF_MT_DEFAULT_STRIDE و pitch الفعليّ

يصف MF_MT_DEFAULT_STRIDE الحدّ الأدنى من stride المنطقيّ للصيغة.
ينبغي أن يأتي pitch الفعليّ لـ sample محدّد من واجهة الـ buffer، خاصّة IMF2DBuffer::Lock2D عندما يكون متاحاً.

7.4. التخمين الصامت لـ 601 مقابل 709

يسهل أن تفوت أخطاء الألوان لأنّها لا تتسبّب بانهيار البرنامج.

كحدّ أدنى، افحص:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

وإن كانت قيمة ما خارج نطاق ما يدعمه الكود صراحةً، فإنّ الفشل بسرعة في الغالب أفضل من التخمين الصامت.

7.5. تقسيم سطح UV لـ NV12 عند width * height

يعتمد إزاحة السطح على stride الفعليّ والارتفاع، لا فقط على width * height.
معاملته كتقسيم قائم على العرض هو طريقة سهلة لإفساد الألوان.

7.6. معالجة فيديو interlaced كما لو كان progressive

تفترض الأمثلة اليدويّة في هذا المقال فيديو progressive.
إذا كان المصدر interlaced وعاملته كإطار progressive واحد، يمكن أن تظهر artifacts شبيهة بالأمشاط. ذلك أحد أسباب أنّ مسار معالجة الفيديو التلقائيّ أو Video Processor MFT قد يكون أنسب في بعض خطوط الأنابيب.

7.7. تجاهل جودة chroma upsampling

مثال NV12 هنا بسيط وقابل للقراءة عمداً. فهو يستخدم قيم chroma المشتركة مباشرةً لكلّ بكسل مغطّى. ذلك عمليّ في الغالب، لكن إن كانت جودة الصورة أهمّ من بساطة التنفيذ، فإنّ جودة chroma upsampling تستحقّ خيار تصميم حقيقيّاً.

8. الخلاصة

عندما تحوّل YUV إلى RGB في Media Foundation، فإنّ بضع أفكار تجعل الموضوع كلّه أسهل بكثير على التفكير:

  • بعد فكّ التشفير، يكون الإطار في الغالب NV12 أو YUY2، لا RGB
  • إذا كانت الراحة هي الأولويّة، فاطلب من Source Reader RGB32
  • إذا كان التحكّم هو الأولويّة، فتلقّ NV12 أو YUY2 وحوّله إلى BGRA بنفسك
  • في المسار اليدويّ، افهم sampling و nominal range و matrix و stride قبل القلق بشأن تفاصيل الصيغة
  • إذا عُومل BT.601 و BT.709 و 16..235 و 4:2:0 و 4:2:2 بشكل غامض، فإنّ النتيجة في الغالب صورة خاطئة بصمت

يبدو تحويل YUV إلى RGB غير ودود للوهلة الأولى. لكن ما إن تتّضح البنية،

  • NV12 يعني زوج U/V واحد لكلّ كتلة 2x2
  • YUY2 يعني زوج U/V واحد لكلّ بكسلين أفقيّين
  • الـ matrix المختار يحوّل تلك القيم إلى RGB

تتوقّف البايتات عن البدو غامضةً وتبدأ في البدو كخطّ أنابيب يمكن التفكير فيه.

9. المراجع

مقالات ذات صلة من شركة كومورا سوفت ذ.م.م.

Microsoft Learn

مقالات ذات صلة

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

كيفيّة دمج الصورة والنصّ في كلّ إطار من فيديو MP4 باستخدام Media Foundation - ترتيب Source Reader والرسم وتحويل الألوان وSink Writer مع نسخة بملفّ .cpp واحد قابل للّصق مباشرةً

نُنظِّم في هذا المقال خطوات Media Foundation لرسم شعار ونصّ على كلّ إطار MP4 بعقليّة Source Reader ثمّ Sink Writer، مع نموذج C++ بملفّ وا...

قراءة المقالة

أين يتصل هذا الموضوع

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

العودة إلى المدونة