كيفيّة تحويل 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.libmfreadwrite.libmfuuid.libole32.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 وحوّله بنفسك. هذان الخياران.
flowchart TB
accTitle: الخياران في هذا المقال
accDescr: مخطّط يبيّن خيار المقال: إن أردت السهولة فاطلب من Source Reader إخراج RGB32، وإن أردت معالجة كميّات كبيرة أو التحكّم في اللون فاستقبل YUV وحوّله بنفسك.
want1["نريد إطار RGB"] -->|"نريد السهولة"| pa1["النمط A: نطلب من Source Reader إخراج RGB32"]
want1 -->|"معالجة كثيفة وتحكّم في اللون"| pb1["النمط B: نستقبل YUV ونحوّل بأنفسنا"]
الشكل 1: إن اخترت السهولة فالنمط A، وإن تحمّلت كمّ المعالجة ومسؤوليّة اللون فالنمط B.
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 23، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. نبدأ بالصورة
أسرع مدخل هو أن نرى ماذا يحدث داخل Media Foundation في رسم.
flowchart LR
File["MP4 / H.264 / HEVC"] --> Decoder["decoder"]
Decoder --> YUV["إطارات YUV مثل NV12 / YUY2 / YV12"]
YUV -->|النمط A| SRVP["video processing في Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|النمط B| App["شيفرة التحويل في التطبيق"]
App --> RGB2["BGRA / RGB"]
الشكل 2: ما يخرجه decoder إطار YUV، ومن هناك ينقسم الطريق إلى RGB إلى النمط A والنمط B.
إن كان محتوى الملفّ مضغوطاً مثل 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يعني أنّ كلّ بكسل يحمل Red / Green / Blue كما هما
عين الإنسان أشدّ حساسيّة لتفاصيل السطوع منها لتفاصيل اللون. لذلك ينفع في الفيديو تصميم يحفظ Y بدقّة أعلى و U/V بخشونة أكبر. هذا سبب شيوع صيغ عائلة YUV.
flowchart TB
accTitle: سبب استخدام عائلة YUV
accDescr: مخطّط يبيّن أنّ عين الإنسان أشدّ حساسيّة لتفاصيل السطوع منها لتفاصيل اللون، فيفيد تصميم يحفظ Y بدقّة أعلى و U/V بخشونة أكبر، وهذا سبب شيوع صيغ عائلة YUV.
eye1["العين أشدّ حساسيّة للسطوع"] --> dsn1["نحفظ Y بدقّة أعلى و U/V بخشونة أكبر"]
dsn1 --> why1["سبب استخدام صيغ عائلة 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 المشتركين إلى كلّ بكسل.
flowchart TB
accTitle: مقارنة وحدة المشاركة في NV12 و YUY2
accDescr: مخطّط يبيّن أنّ NV12 تشترك فيه أربع بكسلات في كتلة 2x2 في زوج U/V، وأنّ YUY2 تشترك فيه بكسلتان أفقيّتان في زوج U/V، لذا يجب أوّلاً تقرير كيف يُسنَد U/V المشتركان إلى كلّ بكسل.
nv1["NV12 (4:2:0)"] --> sh1["أربع بكسلات 2x2 تشترك في زوج U/V"]
yy1["YUY2 (4:2:2)"] --> sh2["بكسلتان أفقيّتان تشتركان في زوج U/V"]
sh1 --> asn1["نقرّر كيف نُسنِد القيمة إلى كلّ بكسل"]
sh2 --> asn1
الشكل 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 العمليّة يكفي في البداية تقسيم العمل إلى ثلاث طبقات:
- إعادة الـ subsampling توسيع U/V في 4:2:0 أو 4:2:2 إلى شكل يستطيع كلّ بكسل الرجوع إليه
- إعادة الـ range في الفيديو تكون Y عادةً 16..235 و U/V 16..240، فنُعيد هذا التحجيم
- ضرب الـ matrix
التحويل إلى RGB بمعاملات مثل
BT.601أوBT.709
أي أنّ تحويل YUV -> RGB في العمل هو تقرير:
- أيّ U/V يخصّ لون هذا البكسل
- بأيّ معاملات نعيد تلك Y/U/V إلى RGB
flowchart TB
accTitle: الطبقات الثلاث في الشيفرة العمليّة
accDescr: مخطّط يبيّن أنّ شيفرة 8-bit SDR العمليّة تُفهَم بسهولة إن قُسِّمت إلى ثلاث طبقات: إعادة الـ subsampling، ثمّ إعادة الـ range، ثمّ ضرب الـ matrix.
y1["إطار YUV"] --> up1["إعادة الـ subsampling"]
up1 --> rg1["إعادة الـ range (مثل 16..235)"]
rg1 --> mx1["ضرب الـ matrix (601 / 709)"]
mx1 --> rgb2["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_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
ثمّ لا تُمرِّر إلا التركيبات التي تدعمها شيفرتك صراحةً. هكذا تقلّ الحوادث الهادئة لاحقاً.
flowchart TB
accTitle: مسار لا يخمّن فضاء اللون
accDescr: مخطّط يبيّن أنّ التخمين الصامت لـ 601 أو 709 من الدقّة يسهّل دخول انحراف اللون حيّز التشغيل دون ملاحظة، لذا نقرأ MF_MT_YUV_MATRIX و MF_MT_VIDEO_NOMINAL_RANGE ولا نمرّر إلا التركيبات المدعومة صراحةً.
gs1["تخمين صامت من الدقّة"] --> sl1["انحراف اللون لا يُسقط البرنامج"]
sl1 --> op1["يدخل التشغيل دون أن يُلاحَظ"]
at1["نقرأ سمات matrix و range"] --> ps1["لا نمرّر إلا التركيبات المدعومة"]
ps1 -.-> sf1["نمنع الحوادث الهادئة"]
الشكل 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.
flowchart TB
accTitle: بنية معادلة التحويل
accDescr: مخطّط يبيّن بنية المعادلة: طرح 16 (مستوى الأسود) من Y، وطرح 128 (المركز) من U و V، ثمّ ضرب معاملات الـ matrix، ثمّ قصّ الناتج.
yy2["نطرح 16 من Y (مستوى الأسود)"] --> co1["نضرب معاملات الـ matrix"]
uv1["نطرح 128 من U / V (المركز)"] --> co1
co1 --> cl1["نقصّ الناتج إلى 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). إن أردت مئات الإطارات في الثانية، فالاعتماد عليها في غير محلّه.
flowchart TB
accTitle: ملاءمة التحويل التلقائيّ وعدمها
accDescr: مخطّط يبيّن أنّ التحويل التلقائيّ في Source Reader معالجة برمجيّة غير محسَّنة للتشغيل، فيناسب استخراج الصور الثابتة والصور المصغّرة والدفعات، ولا يُعتمَد عليه لمعالجة مئات الإطارات في الثانية.
auto1["التحويل التلقائيّ في Source Reader"] --> sw1["معالجة برمجيّة"]
sw1 -->|"يناسب"| bat1["صور ثابتة وصور مصغّرة ودفعات"]
sw1 -.->|"لا نعتمد عليه"| rt1["معالجة مئات الإطارات في الثانية"]
الشكل 8: التحويل التلقائيّ معالجة برمجيّة، فنقصره على أدوات تتعامل مع عدد قليل من الإطارات.
4.2. ما الذي يُضبَط حتّى يظهر RGB32
التدفّق مباشر إلى حدّ بعيد.
- في السمات الممرَّرة إلى
MFCreateSourceReaderFromURLاضبطMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - اختر مجرى الفيديو
- اطلب
MFMediaType_Video/MFVideoFormat_RGB32عبرSetCurrentMediaType - اقرأ العيّنة بـ
ReadSample
بهذا وحده تقوم معالجة فيديو محدودة خلف الـ decoder بتحويل YUV -> RGB32.
flowchart TB
accTitle: أربع خطوات لتفعيل التحويل التلقائيّ
accDescr: مخطّط يبيّن أربع خطوات التحويل التلقائيّ: تفعيل معالجة الفيديو في السمات وإنشاء القارئ، اختيار مجرى الفيديو، طلب RGB32، ثمّ القراءة بـ ReadSample.
a1["تفعيل معالجة الفيديو في السمات"] --> a2["اختيار مجرى الفيديو"]
a2 --> a3["طلب RGB32"]
a3 --> a4["القراءة بـ ReadSample"]
a4 -.-> a5["التحويل إلى 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,
×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 بنفسك
- يخفي قدراً كبيراً من عناء 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 ليصير غير شفاف.
flowchart TB
accTitle: معاملة البايت الرابع في RGB32
accDescr: مخطّط يبيّن أنّ RGB32 في Windows يرتّب B ثمّ G ثمّ R ثمّ بايتاً رابعاً قد يكون alpha أو don't care، لذا يُفضَّل ملؤه بـ 0xFF ليصير غير شفاف قبل تمريره إلى WIC بوصفه 32bppBGRA.
r32["ترتيب RGB32 في الذاكرة"] --> bgr1["ثلاثة بايتات B و G و R"]
r32 --> b41["البايت الرابع alpha أو don't care"]
b41 -->|"نملؤه بـ 0xFF"| wic1["يمكن تمريره إلى WIC بوصفه 32bppBGRA"]
b41 -.->|"تمريره كما هو"| tr3["قد يظهر شفافاً"]
الشكل 10: البايت الرابع غير محسوم، فاملأه بـ 0xFF ليصير غير شفاف قبل التمرير إلى WIC.
هذه نقطة سهلة الوقوع أيضاً في مقال استخراج الصور الثابتة السابق.
5. النمط B: نكتب التحويل بأنفسنا
5.1. متى يناسب هذا المسار
يناسب التحويل اليدويّ حالات مثل:
- تعالج كمّاً كبيراً من الإطارات وتريد تحسين التحويل بنفسك
- تريد تمرير
NV12كما هو إلى GPU أو SIMD - تريد التعامل صراحةً مع
BT.601/BT.709/ range - تريد صيغة خرج غير
RGB32 - التحويل التلقائيّ المحدود في Source Reader لا يكفي
هو نمط تتحمّل فيه كمّ المعالجة ومسؤوليّة اللون مقابل حرّيّة أكبر.
flowchart TB
accTitle: مقايضة التحويل اليدويّ
accDescr: مخطّط يبيّن أنّ التحويل اليدويّ نمط تتحمّل فيه كمّ المعالجة ومسؤوليّة اللون مقابل التحسين والاتّصال بـ GPU أو SIMD والتحكّم الصريح في matrix و range وحرّيّة صيغة الخرج.
own1["نختار التحويل اليدويّ"] --> res1["نتحمّل كمّ المعالجة ومسؤوليّة اللون"]
res1 --> fr1["حرّيّة التحسين و GPU / SIMD وصيغة الخرج"]
res1 --> fr2["تحكّم صريح في matrix / range"]
الشكل 11: التحويل اليدويّ اختيار تأخذ فيه الأداء وحرّيّة اللون مقابل تحمّل المسؤوليّة.
5.2. التدفّق الكامل للتحويل اليدويّ
الخطوات كالتالي.
- اجعل خرج Source Reader
NV12أوYUY2 - خذ الـ subtype الفعليّ والسمات عبر
GetCurrentMediaType - تحقّق من
MF_MT_FRAME_SIZEوMF_MT_DEFAULT_STRIDEوMF_MT_YUV_MATRIXوMF_MT_VIDEO_NOMINAL_RANGE - استخرج الـ buffer من العيّنة وقفله (lock)
- حدّد Y/U/V التي يرجع إليها كلّ بكسل
- اضرب الـ matrix واكتب BGRA
شيفرة هذا المقال مقصورة على 8-bit SDR / progressive / NV12 أو YUY2 / limited range.
تضييق الفرضيّات هنا ليس اختصاراً، بل مهمّ. تحويل YUV إن كُتب على نمط «نستقبل كلّ شيء مؤقّتاً» يفسد اللون بهدوء.
flowchart TB
accTitle: التدفّق الكامل للتحويل اليدويّ
accDescr: مخطّط يبيّن خطوات التحويل اليدويّ: طلب NV12 أو YUY2، التحقّق من نوع الوسائط والسمات الفعليّة، قفل الـ buffer، استخراج Y/U/V لكلّ بكسل، ثمّ ضرب الـ matrix والكتابة إلى BGRA.
f1["طلب NV12 / YUY2"] --> f2["التحقّق من الـ subtype والسمات الفعليّة"]
f2 --> f3["قفل الـ buffer"]
f3 --> f4["استخراج Y/U/V لكلّ بكسل"]
f4 --> f5["ضرب الـ matrix والكتابة إلى BGRA"]
f2 -.-> nar1["تضييق الفرضيّات يقلّل فساد اللون"]
الشكل 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.
flowchart TB
accTitle: نفصل الطلب عن الخرج الفعليّ
accDescr: مخطّط يبيّن أنّ الـ subtype المطلوب عبر SetCurrentMediaType لا يمرّ بالضرورة كما هو، لذا نتحقّق ممّا يخرج فعليّاً بـ GetCurrentMediaType.
req1["نطلب subtype"] -.->|"لا يمرّ بالضرورة كما هو"| out2["الخرج الفعليّ"]
out2 --> gct1["نتحقّق بـ GetCurrentMediaType"]
gct1 --> use1["نكتب المعالجة اللاحقة بالقيم المؤكَّدة"]
الشكل 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 من الدقّة»
في الحالين، تجنّب الطيّ الصامت وحده. انحراف اللون لا يُسقط البرنامج، فيدخل التشغيل دون أن يُلاحَظ.
flowchart TB
accTitle: التفرّع حين يكون matrix بقيمة Unknown
accDescr: مخطّط يبيّن أنّ Unknown لا يعني أنّنا نعلم أنّه BT.709 بل أنّنا لا نعلم، فينقسم الموقف إلى الرفض الصارم بخطأ أو التمرير بافتراض مع تسجيله في السجلّ، وتجنّب الطيّ الصامت وحده.
unk1["matrix = Unknown أي لا نعلم"] -->|"خطّ هذا المقال"| st4["نرفض بصرامة عبر خطأ"]
unk1 -->|"إن لزم التمرير"| dflt1["نمرّر مع تسجيل الافتراض في السجلّ"]
unk1 -.->|"هذا وحده يُتجنَّب"| mute1["الطيّ الصامت"]
الشكل 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 هنا، انكسر الأمر بهدوء لاحقاً.
flowchart TB
accTitle: أولويّة stride
accDescr: مخطّط يبيّن أنّ MF_MT_DEFAULT_STRIDE أدنى stride، وأنّ الـ buffer الفعليّ قد يحمل stride يشمل الحشو، لذا إن أمكن Lock2D نفضّل قيمته ونتجنّب الحسم على أساس العرض.
p1["actual stride الذي يعيده Lock2D"] -->|"الأولويّة إن أمكن"| acc1["القيمة المستخدمة للوصول إلى الـ buffer"]
p2["MF_MT_DEFAULT_STRIDE (أدنى قيمة)"] -->|"احتياط"| acc1
p3["حسم على أساس العرض"] -.->|"ينكسر بهدوء"| acc1
الشكل 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 أوّلاً.
flowchart TB
accTitle: تصميمان لـ chroma upsampling
accDescr: مخطّط يبيّن أنّ الحدّ الأدنى من التنفيذ يستخدم الـ chroma المشترك كما هو على أربع بكسلات بأسلوب nearest-neighbor وهو غالباً كافٍ عمليّاً، وأنّ استهداف أعلى جودة يجعل التحويل بعد upconversion من 4:2:0 عبر 4:2:2 إلى 4:4:4 أنظف نظريّاً.
min1["حدّ أدنى: استخدام الـ chroma المشترك كما هو"] --> pr1["المظهر غالباً كافٍ عمليّاً"]
hq1["التحويل بعد upconversion"] --> pr2["أنظف نظريّاً ويُقدِّم الجودة"]
hq1 -.-> steps1["الترتيب: 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 ReadSampleConvertSampleToBgra32
flowchart TB
accTitle: التفرّع في دالّة المدخل
accDescr: مخطّط يبيّن بنية دالّة المدخل: استخراج buffer متّصل من العيّنة، ثمّ التحويل الخاصّ بـ NV12 إن كان الـ subtype NV12، أو الخاصّ بـ YUY2 إن كان YUY2، وإلا خطأ.
smp1["IMFSample"] --> cont1["استخراج buffer متّصل"]
cont1 -->|"NV12"| cnv1["إلى تحويل NV12"]
cont1 -->|"YUY2"| cnv2["إلى تحويل YUY2"]
cont1 -->|"غير ذلك"| err1["إرجاع خطأ"]
الشكل 17: عند المدخل نجعل الـ buffer متّصلاً ثمّ نتفرّع إلى التحويل حسب الـ subtype، وما لا يُدعَم يسقط إلى خطأ صريح.
جانب الاستدعاء الفعليّ يبدو مثلاً كالتالي.
ComPtr<IMFMediaType> currentType;
HRESULT hr = reader->GetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
¤tType);
if (FAILED(hr)) return hr;
DecodedFrameInfo info;
hr = GetStrictDecodedFrameInfo(currentType.Get(), &info);
if (FAILED(hr)) return hr;
DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> sample;
hr = reader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&flags,
×tamp,
&sample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (!sample) return MF_E_INVALID_STREAM_DATA;
std::vector<BYTE> bgra;
hr = ConvertSampleToBgra32(sample.Get(), info, bgra);
if (FAILED(hr)) return hr;
// bgra は top-down / 32bpp BGRA として扱える
5.10. أين نضع «شيفرة التحويل اليدويّ»
الشيفرة حتّى هنا هي شكل يحوّل فيه التطبيق بعد Source Reader. هذا الأوضح.
لكن إن أردت الإدراج داخل أنبوب Media Foundation فثمّة تصاميم أخرى.
- كتابة
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). إن طويتها تحت «مسألة دقّة» فوّتتَ عيوباً قابلة للإصلاح.
flowchart TB
accTitle: خطّ الفصل بين الفرق المقبول والعطل الحقيقيّ
accDescr: مخطّط يبيّن أنّ فرقاً من زائد أو ناقص 1 إلى 2 يُفسَّر بسقوط الكسور عند أخذ العيّنة وباختلاف سياسة التحويل إلى عدد صحيح، وأنّ الفرق الذي لا يفسَّر بذلك عطل حقيقيّ يستدعي الشكّ في فرضيّات التحويل.
dif1["ننظر إلى الفرق عن القيمة المتوقَّعة"] -->|"فرق ±1〜2"| exp1["يُفسَّر بكسور العيّنة وسياسة التحويل إلى عدد صحيح"]
dif1 -->|"فرق لا يفسَّر"| bug1["عطل حقيقيّ"]
bug1 --> prem1["نشكّ في فرضيّات 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
إن طابق البكسل الواحد، ننتقل إلى الإطار كاملاً. من نفس الفيديو في نفس اللحظة الزمنيّة نستخرج إطاراً واحداً من كلّ من:
- النمط A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - النمط 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.
flowchart TB
accTitle: التحقّق على درجتين
accDescr: مخطّط يبيّن تحقّقاً على درجتين: أوّلاً تمرير Y/U/V معلومة لبكسل واحد ومقارنتها بالحساب اليدويّ، وإن طابقت نقابل إطاري النمط A والنمط B من نفس الفيديو في نفس اللحظة، ثمّ نضيّق موضع الشكّ من شكل الفرق.
st5["الدرجة 1: مقابلة بكسل واحد بالحساب اليدويّ"] --> st6["الدرجة 2: مقابلة إطارَي المسارين"]
st6 --> shp1["نضيّق موضع الشكّ من شكل الفرق"]
st5 -.-> osf1["لا تحتاج فيديو ولا Media Foundation"]
الشكل 19: بعد عبور تحقّق البكسل الواحد نقارن الإطار كاملاً، فيُضيَّق سبب الفرق من شكله.
6. أيّ النمطين نختار
عند التردّد يرتّب الجدول التالي الأمر كثيراً.
| الزاوية | التحويل التلقائيّ (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
التحويل اليدويّ |
|---|---|---|
| سرعة التنفيذ | ◎ | △ |
| استخراج بضع صور ثابتة | ◎ | ○ |
| كمّ كبير من الإطارات / الوقت الحقيقيّ | △ | ◎ |
| التحكّم الصريح في matrix / range | △ | ◎ |
| الدمج مع GPU / D3D | △ | ○〜◎ |
خرج غير RGB32 |
△ | ◎ |
| فهم المبدأ | ○ | ◎ |
للنسخة الأولى، التفكير كالتالي يسهّل الأمر.
- نريد أن يعمل أوّلاً -> التحويل التلقائيّ
- نريد تحمّل مسؤوليّة اللون أو الأداء -> التحويل اليدويّ
في العمل، ترتيب «نؤكّد أوّلاً الصورة الصحيحة بالتحويل التلقائيّ، ثمّ نستبدل بالمسار اليدويّ» فعّال جدّاً أيضاً. إن حملتَ كلّ شيء من البداية صار أصعب أن تعرف أين انكسرت الصورة.
flowchart TB
accTitle: ترتيب عمليّ فعّال للتقدّم
accDescr: مخطّط يبيّن أنّ تأكيد الصورة الصحيحة أوّلاً بالتحويل التلقائيّ ثمّ الاستبدال بالمسار اليدويّ يجعل فصل موضع انكسار الصورة أسهل.
step1["نؤكّد أوّلاً الصورة الصحيحة بالتحويل التلقائيّ"] --> step2["ثمّ نستبدل بالمسار اليدويّ"]
step2 --> good1["يسهل فصل موضع الانكسار"]
allin1["حمل كلّ شيء من البداية"] -.-> lost1["يصعب معرفة أين انكسر"]
الشكل 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_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
انظر إليهما على الأقلّ. والموقف المناسب تقريباً: أرجع خطأ للقيم التي لا تدعمها شيفرتك.
7.5. قطع UV plane في NV12 عند width * height
إزاحة الـ plane تُحدَّد بـ stride الفعليّ والارتفاع. ليست width * height.
إن عومل هذا باستخفاف انزاح اللون أو تفسّدت الصورة.
flowchart TB
accTitle: كيفيّة حساب حدود plane في NV12
accDescr: مخطّط يبيّن أنّ بداية UV plane في NV12 تُحدَّد بحاصل stride الفعليّ في الارتفاع، وأنّ القطع عند حاصل العرض في الارتفاع يؤدّي إلى انحراف لون أو تلف الصورة.
head1["بداية الـ buffer (Y plane)"] -->|"نتقدّم stride × height"| uv2["بداية UV plane"]
wr1["القطع عند width × height"] -.-> brk1["انحراف لون أو تلف الصورة"]
الشكل 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 على 2x2YUY2يشترك في U/V على بكسلتين أفقيّتين- ثمّ يُضرَب matrix على ذلك الـ U/V مع Y
حتّى يصير الأمر مباشراً إلى حدّ بعيد. تبدو سلسلة البايتات الغامضة بلون كونيّ عندئذ بكسلات ذات معنى.
flowchart TB
accTitle: الصورة التي تُحفَظ في الذهن
accDescr: مخطّط يبيّن أنّ استقرار صورة NV12 يشترك في U/V على 2x2 و YUY2 يشترك في U/V على بكسلتين أفقيّتين ثمّ يُضرَب matrix على ذلك الـ U/V مع Y يجعل سلسلة البايتات الغامضة تُقرأ كبكسلات ذات معنى.
im2["NV12: اشتراك U/V على 2x2"] --> ap1["نضرب matrix على ذلك الـ U/V مع Y"]
im3["YUY2: اشتراك U/V على بكسلتين أفقيّتين"] --> ap1
ap1 --> see1["سلسلة البايتات تُرى بكسلات ذات معنى"]
الشكل 22: إن تكوّنت صورة وحدة المشاركة والـ matrix، صارت بايتات YUV قابلة للقراءة مباشرة.
9. روابط مرجعية
شيفرة عيّنة هذا المقال
مقالات ذات صلة من شركة كومورا سوفت ذ.م.م.
- ما هو Media Foundation - لماذا يبدأ في الإحساس بأنّه COM وواجهات Windows الإعلاميّة في آنٍ واحد
- كيفيّة استخراج صورة ثابتة من MP4 باستخدام Media Foundation
Microsoft Learn
- Microsoft Learn, Source Reader
- Microsoft Learn, Using the Source Reader to Process Media Data
- Microsoft Learn, MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- Microsoft Learn, IMFSourceReader::SetCurrentMediaType
- Microsoft Learn, Recommended 8-Bit YUV Formats for Video Rendering
- Microsoft Learn, Extended Color Information
- Microsoft Learn, Uncompressed Video Buffers
- Microsoft Learn, IMF2DBuffer::Lock2D
- Microsoft Learn, MF_MT_VIDEO_NOMINAL_RANGE attribute
- Microsoft Learn, MFVideoTransferMatrix enumeration
- Microsoft Learn, Video Processor MFT
- Microsoft Learn, Uncompressed RGB Video Subtypes
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
كيف تحرق صورة ونصّاً في إطارات MP4 بـ Media Foundation
نرتّب كيف تحرق صورة ونصّاً في كل إطار من فيديو MP4 بـ Media Foundation وتنتج MP4 جديداً، بتقسيم الأدوار بين Source Reader والرسم وتحويل ا...
استخراج صورة ثابتة من MP4 عند زمن محدّد بـ Media Foundation
نلخّص خطوات التنفيذ لأخذ إطار قريب من زمن محدّد في MP4 عبر Source Reader، وترتيب stride و alpha في RGB32، ثم الحفظ PNG.
مدخل إلى Media Foundation - فهم الواجهة من منظور COM
نرتّب ما هو Media Foundation مع المصطلحات الأساسيّة لواجهات وسائط Windows مثل COM و HRESULT و IMFSourceReader و MFT، بالترتيب الذي يُضبَط...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
تطوير تطبيقات ويندوز
لأنه موضوع تنفيذ لمعالجة الوسائط في Windows يشمل Media Foundation وSource Reader وحفظ الصور وتحويل إطارات الفيديو، فهو محتوى يتناسب مع تطوير تطبيقات Windows.
الاستشارات التقنية ومراجعة التصميم
إذا أردت أوّلاً ترتيب توزيع المسؤوليّات في تحويل YUV / RGB والفضاء اللوني وstride وتصميم مسار التحويل، فهو موضوع يسهل المضيّ فيه ضمن الاستشارة التقنيّة ومراجعة التصميم.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- لماذا يُخرج 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).
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.