كيفيّة تحويل إطارات YUV إلى RGB باستخدام Media Foundation - أنماط التحويل التلقائيّ في Source Reader والتحويل اليدويّ
عندما يريد تطبيق ما حفظ إطار بصيغة 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، فإنّه يختار عادةً أحد المسارين:
- السماح لـ Media Foundation بنقل الإطار إلى RGB32 بالكامل
- تلقّي 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، فإنّ نموذجاً ذهنيّاً مبسّطاً من ثلاثة أجزاء يكفي:
- استعادة chroma sampling
توسيع chroma 4:2:0 أو 4:2:2 بحيث يمكن لكلّ بكسل استخدامه - استعادة nominal range
تفسير قيم video-range بشكل صحيح - تطبيق الـ 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_MATRIXMF_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مزاح عن الأسود بمقدار 16Uو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
التدفّق مباشر:
- إنشاء سمات Source Reader مع
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - تحديد مجرى الفيديو
- طلب
MFMediaType_Video+MFVideoFormat_RGB32 - استدعاء
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,
×tamp,
ppSample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (*ppSample == nullptr) return MF_E_INVALID_STREAM_DATA;
if (pTimestamp100ns) *pTimestamp100ns = timestamp;
return S_OK;
}
بعد ذلك، يمكن استخدام GetCurrentMediaType لفحص الحجم و stride الفعليّين للإخراج.
4.4. لماذا هذا المسار جذّاب
قوّة هذا النهج بسيطة: فهو يوصلك إلى صورة تبدو صحيحة بسرعة.
- لست بحاجة إلى كتابة توسيع 4:2:0 أو 4:2:2 بنفسك
- يبقى الكثير من تفاصيل الصيغة مخفيّاً
- الإخراج أسهل في تمريره إلى 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. التدفّق العامّ للتحويل اليدويّ
الخطوات هي:
- ضبط Source Reader لإخراج
NV12أوYUY2 - فحص الـ media type الفعليّ بـ
GetCurrentMediaType - قراءة
MF_MT_FRAME_SIZEوMF_MT_DEFAULT_STRIDEوMF_MT_YUV_MATRIXوMF_MT_VIDEO_NOMINAL_RANGE - قفل sample buffer
- تحديد قيم Y/U/V التي ينبغي أن يستخدمها كلّ بكسل
- تطبيق الـ 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أوYUY2BT.601أوBT.709MFNominalRange_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,
¤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 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_MATRIXMF_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 واحد لكلّ كتلة 2x2YUY2يعني زوج U/V واحد لكلّ بكسلين أفقيّين- الـ matrix المختار يحوّل تلك القيم إلى RGB
تتوقّف البايتات عن البدو غامضةً وتبدأ في البدو كخطّ أنابيب يمكن التفكير فيه.
9. المراجع
مقالات ذات صلة من شركة كومورا سوفت ذ.م.م.
- ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
- كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++
Microsoft Learn
- Source Reader
- Using the Source Reader to Process Media Data
- MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- IMFSourceReader::SetCurrentMediaType
- Recommended 8-Bit YUV Formats for Video Rendering
- Extended Color Information
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- MF_MT_VIDEO_NOMINAL_RANGE attribute
- MFVideoTransferMatrix enumeration
- Video Processor MFT
- Uncompressed RGB Video Subtypes
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيفيّة دمج الصورة والنصّ في كلّ إطار من فيديو MP4 باستخدام Media Foundation - ترتيب Source Reader والرسم وتحويل الألوان وSink Writer مع نسخة بملفّ .cpp واحد قابل للّصق مباشرةً
نُنظِّم في هذا المقال خطوات Media Foundation لرسم شعار ونصّ على كلّ إطار MP4 بعقليّة Source Reader ثمّ Sink Writer، مع نموذج C++ بملفّ وا...
كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation - ملفّ .cpp واحد يمكن لصقه في تطبيق وحدة تحكّم C++
دليل عمليّ لاستخراج إطار ثابت قرب لحظة محدّدة من ملفّ MP4 عبر Source Reader في Media Foundation، مع معالجة الـ seek و stride واتّجاه الصو...
ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
مقال يوضّح لماذا يشعر مطوّرو Media Foundation بأنّهم يكتبون كود COM، ويرتّب المصطلحات الأساسيّة ونقاط الدخول مثل Source Reader و Sink Wri...
المزالق وأفضل الممارسات عند استخدام shared memory - تنظيم مسبق للتزامن، الرؤية، العمر، ABI، والأمان
نُلخّص أبرز المزالق عند استخدام shared memory ونصمّم للتزامن، الرؤية، العمر، ABI، والاستعادة، حتّى يبني القارئ تكاملاً ثابتاً منخفض الأعطال.
كيف نُحوِّل C# إلى native DLL باستخدام Native AOT - استدعاء exports من نوع UnmanagedCallersOnly من C/C++
يوضِّح هذا المقال كيف نُصدر مكتبة C# بوصفها native DLL عبر Native AOT، ونكشف نقاط دخول UnmanagedCallersOnly تُستدعى مباشرةً من C أو C++ ب...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
ندعم تطوير برامج ويندوز للأعمال، وتكامل الأجهزة، وأدوات التواصل.