איך ממירים YUV ל-RGB ב-Media Foundation
· עודכן בתאריך: · Go Komura · Media Foundation, C++, פיתוח Windows, עיבוד וידאו, YUV
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 15 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173475)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). איך ממירים YUV ל-RGB ב-Media Foundation. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173475 https://comcomponent.com/he/blog/media-foundation-yuv-to-rgb-conversion-patterns/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173475
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173476
רוצים לשלוף frame מווידאו ולשמור כ-PNG, להעביר ל-WIC או ל-GDI, או להציג ב-UI. במצבים כאלה צד האפליקציה רוצה רצף pixels ב-RGB.
אבל ה-frames שיוצאים מה-decoder של Media Foundation הם בדרך כלל בפורמט YUV כמו NV12 או YUY2. אם מתייחסים לרצף הבייטים הגולמי כאילו הוא תמונה כמו שהוא, מקבלים צבעים שבורים, פסים, או גוון ירוק מוזר.
במאמר מבוא ל-Media Foundation: להבין את ה-API דרך COM כיסינו את התמונה הכללית, ובמאמר איך לחלץ תמונת סטילס מ-MP4 בזמן נתון עם Media Foundation כיסינו חילוץ stills. הפעם נטפל בהמרת YUV -> RGB עצמה, שנמצאת בדרך שביניהם.
במאמר הזה נפריד בין שני ה-patterns הבאים:
- Pattern A: לתת ל-
IMFSourceReaderלהוביל אוטומטית עד RGB32 - Pattern B: לקבל
NV12/YUY2ולהמיר ל-RGB בעצמנו
המטרה אינה לשנן שמות API. המטרה היא לבנות בראש תמונה ברורה של איפה ב-Media Foundation מופיע YUV, ואיפה הוא הופך ל-RGB.
הקוד במאמר הזה זמין כסט דוגמאות מלא (C++ ל-Pattern A ול-Pattern B, CMake, ובדיקות המרת pixel) ב-GitHub.
media-foundation-yuv-to-rgb-conversion-patterns - komurasoft-blog-samples (GitHub)
סביבת ההרצה שמתחילים ממנה
אם רוצים לקחת את הקוד מהמאמר לפרויקט שלכם, זה כל מה שצריך:
| פריט | הנחה |
|---|---|
| OS | Windows 10 ואילך |
| compiler | MSVC של Visual Studio 2019 / 2022 (C++17) |
| SDK | Windows SDK (headers וספריות ייבוא של Media Foundation). כלול ב-workload “Desktop development with C++” של Visual Studio |
| build | הדוגמה בנויה ב-CMake 3.20 ואילך. אפשר גם ליצור פרויקט Visual Studio ידנית |
יש לקשר ארבע ספריות. בקוד של המאמר זה כתוב עם #pragma comment(lib, ...), אבל אפשר גם לציין את זה בהגדרות הפרויקט.
mfplat.libmfreadwrite.libmfuuid.libole32.lib
הקוד במאמר מניח ש-CoInitializeEx ו-MFStartup כבר רצו. נוסחת ההמרה של pixel בודד (5.6.) לבדה אינה תלויה ב-OS, ולכן בדוגמה ב-GitHub היא יושבת ב-header נפרד, ואפשר לבדוק אותה גם עם g++ ב-Linux.
1. קודם המסקנה
נסכם קודם רק את המסקנה:
- לחילוץ כמה stills או יצירת thumbnail, הכי קל להפעיל את
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGולבקשMFVideoFormat_RGB32 - אבל ההמרה האוטומטית הזו היא software processing, ולא מותאמת ל-playback בזמן אמת
- אם כותבים המרה עצמית, הדרך הקצרה ביותר היא קודם להבין היטב את
NV12ואתYUY2 - YUV -> RGB זה לא “שלושה מקדמים וזהו” — בפועל מעורבים chroma 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["רוצים frame ב-RGB"] -->|"רוצים נוחות"| pa1["Pattern A: Source Reader מוציא RGB32"]
want1 -->|"עיבוד בנפח גדול / שליטה בצבע"| pb1["Pattern B: מקבלים YUV וממירים בעצמנו"]
איור 1: אם בוחרים נוחות — Pattern A. אם לוקחים אחריות על נפח העיבוד ועל הצבע — Pattern B.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 23, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. קודם רואים בציור
קודם כדאי לראות בציור מה קורה בתוך Media Foundation.
flowchart LR
accTitle: המסלול מ-YUV ל-RGB
accDescr: תרשים שמראה שקובץ מקור עובר דרך decoder ל-frame ב-YUV, ומשם מתפצל למסלול A דרך video processing של Source Reader ל-RGB32, או למסלול B דרך קוד המרה עצמי ל-BGRA/RGB.
File["MP4 / H.264 / HEVC"] --> Decoder["decoder"]
Decoder --> YUV["frame ב-YUV מסוג NV12 / YUY2 / YV12 וכדומה"]
YUV -->|Pattern A| SRVP["video processing של Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|Pattern B| App["קוד המרה עצמי"]
App --> RGB2["BGRA / RGB"]
איור 2: מה שה-decoder מוציא הוא frame ב-YUV, ומשם הדרך ל-RGB מתפצלת ל-Pattern A ול-Pattern B.
אם תוכן קובץ הווידאו הוא פורמט דחוס כמו H.264 או HEVC, ה-decoder מחזיר אותו קודם ל-frame לא-דחוס. ה-frame הלא-דחוס הזה אינו בהכרח RGB. להפך: בקטגוריית הווידאו של Windows, YUV הוא הנורמה.
לכן, כשהאפליקציה רוצה RGB, בוחרים באחת משתי האפשרויות:
- לתת לצד Media Foundation להוביל עד RGB32
- לקבל YUV ולהפוך אותו ל-RGB בקוד שלנו
המאמר הזה עוסק בדיוק בנקודת הפיצול הזו.
3. קודם מבהירים את היחס בין YUV ל-RGB
3.1. אומרים YUV, אבל בפועל זה Y’CbCr
שמות ה-API והתיעוד של Windows משתמשים באופן רחב במילה YUV. אבל בהקשר של וידאו דיגיטלי, אפשר כמעט תמיד לקרוא U כ-Cb ו-V כ-Cr.
בגדול:
Yהוא רכיב שקרוב יותר לבהירותU/Vהם רכיבי color difference- ב-
RGBכל pixel מחזיק ישירות 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 | לכל pixel יש Y/U/V משלו | AYUV, I444 |
| 4:2:2 | שני pixels לרוחב חולקים U/V | YUY2, UYVY, I422 |
| 4:2:0 | 2x2 pixels חולקים U/V | NV12, YV12, I420 |
כדאי קודם לראות את הצורה של שני הפורמטים שנפוצים ביותר בפועל.
לפני זה נקבע מונח אחד. stride (בשם אחר, pitch) הוא מספר הבייטים לשורה אחת. זה לא רוחב התמונה עצמו, אלא “כמה בייטים מתקדמים עד תחילת השורה הבאה”, כולל padding בסוף השורה. במאמר הזה stride ו-pitch הם אותו דבר. גם ב-Microsoft Learn מופיעים שני הכינויים, ולכן אין צורך להבחין ביניהם.
בציור הבא, W הוא width, H הוא height, ו-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 מתחילת ה-buffer
| 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, ארבעת ה-pixels ב-block 2x2 חולקים זוג U/V אחד. ל-Y יש ערך לכל pixel.
ה-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, שני pixels לרוחב חולקים זוג U/V אחד. Y0 ו-Y1 נפרדים, אבל U0 ו-V0 משותפים.
כי זה packed, אין צורך לחשב גבול plane, אבל עדיין משתמשים ב-stride כדי לעבור בין שורות.
בשלב הזה כבר רואים שהמרת YUV -> RGB אינה החלפה פשוטה של pixel-אחר-pixel. קודם צריך לחשוב איזה U/V משותף שייך לאיזה pixel.
flowchart TB
accTitle: השוואת יחידת השיתוף בין NV12 ל-YUY2
accDescr: תרשים שמראה ש-NV12 מחלק זוג U/V אחד בין 4 pixels ב-block 2x2, ו-YUY2 מחלק זוג U/V בין 2 pixels לרוחב, ולכן צריך לקבוע מראש איזה pixel מקבל איזה U/V.
nv1["NV12 (4:2:0)"] --> sh1["4 pixels ב-2x2 חולקים זוג U/V"]
yy1["YUY2 (4:2:2)"] --> sh2["2 pixels לרוחב חולקים זוג U/V"]
sh1 --> asn1["קובעים איזה pixel מקבל איזה U/V"]
sh2 --> asn1
איור 4: בשני הפורמטים ה-U/V משותף, ולכן ההמרה לא מסתכמת בהחלפה per-pixel.
3.3. YUV -> RGB זה “המרת מרחב צבע + המרת sampling”
אם מסתכלים ב-Extended Color Information של Media Foundation, המרת הצבע המדויקת כוללת הרבה שלבים. inverse quantization, chroma upsampling, YUV -> RGB, transfer function, המרת primaries, ועד quantization.
עם זאת, כקוד מעשי ל-SDR ב-8-bit, נוח יותר לפצל לשלוש שכבות:
- לשחזר את ה-chroma subsampling להרחיב את ה-U/V המדולל של 4:2:0 או 4:2:2 לצורה שכל pixel יכול לגשת אליה
- לשחזר את ה-range ה-Y בוידאו בדרך כלל 16..235, וה-U/V בדרך כלל 16..240, ולכן צריך להחזיר את הסקאלה
- להפעיל את ה-matrix
המרה ל-RGB עם מקדמים כמו
BT.601אוBT.709
כלומר, המרת YUV -> RGB בפועל היא תהליך שקובע:
- איזה U/V הוא הצבע של אותו pixel
- באילו מקדמים מחזירים את אותו Y/U/V ל-RGB
flowchart TB
accTitle: שלוש השכבות שקוד מעשי צריך לתפוס
accDescr: תרשים שמראה שקוד מעשי ל-SDR ב-8-bit מבוסס על שלוש שכבות: שחזור chroma subsampling, שחזור range, והפעלת matrix, וכך קל יותר להבין את ההמרה מ-YUV ל-RGB.
y1["frame ב-YUV"] --> up1["שחזור chroma subsampling"]
up1 --> rg1["שחזור range (למשל 16..235)"]
rg1 --> mx1["הפעלת matrix (601 / 709)"]
mx1 --> rgb2["RGB"]
איור 5: ההמרה לא מסתכמת בשלושה מקדמים — היא מורכבת משלוש שכבות: sampling, range ו-matrix.
3.4. טיפול רשלני ב-BT.601 וב-BT.709 מסיט את הצבע בהדרגה
בתיעוד של Media Foundation, BT.601 מוסבר כמתאים ל-SDTV ומטה, ו-BT.709 כמועדף בוידאו שמעל SD.
עם זאת, לא כדאי לנחש בשקט “הרזולוציה גדולה אז זה בטח 709”. סטיית צבע לא גורמת ל-crash, ולכן קל לא לשים לב ולהכניס אותה ל-production.
ב-Media Foundation אפשר להחזיק מידע על מרחב הצבע בתוך attributes של ה-media type. לפחות כדאי לבדוק את שני אלה:
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
אם בודקים את שני אלה, ומעבירים במפורש רק את השילובים שהקוד שלנו תומך בהם, פחות סיכוי לתקלה שקטה בהמשך.
flowchart TB
accTitle: הזרימה שנמנעת מניחוש מרחב הצבע
accDescr: תרשים שמראה שניחוש בשקט אם זה 601 או 709 לפי הרזולוציה מוביל לכניסת סטיית צבע ל-production בלי לשים לב, ולכן בודקים את ה-attributes MF_MT_YUV_MATRIX ו-MF_MT_VIDEO_NOMINAL_RANGE ומעבירים רק שילוב נתמך.
gs1["ניחוש בשקט לפי הרזולוציה"] --> sl1["סטיית צבע לא עושה crash"]
sl1 --> op1["נכנסת ל-production בלי לשים לב"]
at1["בדיקת attributes של matrix ו-range"] --> ps1["מעבירים רק שילוב נתמך"]
ps1 -.-> sf1["מונע תקלה שקטה"]
איור 6: לא מנחשים את מרחב הצבע — בודקים את ה-attributes, ומעבירים במפורש רק שילובים נתמכים.
3.5. הנוסחה הראשונה לזכור היא BT.601 ב-limited range
הנוסחה הייצוגית של BT.601 ב-8-bit נראית כך:
C = Y - 16
D = U - 128
E = V - 128
R = clip(1.164383 * C + 1.596027 * E)
G = clip(1.164383 * C - 0.391762 * D - 0.812968 * E)
B = clip(1.164383 * C + 2.017232 * D)
ב-BT.709 המקדמים שונים. נציג גם אותם בהמשך בקוד.
חשוב כאן לא “לשנן מקדמים” אלא את המבנה: מ-Y מפחיתים black level 16, ומ-U/V מסתכלים סביב מרכז 128.
flowchart TB
accTitle: המבנה של נוסחת ההמרה
accDescr: תרשים שמראה שמפחיתים 16 מ-Y (black level), מפחיתים 128 מ-U ו-V (מרכז), מכפילים במקדמי ה-matrix, ומגבילים את התוצאה — זה המבנה של נוסחת ההמרה.
yy2["מפחיתים 16 מ-Y (black level)"] --> co1["מכפילים במקדמי ה-matrix"]
uv1["מפחיתים 128 מ-U / V (מרכז)"] --> co1
co1 --> cl1["clip ל-0..255"]
איור 7: מה שזוכרים זה לא המקדמים, אלא את מבנה הנוסחה: black level 16 ומרכז 128.
4. Pattern A: לתת ל-Media Foundation להמיר אוטומטית
4.1. מתי זה מתאים
השיטה הזו מתאימה למשל למצבים הבאים:
- רוצים לשלוף still אחד מ-MP4
- רוצים ליצור כמה thumbnails
- רוצים להפוך לתמונת RGB ולהעביר ל-WIC
- לא playback בזמן אמת, אלא שימוש ב-batch או בכלי
ל-Source Reader יש יכולת להריץ video processing מוגבל של YUV -> RGB32 באמצעות MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING.
עם זאת, כפי שכתוב גם ב-Microsoft Learn, זה software processing ולא מותאם ל-playback. אם רוצים לעבד מאות frames בשנייה, לא מומלץ להישען על זה.
flowchart TB
accTitle: מתאים ולא מתאים להמרה האוטומטית
accDescr: תרשים שמראה שההמרה האוטומטית של Source Reader היא software processing שלא מותאם ל-playback, ולכן מתאימה לשימושי batch כמו חילוץ still ו-thumbnail, אך לא מומלץ להישען עליה בעיבוד מאות frames בשנייה.
auto1["ההמרה האוטומטית של Source Reader"] --> sw1["software processing"]
sw1 -->|"מתאים"| bat1["still / thumbnail / batch"]
sw1 -.->|"לא נשענים על זה"| rt1["עיבוד מאות frames בשנייה בזמן אמת"]
איור 8: ההמרה האוטומטית היא software processing, ולכן מגבילים אותה לשימושי כלי במספר קטן של frames.
4.2. מה מגדירים כדי לקבל RGB32
הזרימה טבעית למדי.
- ב-attributes שמעבירים ל-
MFCreateSourceReaderFromURL, מגדיריםMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - בוחרים video stream
- ב-
SetCurrentMediaTypeמבקשיםMFMediaType_Video/MFVideoFormat_RGB32 - קוראים sample עם
ReadSample
רק בזה, ה-video processing המוגבל שנכנס אחרי ה-decoder עושה עבורנו YUV -> RGB32.
flowchart TB
accTitle: ארבעת השלבים להפעלת ההמרה האוטומטית
accDescr: תרשים שמראה שיוצרים Reader עם הפעלת video processing ב-attributes, בוחרים video stream, מבקשים RGB32, וקוראים עם ReadSample, ואז ההמרה מתבצעת אחרי ה-decoder.
a1["הפעלת video processing ב-attributes"] --> a2["בחירת video stream"]
a2 --> a3["בקשת RGB32"]
a3 --> a4["קריאה עם ReadSample"]
a4 -.-> a5["ההמרה ל-RGB32 מתבצעת אחרי ה-decoder"]
איור 9: רק אחרי ארבעת השלבים, ה-video processing שאחרי ה-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 מאפשרת לבדוק את ה-size ואת ה-stride בפועל של הפלט.
4.4. החוזק של השיטה הזו
היתרון כאן הוא בעיקר להגיע מהר לתמונה נכונה.
- לא צריך לכתוב בעצמכם את פירוק ה-4:2:0 / 4:2:2
- אפשר להסתיר במידה רבה את הטרחה של matrix / deinterlace
- קל להעביר ל-WIC או ל-GDI
- מספיק מעשי לעיבוד כמה frames
בכלים לחילוץ still, טבעי מאוד להתחיל מכאן.
4.5. יש גם pitfalls
להמרה האוטומטית הזו יש את התכונות הבאות:
| פריט | תוכן |
|---|---|
| יעד ההמרה | בעיקרון RGB32 |
| מימוש | software processing |
| שימוש מתאים | מספר קטן של frames, thumbnail, עיבוד offline |
| שימוש לא מתאים | rendering בזמן אמת מבוסס D3D, עיבוד frames בנפח גדול |
| attributes שלא מסתדרים עם זה | 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 מסודר בזיכרון עם 3 בייטים של B, G, R ובייט רביעי שהוא alpha או don't care, ולכן בטוח יותר למלא 0xFF לפני העברה ל-WIC כ-32bppBGRA.
r32["הסידור בזיכרון של RGB32"] --> bgr1["3 בייטים: B, G, R"]
r32 --> b41["הבייט הרביעי: alpha או don't care"]
b41 -->|"מילוי ב-0xFF"| wic1["אפשר להעביר ל-WIC כ-32bppBGRA"]
b41 -.->|"העברה כמו שהוא"| tr3["עלול לצאת שקוף"]
איור 10: הבייט הרביעי לא קבוע, ולכן ממלאים אותו ב-0xFF לפני העברה ל-WIC.
הנקודה הזו הוזכרה גם במאמר הקודם על חילוץ still, כאחד ה-pitfalls הנפוצים.
5. Pattern B: כותבים בעצמנו את עיבוד ההמרה
5.1. מתי זה מתאים
המרה עצמית מתאימה, למשל, למקרים הבאים:
- מעבדים frames בנפח גדול, ורוצים לייעל את ההמרה בעצמנו
- רוצים להזרים ישירות את
NV12ל-GPU או ל-SIMD - רוצים לטפל במפורש ב-
BT.601/BT.709/ ה-range - רוצים ליצור פורמט פלט שאינו
RGB32 - ההמרה האוטומטית המוגבלת של Source Reader לא מספיקה
אפשר לתאר את זה כ-pattern שבו לוקחים על עצמכם את האחריות על נפח העיבוד ועל הצבע, בתמורה לחופש פעולה.
flowchart TB
accTitle: ה-trade-off של ההמרה העצמית
accDescr: תרשים שמראה שבחירת המרה עצמית פירושה לקחת אחריות על נפח העיבוד ועל הצבע, בתמורה לחופש בייעול, בחיבור ל-GPU/SIMD, בשליטה מפורשת ב-matrix וב-range ובפורמט הפלט.
own1["בחירת המרה עצמית"] --> res1["לקיחת אחריות על נפח העיבוד ועל הצבע"]
res1 --> fr1["חופש בייעול, GPU / SIMD, פורמט הפלט"]
res1 --> fr2["שליטה מפורשת ב-matrix / range"]
איור 11: המרה עצמית היא בחירה שלוקחת אחריות בתמורה לחופש בביצועים ובצבע.
5.2. הזרימה הכוללת של ההמרה העצמית
השלבים הם כדלהלן:
- הגדרת הפלט של Source Reader ל-
NV12או ל-YUY2 - קבלת ה-subtype וה-attributes בפועל עם
GetCurrentMediaType - בדיקת
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIX,MF_MT_VIDEO_NOMINAL_RANGE - שליפת buffer מה-sample ו-lock
- חישוב ה-Y/U/V שכל pixel מצביע אליו
- הפעלת ה-matrix וכתיבה ל-BGRA
הקוד במאמר הזה מוגבל ל-SDR ב-8-bit / progressive / NV12 או YUY2 / limited range.
הגבלת ההנחות כאן היא לא קיצור דרך, אלא דבר חשוב. הסיבה היא שהמרת YUV, אם מממשים אותה כ”מקבל הכול בלי הבחנה”, נוטה לשבור צבעים בשקט.
flowchart TB
accTitle: הזרימה הכוללת של ההמרה העצמית
accDescr: תרשים שמראה שמבקשים NV12 או YUY2, בודקים את ה-media type וה-attributes בפועל, עושים lock ל-buffer, מחשבים את ה-Y/U/V לכל pixel, ומפעילים matrix לכתיבה ל-BGRA, כאשר הגבלת ההנחות מקטינה סיכוי לשבירת צבע.
f1["בקשת NV12 / YUY2"] --> f2["בדיקת ה-subtype וה-attributes בפועל"]
f2 --> f3["lock ל-buffer"]
f3 --> f4["חישוב ה-Y/U/V לכל pixel"]
f4 --> f5["הפעלת matrix וכתיבה ל-BGRA"]
f2 -.-> nar1["הגבלת ההנחות מקטינה סיכוי לשבירת צבע"]
איור 12: ההמרה העצמית מתקדמת בסדר של בקשה, בדיקה, lock, הפניה והמרה, והסיכון יורד ככל שההנחות מוגבלות יותר.
5.3. תחילה מגדירים במפורש את ה-media type של הפלט
תחילה נמסור ל-Source Reader “תוציא לנו YUV כמו שהוא”. גם כאן מניחים ש-CoInitializeEx / MFStartup כבר רצו.
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <wrl/client.h>
using Microsoft::WRL::ComPtr;
HRESULT ConfigureSourceReaderForSubtype(
IMFSourceReader* reader,
REFGUID subtype)
{
if (!reader) return E_POINTER;
HRESULT hr = reader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
if (FAILED(hr)) return hr;
hr = reader->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
if (FAILED(hr)) return hr;
ComPtr<IMFMediaType> outType;
hr = MFCreateMediaType(&outType);
if (FAILED(hr)) return hr;
hr = outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
if (FAILED(hr)) return hr;
hr = outType->SetGUID(MF_MT_SUBTYPE, subtype);
if (FAILED(hr)) return hr;
hr = reader->SetCurrentMediaType(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
nullptr,
outType.Get());
if (FAILED(hr)) return hr;
return S_OK;
}
כאן מעבירים ל-subtype את MFVideoFormat_NV12 או את MFVideoFormat_YUY2.
חשוב לשים לב שה-subtype שביקשנו לא בהכרח יתקבל כמו שהוא. מה שבאמת יוצא, בודקים עם GetCurrentMediaType.
flowchart TB
accTitle: הפרדה בין הבקשה לפלט בפועל
accDescr: תרשים שמראה שה-subtype שביקשו ב-SetCurrentMediaType לא בהכרח מתקבל כמו שהוא, ולכן בודקים עם GetCurrentMediaType מה באמת יוצא.
req1["בקשת subtype"] -.->|"לא בהכרח מתקבל כמו שהוא"| out2["הפלט בפועל"]
out2 --> gct1["בדיקה עם GetCurrentMediaType"]
gct1 --> use1["כתיבת ההמשך לפי הערך שנבדק"]
איור 13: הבקשה היא בקשה, והפלט בפועל תמיד מאושר עם GetCurrentMediaType לפני שימוש.
5.4. לפני ההמרה, מקבלים רק מידע צבע נתמך
בהמרה עצמית, קודם כל מקבלים מה-media type את המידע המינימלי שנדרש.
בדוגמה במאמר הזה, מקבלים רק 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.
בתיעוד ה-enum של Media Foundation יש רשום גם משהו כמו “Unknown מטופל כ-BT.709”, אבל בפועל, אם מעגלים את זה בשקט, קשה יותר לשים לב לסטיית צבע. לפחות במימוש הראשון, בטוח יותר להפוך שילוב לא נתמך לשגיאה.
מתי Unknown חוזר
יתכן שתחשבו “זה לא קפדני מדי?”, אז נסביר את המסלולים שבהם מתקבל Unknown. ברוב המקרים זה “הווידאו המקורי לא מחזיק מידע צבע”.
- ל-VUI של H.264 / HEVC אין מידע צבע. לפי התקן, כש-
colour_description_present_flagהוא 0, מטפלים ב-matrix_coefficientsכ”לא צוין”. אם המידע הזה חסר כשעוברים דרך ה-decoder, גם ה-matrix שמגיע במורד הזרם לא צוין - YUV גולמי שהגיע מ-capture device או מ-container ישן. מסלול שאין בו תיאור של מרחב צבע
- לפעמים אין בכלל attribute
MF_MT_YUV_MATRIX. במקרה כזהGetUINT32לא מחזיר ערך ונכשל עםMF_E_ATTRIBUTENOTFOUND(בקוד שלמעלה זה נדחה ישירות עםFAILED(hr))
חשוב כאן ש-Unknown פירושו “לא ידוע”, ולא “ידוע שזה BT.709”. אם מיישמים 709 על חומר ברזולוציית SD הצבע יסטה, וההפך גם נכון.
עם הבנה זו, יש שני כיווני מדיניות:
- דחייה strict (המדיניות במאמר הזה): מחזירים שגיאה על מה שלא נתמך, ומאפשרים ברמה גבוהה יותר להחליט “החומר הזה לא נתמך”. בטוח יותר לומר “לא ניתן לטפל בזה” מאשר לתת לצבע לסטות בשקט
- קביעת default והמשך: אם בהכרח צריך להמשיך, רושמים בלוג מה ההנחה שנעשתה כש-
Unknownהתקבל. וכך מבהירים במפורש ש”נקבע 601 / 709 לפי הרזולוציה”
בכל מקרה, הדבר היחיד שנמנעים ממנו הוא עיגול בשקט. סטיית צבע לא עושה crash, ולכן היא נכנסת ל-production בלי לשים לב.
flowchart TB
accTitle: הפיצול כש-matrix הוא Unknown
accDescr: תרשים שמראה ש-Unknown פירושו לא ידוע ולא BT.709 בוודאות, ולכן יש שתי מדיניות אפשריות: דחייה strict עם שגיאה, או המשך עם רישום ההנחה בלוג, ובכל מקרה נמנעים מעיגול בשקט.
unk1["matrix הוא Unknown = לא ידוע"] -->|"מדיניות המאמר הזה"| st4["דחייה strict עם שגיאה"]
unk1 -->|"אם חייבים להמשיך"| dflt1["המשך עם רישום ההנחה בלוג"]
unk1 -.->|"מה שנמנעים ממנו"| mute1["עיגול בשקט"]
איור 14: Unknown פירושו “לא ידוע”, ולכן בוחרים בין דחייה להמשך עם רישום, אבל לא מעגלים בשקט.
במצלמות או בפורמטים מסוג JPEG, לפעמים רוצים לטפל בנפרד במסלול שמחזיק full-range. כאן, במקום לשלב את זה בשקט, המדיניות היא להצר במפורש את ההנחות שהקוד הזה מקבל.
5.5. קוראים buffer לפי ה-stride
גם זה חשוב מאוד.
MF_MT_DEFAULT_STRIDEהוא ה-stride המינימלי- ל-sample buffer בפועל יכול להיות actual stride שכולל padding
- אם אפשר להשתמש ב-
IMF2DBuffer::Lock2D, מעדיפים אותו
אם לוקחים את דפוס ה-helper שב-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;
};
ההגדרה המומלצת של surface ל-YUV היא top-left / stride חיובי, אבל ל-buffer access בפועל בטוח יותר להשתמש ב-stride (=pitch) שה-API החזיר בפועל. אם קובעים לפי width, זה נשבר בשקט בהמשך.
flowchart TB
accTitle: סדר העדיפויות של ה-stride
accDescr: תרשים שמראה ש-MF_MT_DEFAULT_STRIDE הוא הערך המינימלי, ול-buffer בפועל עלול להיות stride שכולל padding, ולכן אם אפשר להשתמש ב-Lock2D מעדיפים את הערך שלו, ולא קובעים לפי width.
p1["actual stride שמחזיר Lock2D"] -->|"אם אפשר — עדיפות ראשונה"| acc1["הערך שמשתמשים בו ל-buffer access"]
p2["MF_MT_DEFAULT_STRIDE (ערך מינימלי)"] -->|"fallback"| acc1
p3["קביעה לפי width"] -.->|"נשבר בשקט"| acc1
איור 15: ה-stride שמשתמשים בו למעבר בין שורות הוא בעדיפות ראשונה הערך שנמדד עם Lock2D, ולא קביעה לפי width.
5.6. כותבים בקוד את נוסחת ההמרה של pixel בודד
כאן נעסוק רק ב-BT.601 וב-BT.709 ב-limited range. הפלט יהיה 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 המתאים
- עושים clip לתוצאה ל-0..255
- קובעים את הבייט הרביעי של BGRA ל-
255
5.7. ממירים NV12 ל-BGRA32
מכיוון ש-NV12 הוא 4:2:0, ארבעת ה-pixels ב-block 2x2 חולקים אותו U/V.
במימוש המינימלי ביותר, פשוט להשתמש באותו chroma משותף ישירות עבור 4 ה-pixels.
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 שורות אנכיות -> y / 2
// ה-UV plane משתמש באותו stride כמו ה-Y plane
const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);
// צד הפלט הוא BGRA packed בלי padding, לכן 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 pixels לרוחב חולקים זוג אחד, קודם מוציאים "איזה זוג" עם (x / 2),
// ומכיוון שזוג אחד = 2 bytes, מכפילים ב-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. במראה זה בדרך כלל מספיק מעשי, אבל אם רוצים לשאוף לאיכות התמונה הגבוהה ביותר, תכנון שמבצע קודם upconversion מ-4:2:0 דרך 4:2:2 ל-4:4:4, כמו במאמרי ה-YUV של Microsoft Learn, נקי יותר תיאורטית.
flowchart TB
accTitle: שני התכנונים של chroma upsampling
accDescr: תרשים שמראה שהמימוש המינימלי משתמש ב-chroma משותף ישירות עבור 4 ה-pixels בסגנון 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.
מכיוון ששני pixels חולקים זוג 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 כל 2 pixels” נראה ישירות.
בזכות זה, קל יותר לבנות מודל מנטלי מ-NV12.
5.9. נקודת הכניסה לקריאה מ-sample
לבסוף, אם שולפים 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;
}
כך, השלב הקודם יכול להיות:
- יצירת reader
- בקשת
NV12אוYUY2 - בניית
DecodedFrameInfoמ-GetCurrentMediaType ReadSampleConvertSampleToBgra32
flowchart TB
accTitle: הפיצול בפונקציית הכניסה
accDescr: תרשים שמראה ששולפים buffer רציף מ-sample, ומפצלים לפי ה-subtype - NV12 עובר להמרה ל-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. זו הצורה הכי ברורה.
עם זאת, אם רוצים להשתלב בתוך ה-pipeline של Media Foundation, יש גם תכנונים אחרים.
- כתיבת
MFTעצמאי - שימוש ב-
Video Processor MFT/ XVP - כתיבת shader מ-
NV12ל-RGB בצד ה-GPU
מכאן הנושא קצת משתנה, ולכן הפעם התמקדנו בקוד בצד האפליקציה. עם זאת, כדאי לדעת שיש נקודת ביניים בשם Video Processor MFT, בין “להשאיר ל-Media Foundation” לבין “לעשות הכול באפליקציה”.
5.11. מוודאים שההמרה נכונה
תקלות צבע קשות לזיהוי חזותי, ולכן מפרידים בין “זה רץ” ל”זה נכון” ומאמתים אותם. הסדר הוא שני שלבים.
שלב 1: מכניסים ערכים ידועים ומשווים לחישוב ידני
עדיף להעביר ל-ConvertLimitedYuvPixelToBgra ערכי Y/U/V ידועים, במקום להריץ ישר וידאו. לא צריך קובץ וידאו ולא Media Foundation.
ב-BT.601 ב-limited range, ה-Y/U/V של צבעים ייצוגיים והערך הצפוי מהצבתם בנוסחת 5.6. נראים כך:
| צבע | Y | U | V | R הצפוי | G | B |
|---|---|---|---|---|---|---|
| שחור | 16 | 128 | 128 | 0 | 0 | 0 |
| לבן | 235 | 128 | 128 | 255 | 255 | 255 |
| אדום | 81 | 90 | 240 | 254 | 0 | 0 |
| כחול | 41 | 240 | 110 | 0 | 0 | 255 |
לדוגמה עבור אדום, C = 81 - 16 = 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 — עיגול ל-6 ספרות משפיע רק מהספרה הרביעית אחרי הנקודה ואילך, ולא מופיע בפלט של 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, ההמרה של pixel בודד הזו הוצאה ל-header נפרד שאינו תלוי ב-OS, כך שהבדיקה הזו רצה גם בלי Windows.
שלב 2: משווים בין הפלט של Pattern A ל-Pattern B
אחרי שה-pixel הבודד תואם, השלב הבא הוא כל ה-frame. מאותו זמן באותו וידאו, שולפים תמונה אחת בכל אחד משני המסלולים:
- Pattern A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - Pattern B (מקבלים
NV12/YUY2וממירים בעצמנו)
ומשווים pixel-pixel.
איך קוראים את ההפרש:
לוקחים |A.R - B.R|, |A.G - B.G|, |A.B - B.B| של כל pixel
מוצאים את המקסימום, ואת אחוז ה-pixels שעברו את הסף
אל תצפו כאן להתאמה מלאה. יש לזה שתי סיבות.
- ייתכן שה-video processing בצד Source Reader מבצע chroma upsampling בשיטה שאינה nearest-neighbor. המימוש העצמי בפרק 5.7. הוא מימוש מינימלי שמשתמש ב-chroma המשותף ישירות עבור 4 pixels, ולכן ההפרש בולט יותר באזורי גבול
- הטיפול בעיגול ובדיוק ביניים שונה
לכן, מה שבודקים הוא לא “האם זה תואם”, אלא התבנית של הופעת ההפרש.
| ההפרש שנצפה | מה חושדים בו |
|---|---|
| האזורים השטוחים תואמים, ההפרש רק בגבולות הצבע | ההבדל ב-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: תרשים שמראה שקודם משווים pixel בודד עם ערכים ידועים לחישוב ידני, ואם זה תואם, משווים frames מלאים משני המסלולים באותו זמן באותו וידאו, וממקדים חשד לפי צורת ההפרש.
st5["שלב 1: השוואת pixel בודד לחישוב ידני"] --> st6["שלב 2: השוואת frames משני המסלולים"]
st6 --> shp1["ממקדים חשד לפי צורת ההפרש"]
st5 -.-> osf1["לא נדרש וידאו ולא Media Foundation"]
איור 19: אחרי אימות pixel בודד, השוואת frames שלמים ממקדת את מקור ההפרש לפי צורתו.
6. באיזה pattern לבחור
בהיסוס, הטבלה הבאה מסייעת מאוד להתמקד.
| היבט | המרה אוטומטית (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
המרה עצמית |
|---|---|---|
| מהירות המימוש | ◎ | △ |
| חילוץ כמה stills | ◎ | ○ |
| עיבוד בנפח גדול / realtime | △ | ◎ |
| שליטה מפורשת ב-matrix / range | △ | ◎ |
| שילוב עם GPU / D3D | △ | ○〜◎ |
פלט שאינו RGB32 |
△ | ◎ |
| הבנת העקרונות | ○ | ◎ |
כמדד ראשון, נוח לחשוב כך:
- רוצים שזה פשוט יעבוד ← המרה אוטומטית
- רוצים לקחת אחריות על הצבע ועל הביצועים ← המרה עצמית
בפועל, גם הסדר “קודם לוודא תמונה נכונה עם המרה אוטומטית, ואחר כך להחליף ל-manual path” יעיל למדי. אם לוקחים על עצמכם הכול מההתחלה, קשה יותר לדעת היכן התמונה נשברה.
flowchart TB
accTitle: הסדר המעשי היעיל
accDescr: תרשים שמראה שאם קודם מוודאים תמונה נכונה עם ההמרה האוטומטית ורק אז עוברים ל-manual path, קל יותר לבודד היכן התמונה נשברה, בעוד שנטילת הכול מההתחלה מקשה על כך.
step1["קודם מוודאים תמונה נכונה עם המרה אוטומטית"] --> step2["אחר כך עוברים ל-manual path"]
step2 --> good1["קל יותר לבודד היכן נשבר"]
allin1["נטילת הכול מההתחלה"] -.-> lost1["קשה לדעת היכן נשבר"]
איור 20: אם קודם מבטיחים תמונה נכונה ואז עוברים למימוש עצמי, קל יותר לבודד את מקום התקלה.
7. pitfalls נפוצים בפועל
7.1. הנחה מוטעית ש-RGB32 הוא RGBA עם alpha
RGB32 בזיכרון הוא B, G, R, Alpha או Don't Care.
אם שומרים אותו ישירות כ-BGRA ל-PNG, לפעמים הבייט הרביעי הוא 0 והתמונה שקופה. בטוח יותר למלא 0xFF לפני השמירה.
7.2. קביעת ה-stride לפי width * bytesPerPixel
זו תקלה נפוצה למדי. מכיוון של-sample buffer בפועל יכול להיות padding, העיקרון הוא להשתמש ב-actual stride למעבר בין שורות.
7.3. בלבול בין MF_MT_DEFAULT_STRIDE ל-actual pitch
MF_MT_DEFAULT_STRIDE הוא “ה-stride המינימלי כשמייצגים את הפורמט הזה בזיכרון רציף”.
ל-actual pitch של ה-sample buffer מעדיפים את הערך שמחזיר IMF2DBuffer::Lock2D.
(pitch הוא שם אחר ל-stride. כפי שהוזכר בפרק 3.2., במאמר הזה משתמשים בהם באותה משמעות.)
7.4. ניחוש בשקט של 601 / 709 בלי לבדוק color metadata
תקלות צבע קשה לראות. הן גם לא גורמות ל-crash. לכן הן מטרידות.
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
לפחות אלה, כדאי לבדוק. ובנוסף, נכון להתייחס בגישה של להפוך ערכים שהקוד שלכם לא תומך בהם לשגיאה.
7.5. חיתוך UV plane של NV12 לפי width * height
היסט ה-plane נקבע לפי ה-stride וה-height בפועל. לא לפי width * height.
אם עושים את זה ברשלנות, הצבע נסטה או שהתמונה נשברת.
flowchart TB
accTitle: איך קובעים את גבול ה-plane ב-NV12
accDescr: תרשים שמראה שתחילת ה-UV plane ב-NV12 נקבעת לפי מכפלת ה-stride וה-height בפועל, וחיתוך לפי מכפלת width ו-height מוביל לסטיית צבע ולתמונה שבורה.
head1["תחילת ה-buffer (Y plane)"] -->|"מתקדמים stride × height"| uv2["תחילת ה-UV plane"]
wr1["חיתוך לפי width × height"] -.-> brk1["סטיית צבע / תמונה שבורה"]
איור 21: גבול ה-UV plane נקבע לפי stride × height, לא לפי width × height.
7.6. טיפול ב-interlaced video בהנחת progressive
הדוגמה הידנית במאמר הזה מניחה progressive. אם קוראים interlaced ישירות כ-field בודד, לפעמים מופיע combing. אם דרוש deinterlace, טבעי יותר לשקול את ה-video processing האוטומטי של Source Reader או את Video Processor MFT.
7.7. התעלמות מאיכות ה-chroma upsampling ב-4:2:0
ההמרה של NV12 במאמר הזה, לטובת בהירות, היא בצורה של שימוש ישיר ב-chroma המשותף עבור כל pixel. לפי צורך זה מספיק, אבל אם רוצים איכות תמונה, כדאי לעקוב אחר תפיסת ה-upconversion שבחומרי הפורמט המומלץ של YUV.
8. סיכום
כשממירים YUV ל-RGB עם Media Foundation, אם מחזיקים את הסידור הבא מראש, קל הרבה יותר לא להתבלבל:
- מאחורי ה-decoder, בדרך כלל יוצא לא RGB אלא
NV12אוYUY2 - אם רוצים נוחות, מבקשים
RGB32עםMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING - אם רוצים לשלוט, מקבלים
NV12/YUY2וממירים בעצמכם ל-BGRA - ב-manual path, לפני הנוסחה עצמה, תופסים sampling / range / matrix / stride
- אם
BT.601/BT.709,16..235,4:2:0/4:2:2נשארים מעורפלים, מתקבלת סטיית צבע או תמונה שבורה
היחס בין YUV ל-RGB קצת קשה להיאחז בו בהתחלה. אבל ברגע שהתמונה הבאה נכנסת לראש:
NV12חולק U/V ב-2x2YUY2חולק U/V בשני pixels לרוחב- מפעילים matrix על אותו U/V ועל Y
זה נעשה הרבה יותר טבעי. רצף הבייטים המסתורי מתחיל להיראות כ-pixels בעלי משמעות אמיתית.
flowchart TB
accTitle: התמונה שכדאי להחזיק בראש
accDescr: תרשים שמראה ש-NV12 חולק U/V ב-2x2, YUY2 חולק U/V בשני pixels לרוחב, ומפעילים matrix על אותו U/V ועל Y, וכך רצף הבייטים המסתורי נראה כ-pixels בעלי משמעות.
im2["NV12: חלוקת U/V ב-2x2"] --> ap1["הפעלת matrix על אותו U/V ועל Y"]
im3["YUY2: חלוקת U/V בשני pixels לרוחב"] --> ap1
ap1 --> see1["רצף הבייטים נראה כ-pixels בעלי משמעות"]
איור 22: עם תמונה של יחידת השיתוף ומקדם ה-matrix, רצף הבייטים של ה-YUV נקרא בטבעיות.
9. מקורות
קוד הדוגמה של המאמר הזה
מאמרים קשורים של KomuraSoft
- מבוא ל-Media Foundation: להבין את ה-API דרך COM
- איך לחלץ תמונת סטילס מ-MP4 בזמן נתון עם Media Foundation
Microsoft Learn
- Source Reader
- Using the Source Reader to Process Media Data
- MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING attribute
- IMFSourceReader::SetCurrentMediaType
- Recommended 8-Bit YUV Formats for Video Rendering
- Extended Color Information
- Uncompressed Video Buffers
- IMF2DBuffer::Lock2D
- MF_MT_VIDEO_NOMINAL_RANGE attribute
- MFVideoTransferMatrix enumeration
- Video Processor MFT
- Uncompressed RGB Video Subtypes
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
Time Travel Debugging — להקליט ולהריץ אחורה באגים שלא משתחררים באפליקציות ארוכות-ריצה
באג פעם בחודש משאיר ב-crash dump רק את התוצאה. מקליטים ומריצים אחורה את הביצוע עם WinDbg Time Travel Debugging (TTD): TTD.exe, ring buffe...
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
מה נשאר אחרי שה-parent מת — מחזיקים child processes ב-Job Object
למה SDK helpers שורדים UI שנהרג ומחזיקים את המצלמה או את ה-COM port? מתכננים משך חיים של child process עם Job Objects, KillOnJobClose ו-c...
Win32 Thread Pool API — מקביליות בלי CreateThread, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד ה-native? המאמר מסביר את ה-Win32 Thread Pool API שעוצב מחדש ב-Vista: ארבעת האובייקטים work, timer, wa...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
מדריך מעשי ל-Named Pipes, ה-IPC הסטנדרטי ב-Windows. המאמר מסדר לפי מקורות ראשוניים את הבחירה בין byte mode ל-message mode, תכנון server ל...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
זה מימוש של עיבוד מדיה ב-Windows: Media Foundation, Source Reader, שמירת תמונות והמרת video frames, ולכן זה יושב טוב בפיתוח אפליקציות Windows.
ייעוץ טכני וסקירת תכנון
אם רוצים ליישר קו מראש על חלוקת האחריות בין YUV ל-RGB, מרחב הצבע, stride ותכנון מסלול ההמרה, זה נושא שמתאים לייעוץ טכני ול-design review.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- למה ה-decoder של Media Foundation מוציא YUV ולא RGB?
- כי העין רגישה לפירוט של בהירות יותר מאשר לפירוט של צבע, ולכן בוידאו משתלם לתכנן Y (רכיב שקרוב לבהירות) ברזולוציה גבוהה, ו-U/V (רכיבי color difference) גסים יותר. בקטגוריית הווידאו של Windows, ה-frames הלא-דחוסים שיוצאים מה-decoder הם בדרך כלל פורמט YUV כמו NV12 או YUY2. בהקשר של וידאו דיגיטלי, YUV מתייחס בפועל ל-Y'CbCr, וכך קל יותר לקרוא את התיעוד.
- מה הדרך הפשוטה ביותר לקבל frame ב-RGB?
- להפעיל MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING ב-IMFSourceReader ולבקש MFVideoFormat_RGB32. זו הדרך הקלה ביותר לחילוץ כמה stills או ליצירת thumbnail. אבל ההמרה האוטומטית הזו היא software processing שלא מותאם ל-playback בזמן אמת, ולכן בעיבוד בנפח גדול או כשרוצים שליטה בצבע, מקבלים את ה-YUV כמו שהוא וממירים בעצמכם.
- על מה צריך להיזהר כשממירים YUV ל-RGB בעצמכם?
- זה לא נגמר בשלושה מקדמים. מעורבים כאן chroma 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 יושבים לסירוגין, וארבעה pixels ב-block 2x2 חולקים זוג U/V אחד. YUY2 הוא פורמט 4:2:2, שבו שני pixels לרוחב חולקים U/V. שני הפורמטים נפוצים מאוד בפועל, וההבדל הוא איך מדללים את הצבע (chroma subsampling).