كيفيّة تحويل YUV إلى RGB في Media Foundation

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

سجل التعديلات (4 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240840)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
تُرجمت تعليقات مخطّطات تخطيط ذاكرة NV12 و YUY2 إلى العربية.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621368)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). كيفيّة تحويل YUV إلى RGB في Media Foundation. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621368 https://comcomponent.com/ar/blog/2026/03/15/002-media-foundation-yuv-to-rgb-conversion-patterns/

DOI (أحدث نسخة)
10.5281/zenodo.21621368
DOI (هذه النسخة)
10.5281/zenodo.22279784

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

غير أنّ الإطارات الخارجة من decoder في Media Foundation تكون في الغالب بصيغ عائلة YUV مثل NV12 أو YUY2. إن عوملت البايتات الخامّ هناك كما لو كانت صورة جاهزة، انكسرت الألوان، أو ظهرت خطوط، أو مال المشهد إلى أخضر غريب.

في مقال سابق، ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد، رتّبتُ الصورة الكاملة. وفي كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation رتّبتُ استخراج الصور الثابتة. هذه المرّة أتوقّف عند تحويل YUV -> RGB نفسه، الذي يقع بينهما.

أفصل هنا نمطين:

  • النمط A: نترك IMFSourceReader يصل بالإطار إلى RGB32 تلقائيّاً
  • النمط B: نستقبل NV12 / YUY2 ونحوّلهما إلى RGB بأنفسنا

الهدف ليس حفظ أسماء الـ APIs. الهدف أن تتمكّن من تخيّل أين يظهر YUV في Media Foundation، وأين يتحوّل إلى RGB.

الشيفرة الواردة هنا منشورة على GitHub كعيّنة كاملة (شيفرة C++ للنمطين A و B، وتكوين CMake، واختبارات تحويل البكسل).

media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)

بيئة التشغيل المطلوبة

إن نقلتَ شيفرة هذا المقال إلى مشروعك، فهذا ما تحتاجه فقط.

البند المتطلّب
نظام التشغيل Windows 10 فما فوق
المترجم MSVC في Visual Studio 2019 / 2022 (C++17)
SDK Windows SDK (ترويسات Media Foundation ومكتبات الاستيراد). يأتي ضمن حمل عمل «تطوير سطح المكتب بـ C++» في Visual Studio
البناء العيّنة تتطلّب CMake 3.20 فما فوق. يمكنك أيضاً إنشاء مشروع Visual Studio يدوياً

المكتبات التي تُربَط أربع:

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

في شيفرة المقال مكتوبة بـ #pragma comment(lib, ...)، والنتيجة نفسها إن حدّدتها في إعدادات المشروع.

الشيفرة هنا تفترض أنّ CoInitializeEx و MFStartup قد نُفِّذا. معادلة تحويل بكسل واحد فقط (القسم 5.6) مستقلّة عن نظام التشغيل، ولذلك أُخرجت في العيّنة على GitHub إلى ترويسة منفصلة يمكن اختبارها بـ g++ على Linux أيضاً.

1. الخلاصة أوّلاً

إن اختصرنا النتيجة العمليّة فهي كالتالي.

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

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

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

الشكل 1: إن اخترت السهولة فالنمط A، وإن تحمّلت كمّ المعالجة ومسؤوليّة اللون فالنمط B.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 23، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. نبدأ بالصورة

أسرع مدخل هو أن نرى ماذا يحدث داخل Media Foundation في رسم.

النمط Aالنمط BMP4 / H.264 / HEVCdecoderإطارات YUV مثل NV12 / YUY2 / YV12video processing في Source ReaderRGB32شيفرة التحويل في التطبيقBGRA / RGB

الشكل 2: ما يخرجه decoder إطار YUV، ومن هناك ينقسم الطريق إلى RGB إلى النمط A والنمط B.

إن كان محتوى الملفّ مضغوطاً مثل 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 يعني أنّ كلّ بكسل يحمل Red / Green / Blue كما هما

عين الإنسان أشدّ حساسيّة لتفاصيل السطوع منها لتفاصيل اللون. لذلك ينفع في الفيديو تصميم يحفظ Y بدقّة أعلى و U/V بخشونة أكبر. هذا سبب شيوع صيغ عائلة YUV.

سبب استخدام عائلة YUVمخطّط يبيّن أنّ عين الإنسان أشدّ حساسيّة لتفاصيل السطوع منها لتفاصيل اللون، فيفيد تصميم يحفظ Y بدقّة أعلى و U/V بخشونة أكبر، وهذا سبب شيوع صيغ عائلة YUV.العين أشدّ حساسيّة للسطوعنحفظ Y بدقّة أعلى و U/V بخشونة أكبرسبب استخدام صيغ عائلة YUV

الشكل 3: تصميم عائلة YUV يوافق حساسيّة العين للسطوع فيحفظ Y بدقّة أعلى و U/V بخشونة أكبر.

3.2. 4:4:4 / 4:2:2 / 4:2:0 تعني «كم نختزل من اللون»

هذا لبّ قراءة YUV.

الرمز المعنى أمثلة شائعة
4:4:4 كلّ بكسل يحمل Y/U/V على حدة AYUV, I444
4:2:2 بكسلتان أفقيّتان تشتركان في U/V YUY2, UYVY, I422
4:2:0 أربع بكسلات 2x2 تشترك في U/V NV12, YV12, I420

إن رأيتَ شكل الصيغتين الأكثر وروداً في العمل أوّلاً، صار الباقي أسهل بكثير.

نثبّت مصطلحاً هنا. stride (ويُسمّى أيضاً pitch) هو عدد بايتات صفّ واحد. ليس عرض الصورة نفسه، بل «كم بايتاً نتقدّم حتّى بداية الصفّ التالي»، بما في ذلك حشو نهاية الصفّ. في هذا المقال نستخدم stride و pitch بالمعنى نفسه. Microsoft Learn يستخدم اللفظين كليهما، فلا حاجة للتفريق بينهما عند القراءة.

في الرسوم التالية، W هو العرض، و H الارتفاع، و S هو stride. النقطة المهمّة: S >= W، وليس بالضرورة S == W.

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

  <----------- S bytes ----------->
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (padding)           |    |
 | Y Y Y Y Y | (padding)           |    | Y plane
 | Y Y Y Y Y | (padding)           |    | S * H bytes
 | 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 تشترك في زوج واحد من U/V. لكلّ بكسل قيمة Y خاصّة. يستخدم UV plane نفس stride الذي يستخدمه Y plane، لكن عدد الصفوف نصف. لذلك تقع حدود الـ plane عند S * H لا عند W * H (نعود إليها في 7.5).

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

  <-------------- S bytes ---------------->
  <------- W * 2 bytes ------->
 +-----------------------------+----------+
 | 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 pixels = 4 bytes (Y, U, Y, V)
  plane واحد فقط (packed, لا حدود)

في YUY2، بكسلتان أفقيّتان تشتركان في زوج واحد من U/V. Y0 و Y1 مختلفان، أمّا U0 و V0 فمشتركان. لأنّ الصيغة packed فلا حاجة لحساب حدود plane، لكن الانتقال بين الصفوف يستخدم stride أيضاً.

ما يظهر هنا أنّ تحويل YUV -> RGB ليس استبدالاً بكسل ببكسل. عليك أوّلاً أن تقرّر كيف تُسنِد U/V المشتركين إلى كلّ بكسل.

مقارنة وحدة المشاركة في NV12 و YUY2مخطّط يبيّن أنّ NV12 تشترك فيه أربع بكسلات في كتلة 2x2 في زوج U/V، وأنّ YUY2 تشترك فيه بكسلتان أفقيّتان في زوج U/V، لذا يجب أوّلاً تقرير كيف يُسنَد U/V المشتركان إلى كلّ بكسل.NV12 (4:2:0)أربع بكسلات 2x2 تشترك في زوج U/VYUY2 (4:2:2)بكسلتان أفقيّتان تشتركان في زوج U/Vنقرّر كيف نُسنِد القيمة إلى كلّ بكسل

الشكل 4: في الصيغتين يُشارَك U/V، فالتحويل لا ينتهي باستبدال بكسل ببكسل.

3.3. YUV -> RGB هو «تحويل فضاء لون + تحويل عيّنات»

إن نظرت إلى Extended Color Information في Media Foundation، وجدتَ أنّ التحويل اللونيّ الصارم يمرّ بعدّة مراحل: inverse quantization، و chroma upsampling، و YUV -> RGB، و transfer function، وتحويل primaries، ثمّ quantization.

لكن لشيفرة 8-bit SDR العمليّة يكفي في البداية تقسيم العمل إلى ثلاث طبقات:

  1. إعادة الـ subsampling توسيع U/V في 4:2:0 أو 4:2:2 إلى شكل يستطيع كلّ بكسل الرجوع إليه
  2. إعادة الـ range في الفيديو تكون Y عادةً 16..235 و U/V 16..240، فنُعيد هذا التحجيم
  3. ضرب الـ matrix التحويل إلى RGB بمعاملات مثل BT.601 أو BT.709

أي أنّ تحويل YUV -> RGB في العمل هو تقرير:

  • أيّ U/V يخصّ لون هذا البكسل
  • بأيّ معاملات نعيد تلك Y/U/V إلى RGB
الطبقات الثلاث في الشيفرة العمليّةمخطّط يبيّن أنّ شيفرة 8-bit SDR العمليّة تُفهَم بسهولة إن قُسِّمت إلى ثلاث طبقات: إعادة الـ subsampling، ثمّ إعادة الـ range، ثمّ ضرب الـ matrix.إطار YUVإعادة الـ subsamplingإعادة الـ range (مثل 16..235)ضرب الـ matrix (601 / 709)RGB

الشكل 5: التحويل ليس ثلاث معاملات فقط، بل ثلاث طبقات: subsampling و range و matrix.

3.4. التعامل العابر مع BT.601 و BT.709 يُزيح اللون بهدوء

وثائق Media Foundation تصف BT.601 بأنّه مفضَّل لـ SDTV وما دونه، و BT.709 لما يتجاوز SD.

لكن التخمين الصامت من قبيل «الدقّة كبيرة إذن 709» ليس محموداً. انحراف اللون لا يُسقط البرنامج، فيسهل أن يدخل حيّز التشغيل دون أن يلاحظه أحد.

يستطيع Media Foundation حمل معلومات فضاء اللون كسمات لنوع الوسائط. انظر على الأقلّ إلى هذين:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

ثمّ لا تُمرِّر إلا التركيبات التي تدعمها شيفرتك صراحةً. هكذا تقلّ الحوادث الهادئة لاحقاً.

مسار لا يخمّن فضاء اللونمخطّط يبيّن أنّ التخمين الصامت لـ 601 أو 709 من الدقّة يسهّل دخول انحراف اللون حيّز التشغيل دون ملاحظة، لذا نقرأ MF_MT_YUV_MATRIX و MF_MT_VIDEO_NOMINAL_RANGE ولا نمرّر إلا التركيبات المدعومة صراحةً.تخمين صامت من الدقّةانحراف اللون لا يُسقط البرنامجيدخل التشغيل دون أن يُلاحَظنقرأ سمات matrix و rangeلا نمرّر إلا التركيبات المدعومةنمنع الحوادث الهادئة

الشكل 6: لا نخمّن فضاء اللون؛ نقرأ السمات ولا نمرّر إلا التركيبات التي ندعمها صراحةً.

3.5. المعادلة التي تُحفَظ أوّلاً هي نسخة limited range من BT.601

المعادلة النموذجيّة لـ 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 تتغيّر المعاملات. سنراها في الشيفرة لاحقاً.

المهمّ هنا ليس حفظ المعاملات، بل بنية المعادلة: اطرح مستوى الأسود 16 من Y، وانظر إلى U/V حول المركز 128.

بنية معادلة التحويلمخطّط يبيّن بنية المعادلة: طرح 16 (مستوى الأسود) من Y، وطرح 128 (المركز) من U و V، ثمّ ضرب معاملات الـ matrix، ثمّ قصّ الناتج.نطرح 16 من Y (مستوى الأسود)نضرب معاملات الـ matrixنطرح 128 من U / V (المركز)نقصّ الناتج إلى 0..255

الشكل 7: ما يُحفَظ ليس المعاملات بل بنية المعادلة: مستوى الأسود 16 والمركز 128.

4. النمط A: نترك Media Foundation يحوّل تلقائيّاً

4.1. متى يناسب هذا المسار

يناسب هذا الأسلوب مواضع مثل:

  • استخراج صورة ثابتة واحدة من MP4
  • توليد بضع صور مصغّرة
  • تحويل الإطار إلى صورة RGB وتمريرها إلى WIC
  • الاستخدام في دفعات أو أدوات، لا في تشغيل الوقت الحقيقيّ

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

لكن كما يرد في Microsoft Learn، هذه معالجة برمجيّة وغير محسَّنة للتشغيل (playback). إن أردت مئات الإطارات في الثانية، فالاعتماد عليها في غير محلّه.

ملاءمة التحويل التلقائيّ وعدمهامخطّط يبيّن أنّ التحويل التلقائيّ في Source Reader معالجة برمجيّة غير محسَّنة للتشغيل، فيناسب استخراج الصور الثابتة والصور المصغّرة والدفعات، ولا يُعتمَد عليه لمعالجة مئات الإطارات في الثانية.يناسبلا نعتمد عليهالتحويل التلقائيّ في Source Readerمعالجة برمجيّةصور ثابتة وصور مصغّرة ودفعاتمعالجة مئات الإطارات في الثانية

الشكل 8: التحويل التلقائيّ معالجة برمجيّة، فنقصره على أدوات تتعامل مع عدد قليل من الإطارات.

4.2. ما الذي يُضبَط حتّى يظهر RGB32

التدفّق مباشر إلى حدّ بعيد.

  1. في السمات الممرَّرة إلى MFCreateSourceReaderFromURL اضبط MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. اختر مجرى الفيديو
  3. اطلب MFMediaType_Video / MFVideoFormat_RGB32 عبر SetCurrentMediaType
  4. اقرأ العيّنة بـ ReadSample

بهذا وحده تقوم معالجة فيديو محدودة خلف الـ decoder بتحويل YUV -> RGB32.

أربع خطوات لتفعيل التحويل التلقائيّمخطّط يبيّن أربع خطوات التحويل التلقائيّ: تفعيل معالجة الفيديو في السمات وإنشاء القارئ، اختيار مجرى الفيديو، طلب RGB32، ثمّ القراءة بـ ReadSample.تفعيل معالجة الفيديو في السماتاختيار مجرى الفيديوطلب RGB32القراءة بـ ReadSampleالتحويل إلى RGB32 خلف الـ decoder

الشكل 9: بأربع خطوات، توصل معالجة الفيديو خلف الـ decoder الإطار إلى 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,
        &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 بنفسك
  • يخفي قدراً كبيراً من عناء matrix و deinterlace
  • يسهل تمريره إلى WIC أو GDI
  • كافٍ عمليّاً إن عالجتَ بضعة إطارات

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

4.5. لكن ثمّة مطبات أيضاً

لهذا التحويل التلقائيّ الخصائص التالية.

البند المحتوى
وجهة التحويل في الأساس RGB32
التنفيذ معالجة برمجيّة
الاستخدام المناسب عدد قليل من الإطارات، صور مصغّرة، معالجة دون اتصال
الاستخدام غير المناسب رسم الوقت الحقيقيّ المعتمد على D3D، معالجة كمّ كبير من الإطارات
سمات سيّئة التوافق MF_SOURCE_READER_D3D_MANAGER، MF_READWRITE_DISABLE_CONVERTERS

ونقطة مهمّة أخرى: معاملة البايت الرابع في RGB32. RGB32 في Windows، في الذاكرة، هو ترتيب Blue / Green / Red / Alpha أو Don’t Care. ليس ARGB32. إن مرّرته إلى WIC بوصفه 32bppBGRA، فالأأمن أن تملأ البايت الرابع بـ 0xFF ليصير غير شفاف.

معاملة البايت الرابع في RGB32مخطّط يبيّن أنّ RGB32 في Windows يرتّب B ثمّ G ثمّ R ثمّ بايتاً رابعاً قد يكون alpha أو don't care، لذا يُفضَّل ملؤه بـ 0xFF ليصير غير شفاف قبل تمريره إلى WIC بوصفه 32bppBGRA.نملؤه بـ 0xFFتمريره كما هوترتيب RGB32 في الذاكرةثلاثة بايتات B و G و Rالبايت الرابع alpha أو don't careيمكن تمريره إلى WIC بوصفه 32bppBGRAقد يظهر شفافاً

الشكل 10: البايت الرابع غير محسوم، فاملأه بـ 0xFF ليصير غير شفاف قبل التمرير إلى WIC.

هذه نقطة سهلة الوقوع أيضاً في مقال استخراج الصور الثابتة السابق.

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

5.1. متى يناسب هذا المسار

يناسب التحويل اليدويّ حالات مثل:

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

هو نمط تتحمّل فيه كمّ المعالجة ومسؤوليّة اللون مقابل حرّيّة أكبر.

مقايضة التحويل اليدويّمخطّط يبيّن أنّ التحويل اليدويّ نمط تتحمّل فيه كمّ المعالجة ومسؤوليّة اللون مقابل التحسين والاتّصال بـ GPU أو SIMD والتحكّم الصريح في matrix و range وحرّيّة صيغة الخرج.نختار التحويل اليدويّنتحمّل كمّ المعالجة ومسؤوليّة اللونحرّيّة التحسين و GPU / SIMD وصيغة الخرجتحكّم صريح في matrix / range

الشكل 11: التحويل اليدويّ اختيار تأخذ فيه الأداء وحرّيّة اللون مقابل تحمّل المسؤوليّة.

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

الخطوات كالتالي.

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

شيفرة هذا المقال مقصورة على 8-bit SDR / progressive / NV12 أو YUY2 / limited range. تضييق الفرضيّات هنا ليس اختصاراً، بل مهمّ. تحويل YUV إن كُتب على نمط «نستقبل كلّ شيء مؤقّتاً» يفسد اللون بهدوء.

التدفّق الكامل للتحويل اليدويّمخطّط يبيّن خطوات التحويل اليدويّ: طلب NV12 أو YUY2، التحقّق من نوع الوسائط والسمات الفعليّة، قفل الـ buffer، استخراج Y/U/V لكلّ بكسل، ثمّ ضرب الـ matrix والكتابة إلى BGRA.طلب NV12 / YUY2التحقّق من الـ subtype والسمات الفعليّةقفل الـ bufferاستخراج Y/U/V لكلّ بكسلضرب الـ matrix والكتابة إلى BGRAتضييق الفرضيّات يقلّل فساد اللون

الشكل 12: التحويل اليدويّ يسير بطلب ثمّ تحقّق ثمّ قفل ثمّ رجوع ثمّ تحويل، ويصير أأمن كلّما ضاقت الفرضيّات.

5.3. صرّح أوّلاً بنوع وسائط الخرج

أوّلاً، أخبر 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;
}

مرّر هنا MFVideoFormat_NV12 أو MFVideoFormat_YUY2 في subtype.

انتبه: الـ subtype المطلوب لا يمرّ بالضرورة كما هو. ما يخرج فعليّاً تتأكّد منه بـ GetCurrentMediaType.

نفصل الطلب عن الخرج الفعليّمخطّط يبيّن أنّ الـ subtype المطلوب عبر SetCurrentMediaType لا يمرّ بالضرورة كما هو، لذا نتحقّق ممّا يخرج فعليّاً بـ GetCurrentMediaType.لا يمرّ بالضرورة كما هونطلب subtypeالخرج الفعليّنتحقّق بـ GetCurrentMediaTypeنكتب المعالجة اللاحقة بالقيم المؤكَّدة

الشكل 13: الطلب طلب، والخرج الفعليّ يُؤكَّد دائماً بـ GetCurrentMediaType قبل الاستخدام.

5.4. قبل التحويل، لا نقبل إلا معلومات اللون التي ندعمها

في التحويل اليدويّ نستخرج أوّلاً الحدّ الأدنى من نوع الوسائط. عيّنة هذا المقال تقبل NV12 / YUY2 فقط، ولا تُمرِّر إلا matrix من BT.601 أو BT.709، و range من MFNominalRange_16_235 فقط.

#include <vector>

struct DecodedFrameInfo
{
    GUID subtype = GUID_NULL;
    UINT32 width = 0;
    UINT32 height = 0;
    LONG defaultStride = 0;
    MFVideoTransferMatrix matrix = MFVideoTransferMatrix_Unknown;
    MFNominalRange nominalRange = MFNominalRange_Unknown;
};

HRESULT GetDefaultStride(
    IMFMediaType* pType,
    LONG* plStride)
{
    if (!pType || !plStride) return E_POINTER;

    LONG stride = 0;
    HRESULT hr = pType->GetUINT32(
        MF_MT_DEFAULT_STRIDE,
        reinterpret_cast<UINT32*>(&stride));

    if (FAILED(hr))
    {
        GUID subtype = GUID_NULL;
        UINT32 width = 0;
        UINT32 height = 0;

        hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
        if (FAILED(hr)) return hr;

        hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
        if (FAILED(hr)) return hr;

        hr = MFGetStrideForBitmapInfoHeader(subtype.Data1, width, &stride);
        if (FAILED(hr)) return hr;

        hr = pType->SetUINT32(MF_MT_DEFAULT_STRIDE, static_cast<UINT32>(stride));
        if (FAILED(hr)) return hr;
    }

    *plStride = stride;
    return S_OK;
}

HRESULT GetStrictDecodedFrameInfo(
    IMFMediaType* pType,
    DecodedFrameInfo* pInfo)
{
    if (!pType || !pInfo) return E_POINTER;

    HRESULT hr = pType->GetGUID(MF_MT_SUBTYPE, &pInfo->subtype);
    if (FAILED(hr)) return hr;

    if (pInfo->subtype != MFVideoFormat_NV12 &&
        pInfo->subtype != MFVideoFormat_YUY2)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &pInfo->width, &pInfo->height);
    if (FAILED(hr)) return hr;

    hr = GetDefaultStride(pType, &pInfo->defaultStride);
    if (FAILED(hr)) return hr;

    UINT32 value = 0;

    hr = pType->GetUINT32(MF_MT_YUV_MATRIX, &value);
    if (FAILED(hr)) return hr;

    pInfo->matrix = static_cast<MFVideoTransferMatrix>(value);
    if (pInfo->matrix != MFVideoTransferMatrix_BT601 &&
        pInfo->matrix != MFVideoTransferMatrix_BT709)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    hr = pType->GetUINT32(MF_MT_VIDEO_NOMINAL_RANGE, &value);
    if (FAILED(hr)) return hr;

    pInfo->nominalRange = static_cast<MFNominalRange>(value);
    if (pInfo->nominalRange != MFNominalRange_16_235)
    {
        return MF_E_INVALIDMEDIATYPE;
    }

    return S_OK;
}

هنا تعمّدنا أن نكون صارمين (strict). وثائق تعداد Media Foundation تقول أحياناً «عَامِل Unknown على أنّه BT.709»، لكن في العمل إن طويتَ ذلك بصمت صار انحراف اللون أصعب ملاحظة. في التنفيذ الأوّل على الأقلّ، إرجاع خطأ للتركيبات غير المدعومة أأمن.

متى يعود Unknown؟

قد يبدو التشدّد مبالغاً فيه، لذا نسرد مسارات ظهور Unknown. في الغالب هي حالات «المصدر الأصليّ لا يحمل معلومات لون».

  • معلومات اللون غير موجودة في VUI الخاصّ بـ H.264 / HEVC. حسب المعيار، حين يكون colour_description_present_flag صفراً تُعامَل matrix_coefficients على أنّها «غير محدَّدة». إن مرّت عبر الـ decoder بلا هذه المعلومات، وصل matrix غير محدَّد أيضاً إلى المراحل التالية
  • YUV خامّ قادم من جهاز التقاط أو حاوية قديمة. مسار بلا وصف لفضاء اللون
  • وقد لا تكون سمة MF_MT_YUV_MATRIX نفسها موجودة. عندها لا يعيد GetUINT32 قيمة، بل يفشل بـ MF_E_ATTRIBUTENOTFOUND (والشيفرة أعلاه ترفض ذلك كما هو عبر FAILED(hr))

المهمّ: Unknown لا يعني «نعلم أنّه BT.709»، بل «لا نعلم». تطبيق 709 على مادّة بدقّة SD يزيح اللون، والعكس يزيحه أيضاً.

بعد ذلك ينقسم الموقف إلى خطّين:

  • الرفض الصارم (خطّ هذا المقال): نعيد خطأ بوصف الحالة خارج الدعم، ونترك الطبقة الأعلى تقرّر أنّ هذه المادّة غير مدعومة. التصريح بأنّنا لا نعالج أأمن من انحراف لون هادئ
  • التمرير بافتراض افتراضيّ: إن اضطررت إلى التمرير، اترك في السجلّ ما افترضتَه عند Unknown. ثمّ صرّح بأنّك «حسمت 601 / 709 من الدقّة»

في الحالين، تجنّب الطيّ الصامت وحده. انحراف اللون لا يُسقط البرنامج، فيدخل التشغيل دون أن يُلاحَظ.

التفرّع حين يكون matrix بقيمة Unknownمخطّط يبيّن أنّ Unknown لا يعني أنّنا نعلم أنّه BT.709 بل أنّنا لا نعلم، فينقسم الموقف إلى الرفض الصارم بخطأ أو التمرير بافتراض مع تسجيله في السجلّ، وتجنّب الطيّ الصامت وحده.خطّ هذا المقالإن لزم التمريرهذا وحده يُتجنَّبmatrix = Unknown أي لا نعلمنرفض بصرامة عبر خطأنمرّر مع تسجيل الافتراض في السجلّالطيّ الصامت

الشكل 14: Unknown يعني «لا نعلم»، فإمّا أن نرفض أو نمرّر مع سجلّ، ولا نطوي بصمت.

في مسارات الكاميرات وعائلة JPEG قد ترغب في مسار full-range منفصل. هنا لا نخلط ذلك بصمت، بل نضيّق صراحةً الفرضيّات التي تقبلها هذه الشيفرة.

5.5. نقرأ الـ buffer واثقين من stride

هذه أيضاً نقطة مهمّة جدّاً.

  • MF_MT_DEFAULT_STRIDE هو أدنى stride
  • عيّنة الـ buffer الفعليّة قد تحمل actual stride يشمل الحشو (padding)
  • إن أمكن استخدام IMF2DBuffer::Lock2D فأعطه الأولويّة

نمط المساعد في Uncompressed Video Buffers على Microsoft Learn، إن جُعل أسهل استخداماً، يصبح كالتالي.

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 تفترض top-left / stride موجب، لكن عند الوصول الفعليّ إلى الـ buffer الأأمن أن تستخدم stride (= pitch) الذي أعاده الـ API كما هو. إن حسمتَ القيمة على أساس width هنا، انكسر الأمر بهدوء لاحقاً.

أولويّة strideمخطّط يبيّن أنّ MF_MT_DEFAULT_STRIDE أدنى stride، وأنّ الـ buffer الفعليّ قد يحمل stride يشمل الحشو، لذا إن أمكن Lock2D نفضّل قيمته ونتجنّب الحسم على أساس العرض.الأولويّة إن أمكناحتياطينكسر بهدوءactual stride الذي يعيده Lock2Dالقيمة المستخدمة للوصول إلى الـ bufferMF_MT_DEFAULT_STRIDE (أدنى قيمة)حسم على أساس العرض

الشكل 15: stride المستخدم للتنقّل بين الصفوف يُؤخَذ أوّلاً من القيمة الفعليّة لـ Lock2D، ولا يُحسَم من العرض.

5.6. نكتب معادلة البكسل الواحد في شيفرة

هنا نعالج limited range لـ BT.601 و BT.709 فقط. الخرج BGRA32 لأنّه أسهل تمريره إلى WIC أو GDI.

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
  • نقصّ الناتج إلى 0..255
  • نضع 255 في البايت الرابع من BGRA

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

NV12 هو 4:2:0، لذا أربع بكسلات في كتلة 2x2 تشترك في نفس U/V. في الحدّ الأدنى من التنفيذ، أوضح أسلوب هو استخدام ذلك الـ chroma المشترك كما هو على البكسلات الأربع.

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. المظهر غالباً كافٍ عمليّاً، لكن إن استهدفت أعلى جودة فالتصميم الأنظف نظريّاً، كما في مقال YUV على Microsoft Learn، هو إجراء upconversion من 4:2:0 -> 4:2:2 -> 4:4:4 أوّلاً.

تصميمان لـ chroma upsamplingمخطّط يبيّن أنّ الحدّ الأدنى من التنفيذ يستخدم الـ chroma المشترك كما هو على أربع بكسلات بأسلوب nearest-neighbor وهو غالباً كافٍ عمليّاً، وأنّ استهداف أعلى جودة يجعل التحويل بعد upconversion من 4:2:0 عبر 4:2:2 إلى 4:4:4 أنظف نظريّاً.حدّ أدنى: استخدام الـ chroma المشترك كما هوالمظهر غالباً كافٍ عمليّاًالتحويل بعد upconversionأنظف نظريّاً ويُقدِّم الجودةالترتيب: 4:2:0 → 4:2:2 → 4:4:4

الشكل 16: التفريق بين حدّ أدنى يستخدم الـ chroma المشترك كما هو، وتصميم يُجري upconversion على مراحل.

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

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

#include <cstddef>

HRESULT ConvertYuy2ToBgra32(
    IMFMediaBuffer* buffer,
    const DecodedFrameInfo& info,
    std::vector<BYTE>& dstBgra)
{
    if (!buffer) return E_POINTER;
    if (info.subtype != MFVideoFormat_YUY2) return MF_E_INVALIDMEDIATYPE;
    if ((info.width & 1u) != 0) return MF_E_INVALIDMEDIATYPE;

    dstBgra.resize(static_cast<size_t>(info.width) * info.height * 4);

    BufferLock lock(buffer);

    BYTE* scanline0 = nullptr;
    LONG actualStride = 0;
    HRESULT hr = lock.Lock(
        info.defaultStride,
        info.height,
        &scanline0,
        &actualStride);
    if (FAILED(hr)) return hr;

    for (UINT32 y = 0; y < info.height; ++y)
    {
        const BYTE* src =
            scanline0 +
            static_cast<ptrdiff_t>(actualStride) * static_cast<ptrdiff_t>(y);

        BYTE* dstRow =
            dstBgra.data() + static_cast<size_t>(info.width) * 4 * y;

        for (UINT32 x = 0; x < info.width; x += 2)
        {
            const BYTE Y0 = src[0];
            const BYTE U  = src[1];
            const BYTE Y1 = src[2];
            const BYTE V  = src[3];

            hr = ConvertLimitedYuvPixelToBgra(
                Y0,
                U,
                V,
                info.matrix,
                dstRow + static_cast<size_t>(x) * 4);
            if (FAILED(hr))
            {
                lock.Unlock();
                return hr;
            }

            hr = ConvertLimitedYuvPixelToBgra(
                Y1,
                U,
                V,
                info.matrix,
                dstRow + static_cast<size_t>(x + 1) * 4);
            if (FAILED(hr))
            {
                lock.Unlock();
                return hr;
            }

            src += 4;
        }
    }

    lock.Unlock();
    return S_OK;
}

في YUY2 تترتّب البايتات Y0 U Y1 V، فبنية «إعادة استخدام U/V كلّ بكسلتين» ظاهرة كما هي. من هذه الجهة، بناء النموذج الذهنيّ أسهل من NV12.

5.9. مدخل الاستدعاء من العيّنة

أخيراً، إن استخرجتَ buffer متّصلاً من IMFSample ثمّ تفرّعت حسب الـ 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;
}

بهذا يمكن أن يصير ما قبله:

  • إنشاء القارئ
  • طلب NV12 أو YUY2
  • بناء DecodedFrameInfo من GetCurrentMediaType
  • ReadSample
  • ConvertSampleToBgra32
التفرّع في دالّة المدخلمخطّط يبيّن بنية دالّة المدخل: استخراج buffer متّصل من العيّنة، ثمّ التحويل الخاصّ بـ NV12 إن كان الـ subtype NV12، أو الخاصّ بـ YUY2 إن كان YUY2، وإلا خطأ.NV12YUY2غير ذلكIMFSampleاستخراج buffer متّصلإلى تحويل NV12إلى تحويل YUY2إرجاع خطأ

الشكل 17: عند المدخل نجعل الـ buffer متّصلاً ثمّ نتفرّع إلى التحويل حسب الـ subtype، وما لا يُدعَم يسقط إلى خطأ صريح.

جانب الاستدعاء الفعليّ يبدو مثلاً كالتالي.

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
  • كتابة مظلّل NV12 -> RGB على جهة GPU

عند هذا الحدّ يتغيّر الموضوع قليلاً، لذا اقتصرنا هذه المرّة على شيفرة جانب التطبيق. لكن من المفيد أن تعرف أنّ بين «نترك الأمر لـ Media Foundation» و«نفعل كلّ شيء في التطبيق» محطّة وسطى هي Video Processor MFT.

5.11. نتأكّد أنّ التحويل صحيح

حوادث اللون صعبة الرؤية بالعين، لذا نفصل «اشتغل» عن «صحيح». الترتيب درجتان.

الدرجة الأولى: نُدخل قيماً معلومة ونقارنها بالحساب اليدويّ

أوثق من تشغيل فيديو مباشرة أن تمرّر Y/U/V معلومة إلى ConvertLimitedYuvPixelToBgra. لا تحتاج ملفّ فيديو ولا Media Foundation.

في limited range لـ BT.601، قيم 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 = 65 و D = 90 - 128 = -38 و E = 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.

النقطة المهمّة هنا أنّ الأحمر صار 254 لا 255. السبب ليس دقّة المعاملات. السبب أنّ Y/U/V الداخلة أعداد صحيحة مُدوَّرة سلفاً.

إن أسقطتَ الأحمر النظريّ (255, 0, 0) إلى limited range لـ BT.601 حصلتَ على Y = 16 + 219 × 0.299 = 81.481 و U = 90.203 و V تمام 240. عند الحفظ كعيّنة 8-bit تختفي هذه الكسور فيصير Y = 81. الـ 0.481 المفقود يصبح عند الإعادة نقصاً قدره 0.481 × 1.164383 ≒ 0.56. 255 − 0.56 = 254.44 ── و 254.44 أعلاه ناتج هذا الحساب. حتّى لو كانت المعاملات بدقّة لانهائيّة يبقى الناتج 254.44، وأثر التدوير إلى ستّ خانات يظهر تحت الخانة الرابعة بعد الفاصلة ولا يظهر في خرج 8-bit.

كيف تُسقَط القيمة أخيراً إلى عدد صحيح يؤثّر أيضاً في النتيجة. ClampToByte في 5.6 يحصر القيمة في [0, 255] ثمّ يقطع value + 0.5، أي التقريب إلى الأقرب. إن استخدمتَ قطعاً بسيطاً (static_cast<BYTE>(value)) بقي هذا الأحمر 254 نفسه، لكن قيماً قريبة من الحدود مثل B = 255.04 للأزرق أو R = 0.38 للأحمر تنزاح بمقدار 1. قبل المقارنة بتنفيذ آخر، تأكّد أيّ الأسلوبين يستخدمه الطرف الآخر.

إذن سبب السماح بفرق ±1〜2 ليس «اختلاف دقّة المعاملات»، بل أمران: «سقوط الكسور عند أخذ العيّنة» و«اختلاف سياسة التحويل إلى عدد صحيح بين التنفيذات». بالمقابل، الفرق الذي لا يفسَّر بهذين أمر حقيقيّ. أحمر يصبح 250، أحمر وأزرق يتبادلان، المناطق الداكنة وحدها تطفو ── هذه الفروق ليست دقّة معاملات، بل فرضيّات التحويل (الخلط بين BT.601 و BT.709، والخلط بين full range و limited range، وتبديل U و V، وسوء قراءة stride). إن طويتها تحت «مسألة دقّة» فوّتتَ عيوباً قابلة للإصلاح.

خطّ الفصل بين الفرق المقبول والعطل الحقيقيّمخطّط يبيّن أنّ فرقاً من زائد أو ناقص 1 إلى 2 يُفسَّر بسقوط الكسور عند أخذ العيّنة وباختلاف سياسة التحويل إلى عدد صحيح، وأنّ الفرق الذي لا يفسَّر بذلك عطل حقيقيّ يستدعي الشكّ في فرضيّات التحويل.فرق ±1〜2فرق لا يفسَّرننظر إلى الفرق عن القيمة المتوقَّعةيُفسَّر بكسور العيّنة وسياسة التحويل إلى عدد صحيحعطل حقيقيّنشكّ في فرضيّات matrix و range و U/V و stride

الشكل 18: الفرق الصغير يُفسَّر بأخذ العيّنة والتحويل إلى عدد صحيح، وما لا يفسَّر بذلك يدعو إلى الشكّ في خلط الفرضيّات.

إن كتبته اختباراً فهذا الشكل كافٍ.

#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 أُخرجت تحويل البكسل الواحد هذا إلى ترويسة مستقلّة عن نظام التشغيل، فيعمل هذا الاختبار دون Windows.

الدرجة الثانية: نقابل خرج النمط A بخرج النمط B

إن طابق البكسل الواحد، ننتقل إلى الإطار كاملاً. من نفس الفيديو في نفس اللحظة الزمنيّة نستخرج إطاراً واحداً من كلّ من:

  1. النمط A (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. النمط B (استقبال NV12 / YUY2 والتحويل اليدويّ)

ثمّ نقارن بكسل ببكسل.

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

هنا لا تتوقّع تطابقاً تامّاً. لسببين.

  • معالجة الفيديو في جهة Source Reader قد تُجري chroma upsampling بطريقة غير nearest-neighbor. تنفيذ 5.7 اليدويّ حدّ أدنى يستخدم الـ chroma المشترك كما هو على أربع بكسلات، فيظهر الفرق أكثر عند الحوافّ
  • معاملة التدوير والدقّة الوسيطة مختلفة

لذلك ما ينبغي النظر إليه ليس «هل يتطابقان» بل نمط ظهور الفرق.

الفرق الظاهر موضع الشكّ
المناطق المسطّحة تتطابق، والفرق عند حدود اللون فقط اختلاف chroma upsampling. متوقَّع
انزياح موحّد على الصورة كلّها خلط matrix (601 / 709) أو range (16..235 / 0..255)
خطوط، أو انزياح مائل حسم stride. 7.2 و 7.5
الأحمر والأزرق يتبادلان خلط BGRA و RGBA
الصورة كلّها شفّافة / سوداء البايت الرابع لم يُملأ بـ 0xFF. 7.1

شكل الفرق يضيّق كثيراً أين تشكّ. إن انزاحت الصورة كلّها موحَّدة فالمعادلة أو معلومات اللون، وإن كان الفرق موضعيّاً فالفهارس أو stride.

التحقّق على درجتينمخطّط يبيّن تحقّقاً على درجتين: أوّلاً تمرير Y/U/V معلومة لبكسل واحد ومقارنتها بالحساب اليدويّ، وإن طابقت نقابل إطاري النمط A والنمط B من نفس الفيديو في نفس اللحظة، ثمّ نضيّق موضع الشكّ من شكل الفرق.الدرجة 1: مقابلة بكسل واحد بالحساب اليدويّالدرجة 2: مقابلة إطارَي المساريننضيّق موضع الشكّ من شكل الفرقلا تحتاج فيديو ولا Media Foundation

الشكل 19: بعد عبور تحقّق البكسل الواحد نقارن الإطار كاملاً، فيُضيَّق سبب الفرق من شكله.

6. أيّ النمطين نختار

عند التردّد يرتّب الجدول التالي الأمر كثيراً.

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

للنسخة الأولى، التفكير كالتالي يسهّل الأمر.

  • نريد أن يعمل أوّلاً -> التحويل التلقائيّ
  • نريد تحمّل مسؤوليّة اللون أو الأداء -> التحويل اليدويّ

في العمل، ترتيب «نؤكّد أوّلاً الصورة الصحيحة بالتحويل التلقائيّ، ثمّ نستبدل بالمسار اليدويّ» فعّال جدّاً أيضاً. إن حملتَ كلّ شيء من البداية صار أصعب أن تعرف أين انكسرت الصورة.

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

الشكل 20: إن أمّنتَ الصورة الصحيحة أوّلاً ثمّ انتقلت إلى التنفيذ اليدويّ، سهل فصل موضع الانكسار.

7. مطبات سهلة الوقوع في العمل

7.1. الاعتقاد أنّ RGB32 هو RGBA يحمل alpha

RGB32 في الذاكرة هو B, G, R, Alpha أو Don't Care. إن حفظته PNG بوصفه BGRA كما هو، قد يكون البايت الرابع صفراً فتصير الصورة شفّافة. الأأمن إدخال 0xFF قبل الحفظ.

7.2. حسم stride بـ width * bytesPerPixel

حادث شائع جدّاً. عيّنة الـ buffer الفعليّة قد تحتوي حشواً، فالمبدأ أن التنقّل بين الصفوف يستخدم actual stride.

7.3. الخلط بين MF_MT_DEFAULT_STRIDE و actual pitch

MF_MT_DEFAULT_STRIDE هو «أدنى stride حين تُمثَّل تلك الصيغة في ذاكرة متّصلة». actual pitch لعيّنة الـ buffer يُفضَّل أن يؤخَذ من القيمة التي يعيدها IMF2DBuffer::Lock2D. (pitch اسم آخر لـ stride. كما في 3.2، نستخدمهما في هذا المقال بالمعنى نفسه.)

7.4. تخمين 601 / 709 بصمت دون قراءة بيانات اللون

حوادث اللون صعبة الرؤية بالعين. ولا تُسقط البرنامج. لذلك هي مزعجة.

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

انظر إليهما على الأقلّ. والموقف المناسب تقريباً: أرجع خطأ للقيم التي لا تدعمها شيفرتك.

7.5. قطع UV plane في NV12 عند width * height

إزاحة الـ plane تُحدَّد بـ stride الفعليّ والارتفاع. ليست width * height. إن عومل هذا باستخفاف انزاح اللون أو تفسّدت الصورة.

كيفيّة حساب حدود plane في NV12مخطّط يبيّن أنّ بداية UV plane في NV12 تُحدَّد بحاصل stride الفعليّ في الارتفاع، وأنّ القطع عند حاصل العرض في الارتفاع يؤدّي إلى انحراف لون أو تلف الصورة.نتقدّم stride × heightبداية الـ buffer (Y plane)بداية UV planeالقطع عند width × heightانحراف لون أو تلف الصورة

الشكل 21: حدود UV plane تُحسَب بـ stride×height، ولا تُقطَع عند width×height.

7.6. معالجة فيديو متداخل (interlaced) بفرضيّة progressive

العيّنة اليدويّة في هذا المقال تفترض progressive. إن قرأتَ interlaced كما هو على أنّه حقل واحد، قد يظهر تشويش مُسنَّن. إن احتجتَ إلى deinterlace فأوضح أن تنظر إلى معالجة الفيديو التلقائيّة في Source Reader أو إلى Video Processor MFT.

7.7. تجاهل جودة chroma upsampling في 4:2:0

تحويل NV12 في هذا المقال، لصالح الوضوح، يستخدم الـ chroma المشترك كما هو على كلّ بكسل. قد يكفي حسب الاستخدام، لكن إن قدّمت الجودة فالأولى تتبّع فكرة الـ upconversion في مواد الصيغ الموصى بها لـ YUV.

8. الخلاصة

عند تحويل YUV إلى RGB في Media Foundation، يقلّ التيه كثيراً إن حملتَ هذا الترتيب أوّلاً.

  • خلف الـ decoder يظهر عادةً NV12 أو YUY2 لا RGB
  • إن أردت السهولة فاطلب RGB32 عبر MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
  • إن أردت التحكّم فاستقبل NV12 / YUY2 وحوّلهما بنفسك إلى BGRA
  • في المسار اليدويّ، أمسك sampling / 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 على ذلك الـ U/V مع Y

حتّى يصير الأمر مباشراً إلى حدّ بعيد. تبدو سلسلة البايتات الغامضة بلون كونيّ عندئذ بكسلات ذات معنى.

الصورة التي تُحفَظ في الذهنمخطّط يبيّن أنّ استقرار صورة NV12 يشترك في U/V على 2x2 و YUY2 يشترك في U/V على بكسلتين أفقيّتين ثمّ يُضرَب matrix على ذلك الـ U/V مع Y يجعل سلسلة البايتات الغامضة تُقرأ كبكسلات ذات معنى.NV12: اشتراك U/V على 2x2نضرب matrix على ذلك الـ U/V مع YYUY2: اشتراك U/V على بكسلتين أفقيّتينسلسلة البايتات تُرى بكسلات ذات معنى

الشكل 22: إن تكوّنت صورة وحدة المشاركة والـ matrix، صارت بايتات YUV قابلة للقراءة مباشرة.

9. روابط مرجعية

شيفرة عيّنة هذا المقال

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

Microsoft Learn

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

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

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

لماذا يُخرج decoder في Media Foundation YUV لا RGB؟
لأنّ عين الإنسان أشدّ حساسيّة لتفاصيل السطوع منها لتفاصيل اللون، فيفيد في الفيديو أن يُحفَظ مكوّن Y (الأقرب إلى السطوع) بدقّة أعلى، ومكوّنا U/V (فرق اللون) بدقّة أخشن. لذلك، في مسارات الفيديو على Windows، تكون الإطارات غير المضغوطة الخارجة من decoder عادةً بصيغ YUV مثل NV12 أو YUY2. وفي سياق الفيديو الرقميّ يسهل الفهم إن قرأتَ كلمة YUV على أنّها تعني عمليّاً Y'CbCr.
ما أسهل طريقة للحصول على إطار RGB؟
تفعيل MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING على IMFSourceReader وطلب MFVideoFormat_RGB32. لاستخراج بضع صور ثابتة أو توليد صور مصغّرة، هذا هو المسار الأسهل. لكنّ هذا التحويل التلقائيّ معالجة برمجيّة (software) غير محسَّنة للتشغيل في الوقت الحقيقيّ، فإن أردت معالجة كميّات كبيرة أو التحكّم في اللون فاستقبل YUV وحوّله بنفسك.
ما الذي ينبغي الانتباه إليه عند تحويل YUV إلى RGB يدوياً؟
الأمر ليس ضرب ثلاث معاملات ثمّ الانتهاء. يتداخل معه الـ subsampling (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 يأتي UV plane تتناوب فيه قيم U و V، وتشترك أربع بكسلات في كتلة 2x2 في زوج واحد من U/V. YUY2 صيغة 4:2:2: تشترك بكسلتان متجاورتان أفقيّاً في زوج U/V. كلتاهما شائعتان في العمل، ويختلف بينهما أسلوب اختزال اللون (subsampling).

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

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