איך ממירים YUV ל-RGB עם Media Foundation

· עודכן בתאריך: · · Media Foundation, C++, פיתוח Windows, עיבוד וידאו, YUV

רוצים לחלץ מסגרת מווידאו ולשמור כ-PNG, להעביר ל-WIC או ל-GDI, או להציג ב-UI. במצבים כאלה, צד היישום רוצה רצף פיקסלים בפורמט RGB.

עם זאת, המסגרת שיוצאת מהמפענח (decoder) של Media Foundation היא בדרך כלל בפורמט מסוג YUV כמו NV12 או YUY2. אם מטפלים ברצף הבייטים הגולמי כאילו הוא תמונה כמו שהיא, מתקבלת תמונה עצובה — צבעים מקולקלים, פסים, גוון ירקרק משונה.

במאמר מבוא ל-Media Foundation - הבנת ה-API מנקודת מבט של COM שכתבנו קודם סידרנו את התמונה הכוללת, ובמאמר איך לחלץ תמונת סטילס מ-MP4 בזמן נתון עם Media Foundation סידרנו את חילוץ תמונות הסטילס. הפעם נעסוק בהמרת YUV -> RGB עצמה, שנמצאת בדרך שביניהם.

במאמר הזה נסדר בנפרד את שתי התבניות הבאות:

  • תבנית A: לתת ל-IMFSourceReader להוביל אוטומטית עד ל-RGB32
  • תבנית B: לקבל NV12 /‏ YUY2 ולהמיר ל-RGB בעצמנו

המטרה היא לא לזכור שמות API. המטרה היא לבנות תמונה מנטלית להיכן ב-Media Foundation מופיע YUV, והיכן הוא הופך ל-RGB.

בנוסף, הקוד שמופיע במאמר הזה זמין כסט קוד לדוגמה שלם (קוד C++ לתבנית A ולתבנית B, הרכב CMake, בדיקות המרת פיקסלים) ב-GitHub.

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

סביבת ההנחות להרצה

אם רוצים להעביר את הקוד מהמאמר הזה לפרויקט שלכם, כל מה שצריך הוא זה:

פריט הנחה
מערכת הפעלה Windows 10 ואילך
מהדר MSVC של Visual Studio 2019 / 2022 (‏C++17)
SDK ‏Windows SDK (header-ים וספריות ייבוא של Media Foundation). כלול בעומס העבודה “פיתוח שולחני עם C++” של Visual Studio
build הדוגמה בנויה ב-CMake 3.20 ואילך. אפשר גם ליצור פרויקט Visual Studio ידנית

יש לקשר ארבע ספריות. בקוד המאמר זה נכתב עם #pragma comment(lib, ...), אבל אפשר גם לציין זאת דרך הגדרות הפרויקט.

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

שימו לב שהקוד במאמר הזה מניח ש-CoInitializeEx ו-MFStartup כבר בוצעו. נוסחת ההמרה של פיקסל בודד (5.6.) בלבד אינה תלויה במערכת ההפעלה, ולכן בדוגמה ב-GitHub היא הוצאה ל-header נפרד, כך שאפשר לבדוק אותה גם עם g++ ב-Linux.

1. קודם המסקנה

נסכם קודם רק את המסקנה:

  • לחילוץ כמה תמונות סטילס או יצירת thumbnail, הכי קל להפעיל את MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING ולבקש MFVideoFormat_RGB32
  • אבל ההמרה האוטומטית הזו היא עיבוד בתוכנה, שלא מותאם לניגון בזמן אמת
  • אם כותבים המרה עצמית, הדרך הקצרה ביותר היא להבין היטב קודם את NV12 ו-YUY2
  • YUV -> RGB זה לא “מכפלה בשלושה מקדמים וזהו” — בפועל מעורבים דגימת צבע מופחתת, range ו-matrix
  • תיעוד Media Foundation משתמש באופן רחב במילה YUV, אבל בוידאו דיגיטלי היא בפועל מתייחסת ל-Y’CbCr, וכך קל יותר להבין
  • מה שנוטה לקלקל צבעים בעבודה מעשית הוא אי-בדיקה של MF_MT_YUV_MATRIX ו-MF_MT_VIDEO_NOMINAL_RANGE, וגם הנחה מוטעית ש-stride שווה ל-width * bytesPerPixel

בקיצור, אם רוצים בנוחות — נותנים ל-Source Reader להוציא RGB32. אם רוצים עיבוד מסיבי ושליטה בצבע — מקבלים YUV וממירים בעצמכם. אלה שתי האפשרויות.

שתי האפשרויות של המאמר הזהתרשים המראה שאם רוצים בנוחות, נותנים ל-Source Reader להוציא RGB32, ואם רוצים עיבוד מסיבי ושליטה בצבע, מקבלים YUV וממירים בעצמכם.רוצים בנוחותעיבוד מסיבי / שליטה בצבערוצים מסגרת RGBתבנית A: Source Reader מוציא RGB32תבנית B: מקבלים YUV וממירים בעצמנו

איור 1: אם בוחרים נוחות — תבנית A, ואם לוקחים אחריות על נפח העיבוד והצבע — תבנית B.

מפת הידע של המאמר

המאמר הזה יוצא מהעובדה שהפריימים הלא-דחוסים שיוצאים מה-decoder של Media Foundation הם בדרך כלל מסוג Y’CbCr כמו NV12 או YUY2 ולא RGB, ומסדר שני מסלולים להמרה ל-RGB. הפעלת ‏MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING ב-Source Reader ובקשת RGB32 היא המרה אוטומטית נוחה, אך מתבצעת ב-software ואינה מתאימה לניגון בזמן אמת; כאשר נדרש עיבוד בכמות גדולה או שליטה בצבע, עדיף לקבל NV12/YUY2 ולהמיר אותם בעצמכם ל-BGRA. בהמרה עצמאית יש צורך לשחזר את ה-chroma subsampling, ולבדוק את MF_MT_YUV_MATRIX (‏BT.601/BT.709) ואת ‏MF_MT_VIDEO_NOMINAL_RANGE שב-IMFMediaType כדי לבחור נכון את המקדמים ואת הטווח, וכן לא לקבוע את ה-stride לפי width כפול bytesPerPixel אלא להשתמש בערך האמיתי שמחזיר ‏IMF2DBuffer::Lock2D.

מפת הידע של המרת YUV ל-RGB ב-Media Foundationתרשים המראה שני מסלולים להמרת פריימים מסוג Y'CbCr כמו NV12/YUY2, שיוצא מה-decoder של Media Foundation, ל-RGB, ואת הקשר למכשולי היישום: chroma subsampling, matrix, nominal range ו-stride.מחייבמוגדר באמצעותמוגדר באמצעותמשתמש במשתמש במשתמש במשתמש במשתמש במממש אתמשתמש במענה מומלץ למחייבנבדק באמצעותמענה מומלץ למשתמש במשתמש במוגדר באמצעותמחייבמשתמש במשתמש במשתמש במשתמש במשתמש בהמרת YUV ל-RGBפורמט הפיקסלים NV12פורמט הפיקסלים YUY2דגימת צבע מופחתת (4:4:4/4:2:2/4:2:0)מאפיין MF_MT_YUV_MATRIXמאפיין MF_MT_VIDEO_NOMINAL_RANGEY'CbCrIMFSourceReader(Source Reader)MF_SOURCE_READER_ENABLE_VIDEO_PROCESSINGMFVideoFormat_RGB32Video Processor MFT‏stride (מספר הבייטים בשורת תמונה)IMF2DBuffer::Lock2DIMFMediaTypeIMFAttributesBT.601BT.709Media Foundation

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 23, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

2. קודם רואים בציור

תחילה, נוח יותר לראות בציור מה קורה בתוך Media Foundation.

מסלול ה-YUV אל ה-RGBתרשים המראה שקובץ מקור עובר דרך המפענח למסגרת YUV, ומשם מפוצל למסלול A דרך עיבוד וידאו של Source Reader ל-RGB32, או למסלול B דרך קוד המרה עצמי ל-BGRA/RGB.תבנית Aתבנית BMP4 / H.264 / HEVCמפענחמסגרת YUV מסוג NV12 / YUY2 / YV12 וכדומהעיבוד הוידאו של Source ReaderRGB32קוד המרה עצמיBGRA / RGB

איור 2: מה שהמפענח מוציא הוא מסגרת YUV, ומשם הדרך אל RGB מתפצלת לתבנית A ולתבנית B.

אם תוכן קובץ הוידאו הוא פורמט דחוס כמו H.264 או HEVC, המפענח הופך אותו קודם למסגרת לא-דחוסה. המסגרת הלא-דחוסה הזו לא בהכרח RGB. להפך, בקטגוריית הוידאו של Windows, מקובל שזה מסוג YUV.

לכן, כשהיישום רוצה RGB, בוחרים באחת משתי האפשרויות:

  1. לתת לצד Media Foundation להוביל עד RGB32
  2. לקבל YUV ולהפוך אותו ל-RGB בקוד שלנו

הנושא של המאמר הזה הוא בדיוק נקודת ההסתעפות הזו.

3. קודם מסדרים את היחס בין YUV ל-RGB

3.1. אומרים YUV אבל בפועל מדובר ב-Y’CbCr

שמות ה-API והתיעוד של Windows משתמשים באופן רחב במילה YUV. אבל בהקשר של וידאו דיגיטלי, אפשר כמעט תמיד לקרוא U כ-Cb ו-V כ-Cr.

באופן גס:

  • Y הוא רכיב שקרוב יותר לבהירות
  • U /‏ V הם רכיבי הפרש צבע
  • RGB הוא כזה שבו כל פיקסל מחזיק ישירות Red /‏ Green /‏ Blue

זה היחס.

העין האנושית רגישה יותר לפירוט הבהירות מאשר לפירוט הצבע. לכן בוידאו, תכנון של Y מדויק, ו-U/V גסים מעט יעיל. זו הסיבה שפורמטים מסוג YUV נפוצים כל כך.

הסיבה לשימוש בפורמטים מסוג YUVתרשים המראה שהעין האנושית רגישה יותר לפירוט הבהירות מאשר לפירוט הצבע, ולכן תכנון שבו Y מדויק ו-U/V גסים יעיל, וזו הסיבה שפורמטים מסוג YUV נפוצים.העין האנושית רגישה לבהירותY מדויק, U/V גסיםהסיבה לשימוש בפורמטים מסוג YUV

איור 3: בהתאם לתכונת העין שרגישה לבהירות, התכנון של YUV מבוסס על Y מדויק ו-U/V גסים.

3.2. 4:4:4 /‏ 4:2:2 /‏ 4:2:0 זה “עד כמה מדללים את הצבע”

זו הנקודה המרכזית בקריאת YUV.

סימון משמעות דוגמה מייצגת
4:4:4 לכל פיקסל יש Y/U/V משלו AYUV,‏ I444
4:2:2 שני פיקסלים לרוחב חולקים U/V YUY2,‏ UYVY,‏ I422
4:2:0 ‏2x2 פיקסלים חולקים U/V NV12,‏ YV12,‏ I420

נוח לראות תחילה את הצורה של שני הפורמטים הנפוצים ביותר בעבודה מעשית.

לפני זה נקבע מונח אחד. stride (בשם אחר, pitch) הוא מספר הבייטים לשורה אחת. זה לא רוחב התמונה עצמו, אלא “כמה בייטים מתקדמים עד תחילת השורה הבאה”, כולל 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 בייטים ----------->
  <--- W --->
 +-----------+---------------------+  --+
 | Y Y Y Y Y | (padding)           |    |
 | Y Y Y Y Y | (padding)           |    | Y plane
 | Y Y Y Y Y | (padding)           |    | S * H בייטים
 | 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, 4 הפיקסלים בבלוק 2x2 חולקים זוג U/V אחד. ל-Y יש ערך לכל פיקסל. ‏plane ה-UV משתמש באותו stride כמו plane ה-Y, אך מספר השורות הוא מחצית. לכן גבול ה-plane הוא S * H, ולא W * H (נחזור לזה בפרק 7.5.).

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

  <-------------- S בייטים ---------------->
  <------- W * 2 בייטים ------->
 +-----------------------------+----------+
 | 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 פיקסלים = 4 בייטים (Y, U, Y, V)
  יש plane אחד בלבד (packed, אין גבול)

ב-YUY2, 2 פיקסלים לרוחב חולקים זוג U/V אחד. ‏Y0 ו-Y1 נפרדים, אבל U0 ו-V0 משותפים. מכיוון שזה packed, אין צורך בחישוב גבול plane, אבל עדיין משתמשים ב-stride למעבר בין שורות.

בשלב הזה כבר רואים שהמרת YUV -> RGB אינה החלפה פשוטה של פיקסל-אחר-פיקסל. קודם צריך לחשוב איזה U/V המשותף מוקצה לאיזה פיקסל.

השוואת יחידת השיתוף בין NV12 ל-YUY2תרשים המראה ש-NV12 מחלק זוג U/V אחד בין 4 פיקסלים בבלוק 2x2, ו-YUY2 מחלק זוג U/V בין 2 פיקסלים לרוחב, ולכן צריך לקבוע מראש איזה פיקסל מקבל איזה U/V.NV12 (4:2:0)4 פיקסלים ב-2x2 חולקים זוג U/VYUY2 (4:2:2)2 פיקסלים לרוחב חולקים זוג U/Vקובעים איזה פיקסל מקבל איזה U/V

איור 4: בשני הפורמטים ה-U/V משותף, ולכן ההמרה לא מסתכמת בהחלפה פר-פיקסל.

3.3. YUV -> RGB זה “המרת מרחב צבע + המרת דגימה”

אם מסתכלים ב-Extended Color Information של Media Foundation, המרת הצבע המדויקת כוללת הרבה שלבים. inverse quantization, chroma upsampling,‏ YUV -> RGB,‏ transfer function, המרת primaries ועד quantization.

עם זאת, כקוד מעשי ל-SDR ב-8 סיביות, נוח יותר להבין דרך חלוקה לשלוש שכבות:

  1. החזרת הדגימה הרחבת ה-U/V הדללים של 4:2:0 או 4:2:2 לצורה שכל פיקסל יכול לגשת אליה
  2. החזרת ה-range ה-Y בוידאו בדרך כלל 16..235, וה-U/V בדרך כלל 16..240, ולכן צריך להחזיר את הסקאלה
  3. הפעלת ה-matrix המרה ל-RGB באמצעות מקדמים כמו BT.601 או BT.709

כלומר, המרת YUV -> RGB בעבודה מעשית היא תהליך שקובע:

  • איזה U/V הוא הצבע של אותו פיקסל
  • באילו מקדמים מחזירים את אותו Y/U/V ל-RGB
שלוש השכבות שקוד מעשי צריך לתפוסתרשים המראה שקוד מעשי ל-SDR ב-8 סיביות מבוסס על שלוש שכבות: החזרת הדגימה, החזרת ה-range, והפעלת ה-matrix, וכך קל יותר להבין את ההמרה מ-YUV ל-RGB.מסגרת YUVהחזרת הדגימההחזרת ה-range (למשל 16..235)הפעלת ה-matrix (601 / 709)RGB

איור 5: ההמרה לא מסתכמת בשלושה מקדמים — היא מורכבת משלוש שכבות: דגימה, range ו-matrix.

3.4. טיפול רשלני ב-BT.601 וב-BT.709 מסיט את הצבע בהדרגה

בתיעוד של Media Foundation, BT.601 מוסבר כמתאים ל-SDTV ומטה, ו-BT.709 כמועדף בוידאו שמעל SD.

עם זאת, לא כדאי לנחש בשקט “הרזולוציה גדולה אז זה בטח 709”. סטיית צבע לא גורמת לקריסה, ולכן קל להישאר לא מודעים אליה ולהכניס אותה לתפעול.

ב-Media Foundation אפשר להחזיק מידע על מרחב הצבע בתוך תכונות סוג המדיה. לכל הפחות, כדאי לבדוק את שני אלה:

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

אם בודקים את שני אלה, ומעבירים במפורש רק את השילובים שהקוד שלנו תומך בהם, פחות סיכוי לתקלה שקטה בהמשך.

הזרימה שנמנעת מניחוש מרחב הצבעתרשים המראה שניחוש בשקט אם זה 601 או 709 לפי הרזולוציה מוביל לכניסת סטיית צבע לתפעול בלי לשים לב, ולכן בודקים את התכונות MF_MT_YUV_MATRIX ו-MF_MT_VIDEO_NOMINAL_RANGE ומעבירים רק שילוב תואם.ניחוש שקט לפי הרזולוציהסטיית צבע לא קורסתנכנסת לתפעול בלי לשים לבבדיקת התכונות matrix ו-rangeמעבירים רק שילוב תואםמונע תקלה שקטה

איור 6: לא מנחשים את מרחב הצבע — בודקים את התכונות, ומעבירים רק שילובים תואמים במפורש.

3.5. הנוסחה הראשונה לזכור היא BT.601 ב-limited range

הנוסחה הייצוגית של BT.601 ב-8 סיביות היא כזו:

C = Y - 16
D = U - 128
E = V - 128

R = clip(1.164383 * C + 1.596027 * E)
G = clip(1.164383 * C - 0.391762 * D - 0.812968 * E)
B = clip(1.164383 * C + 2.017232 * D)

ב-BT.709 המקדמים שונים. נציג גם אותם בהמשך בקוד.

חשוב כאן לא “לשנן מקדמים” אלא את המבנה: מ-Y מפחיתים רמת שחור 16, ומ-U/V מסתכלים סביב מרכז 128.

המבנה של נוסחת ההמרהתרשים המראה שמפחיתים 16 מ-Y (רמת שחור), מפחיתים 128 מ-U ו-V (מרכז), מכפילים במקדמי ה-matrix, ומגבילים את התוצאה — זה המבנה של נוסחת ההמרה.מפחיתים 16 מ-Y (רמת שחור)מכפילים במקדמי ה-matrixמפחיתים 128 מ-U / V (מרכז)מגבילים ל-0..255

איור 7: מה שזוכרים זה לא המקדמים, אלא את מבנה הנוסחה: רמת שחור 16 ומרכז 128.

4. תבנית A: לתת ל-Media Foundation להמיר אוטומטית

4.1. באילו מצבים זה מתאים

השיטה הזו מתאימה למשל למצבים הבאים:

  • רוצים לחלץ תמונה אחת מ-MP4
  • רוצים ליצור כמה תמונות thumbnail
  • רוצים להפוך לתמונת RGB ולהעביר ל-WIC
  • לא ניגון בזמן אמת, אלא שימוש ב-batch או בכלי

ל-Source Reader יש יכולת להריץ עיבוד וידאו מוגבל של YUV -> RGB32 באמצעות MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING.

עם זאת, כפי שכתוב גם ב-Microsoft Learn, זה עיבוד בתוכנה ולא מותאם ל-playback. אם רוצים לעבד מאות מסגרות בשנייה, לא מומלץ להישען על זה.

מתאים ולא מתאים להמרה האוטומטיתתרשים המראה שההמרה האוטומטית של Source Reader היא עיבוד בתוכנה שלא מותאם ל-playback, ולכן מתאימה לשימושי batch כמו חילוץ תמונת סטילס וthumbnail, אך לא מומלץ להישען עליה בעיבוד מאות מסגרות בשנייה.מתאיםלא נשענים על זהההמרה האוטומטית של Source Readerעיבוד בתוכנהתמונת סטילס / thumbnail / batchעיבוד מאות מסגרות בשנייה בזמן אמת

איור 8: ההמרה האוטומטית היא עיבוד בתוכנה, ולכן מגבילים אותה לשימושי כלי במספר קטן של מסגרות.

4.2. מה מגדירים כדי לקבל RGB32

הזרימה טבעית למדי.

  1. ב-attributes שמעבירים ל-MFCreateSourceReaderFromURL, מגדירים MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE
  2. בוחרים stream וידאו
  3. ב-SetCurrentMediaType מבקשים MFMediaType_Video /‏ MFVideoFormat_RGB32
  4. קוראים sample עם ReadSample

רק בזה, עיבוד הוידאו המוגבל שנכנס אחרי המפענח עושה עבורנו YUV -> RGB32.

ארבעת השלבים להפעלת ההמרה האוטומטיתתרשים המראה שיוצרים Reader עם הפעלת עיבוד וידאו ב-attributes, בוחרים stream וידאו, מבקשים RGB32, וקוראים עם ReadSample, ואז ההמרה מתבצעת אחרי המפענח.הפעלת עיבוד וידאו ב-attributesבחירת stream וידאובקשת RGB32קריאה עם ReadSampleההמרה ל-RGB32 מתבצעת אחרי המפענח

איור 9: רק אחרי ארבעת השלבים, עיבוד הוידאו שאחרי המפענח מוביל עד RGB32.

4.3. הקוד

הקוד הבא מניח ש-CoInitializeEx ו-MFStartup כבר בוצעו. בהרכב מינימלי, זה נראה בערך כך:

#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <wrl/client.h>

#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")

using Microsoft::WRL::ComPtr;

HRESULT CreateSourceReaderWithAutoRgb(
    const wchar_t* path,
    IMFSourceReader** ppReader)
{
    if (!path || !ppReader) return E_POINTER;
    *ppReader = nullptr;

    ComPtr<IMFAttributes> attrs;
    HRESULT hr = MFCreateAttributes(&attrs, 2);
    if (FAILED(hr)) return hr;

    hr = attrs->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
    if (FAILED(hr)) return hr;

    hr = MFCreateSourceReaderFromURL(path, attrs.Get(), ppReader);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetStreamSelection(MF_SOURCE_READER_FIRST_VIDEO_STREAM, TRUE);
    if (FAILED(hr)) return hr;

    ComPtr<IMFMediaType> outType;
    hr = MFCreateMediaType(&outType);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr)) return hr;

    hr = outType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr)) return hr;

    hr = (*ppReader)->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        outType.Get());
    if (FAILED(hr)) return hr;

    return S_OK;
}

HRESULT ReadOneRgb32Sample(
    IMFSourceReader* reader,
    IMFSample** ppSample,
    LONGLONG* pTimestamp100ns)
{
    if (!reader || !ppSample) return E_POINTER;
    *ppSample = nullptr;
    if (pTimestamp100ns) *pTimestamp100ns = 0;

    DWORD streamIndex = 0;
    DWORD flags = 0;
    LONGLONG timestamp = 0;

    HRESULT hr = reader->ReadSample(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        0,
        &streamIndex,
        &flags,
        &timestamp,
        ppSample);

    if (FAILED(hr)) return hr;
    if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
    if (*ppSample == nullptr) return MF_E_INVALID_STREAM_DATA;

    if (pTimestamp100ns) *pTimestamp100ns = timestamp;
    return S_OK;
}

אחרי זה, קריאה ל-GetCurrentMediaType מאפשרת לבדוק את ה-size ואת ה-stride בפועל של הפלט.

4.4. החוזק של השיטה הזו

היתרון בשיטה הזו הוא בעיקר להתקרב מהר לתמונה נכונה.

  • לא צריך לכתוב בעצמכם את פירוק ה-4:2:0 /‏ 4:2:2
  • אפשר להסתיר במידה רבה את הטרחה של matrix / deinterlace
  • קל להעביר ל-WIC או ל-GDI
  • מספיק מעשי לעיבוד כמה מסגרות

בכלים לחילוץ תמונת סטילס, טבעי מאוד להתחיל מכאן.

4.5. יש גם מלכודות

להמרה האוטומטית הזו יש את התכונות הבאות:

פריט תוכן
יעד ההמרה בעיקרון RGB32
מימוש עיבוד בתוכנה
שימוש מתאים מספר קטן של מסגרות, thumbnail, עיבוד לא-מקוון
שימוש לא מתאים rendering בזמן אמת מבוסס D3D, עיבוד מסיבי של מסגרות
תכונות שלא מסתדרות עם זה MF_SOURCE_READER_D3D_MANAGER,‏ MF_READWRITE_DISABLE_CONVERTERS

ועוד דבר חשוב הוא הטיפול בבייט הרביעי של RGB32. ‏RGB32 ב-Windows בזיכרון מסודר כ-Blue /‏ Green /‏ Red /‏ Alpha או Don’t Care. זה לא ARGB32. אם מעבירים ל-WIC כ-32bppBGRA, בטוח יותר למלא את הבייט הרביעי ב-0xFF כדי לקבל אטימות.

הטיפול בבייט הרביעי של RGB32תרשים המראה ש-RGB32 ב-Windows מסודר בזיכרון עם 3 בייטים של B, G, R ובייט רביעי שהוא alpha או don't care, ולכן בטוח יותר למלא 0xFF לפני העברה ל-WIC כ-32bppBGRA.מילוי ב-0xFFהעברה כמו שהואהסידור בזיכרון של RGB323 בייטים: B, G, Rהבייט הרביעי: alpha או don't careאפשר להעביר ל-WIC כ-32bppBGRAעלול לצאת שקוף

איור 10: הבייט הרביעי לא קבוע, ולכן ממלאים אותו ב-0xFF לפני העברה ל-WIC.

הנקודה הזו הוזכרה גם במאמר הקודם על חילוץ תמונת סטילס, כאחת ממלכודות נפוצות.

5. תבנית B: כותבים בעצמנו את עיבוד ההמרה

5.1. באילו מצבים זה מתאים

המרה עצמית מתאימה, למשל, למקרים הבאים:

  • מעבדים מסיבי של מסגרות, ורוצים לייעל את ההמרה בעצמנו
  • רוצים להזרים ישירות את NV12 ל-GPU או ל-SIMD
  • רוצים לטפל במפורש ב-BT.601 /‏ BT.709 / ה-range
  • רוצים ליצור פורמט פלט שאינו RGB32
  • ההמרה האוטומטית המוגבלת של Source Reader לא מספיקה

אפשר לתאר את זה כתבנית שבה לוקחים על עצמכם את אחריות נפח העיבוד והצבע, בתמורה לחופש פעולה.

הסחר של ההמרה העצמיתתרשים המראה שבחירת המרה עצמית פירושה לקחת אחריות על נפח העיבוד והצבע, בתמורה לחופש בייעול, בחיבור ל-GPU/SIMD, בשליטה במפורש ב-matrix וב-range ובפורמט הפלט.בחירת המרה עצמיתלקיחת אחריות על נפח העיבוד והצבעחופש בייעול, GPU / SIMD, פורמט הפלטשליטה מפורשת ב-matrix / range

איור 11: המרה עצמית היא בחירה שלוקחת אחריות בתמורה לחופש בביצועים ובצבע.

5.2. הזרימה הכוללת של ההמרה העצמית

השלבים הם כדלהלן:

  1. הגדרת הפלט של Source Reader ל-NV12 או ל-YUY2
  2. קבלת ה-subtype והתכונות בפועל עם GetCurrentMediaType
  3. בדיקת MF_MT_FRAME_SIZE,‏ MF_MT_DEFAULT_STRIDE,‏ MF_MT_YUV_MATRIX,‏ MF_MT_VIDEO_NOMINAL_RANGE
  4. שליפת buffer מה-sample ונעילתו
  5. חישוב ה-Y/U/V שכל פיקסל מפנה אליו
  6. הפעלת ה-matrix וכתיבה ל-BGRA

הקוד במאמר הזה מוגבל ל-SDR ב-8 סיביות /‏ progressive /‏ NV12 או YUY2 /‏ limited range. הגבלת ההנחות כאן היא לא קיצור דרך, אלא דבר חשוב. הסיבה היא שהמרת YUV, אם מיישמים אותה כ”מקבל הכול בלי הבחנה”, נוטה לקלקל צבעים בשקט.

הזרימה הכוללת של ההמרה העצמיתתרשים המראה שמבקשים NV12 או YUY2, בודקים את סוג המדיה והתכונות בפועל, נועלים את ה-buffer, מחשבים את ה-Y/U/V לכל פיקסל, ומפעילים matrix לכתיבה ל-BGRA, כאשר הגבלת ההנחות מקטינה סיכוי לקלקול צבע.בקשת NV12 / YUY2בדיקת ה-subtype והתכונות בפועלנעילת ה-bufferחישוב ה-Y/U/V לכל פיקסלהפעלת matrix וכתיבה ל-BGRAהגבלת ההנחות מקטינה סיכוי לקלקול צבע

איור 12: ההמרה העצמית מתקדמת בסדר של בקשה, בדיקה, נעילה, הפניה והמרה, ומצטמצם הסיכון ככל שההנחות מוגבלות יותר.

5.3. תחילה מגדירים במפורש את 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.

הפרדה בין הבקשה לפלט בפועלתרשים המראה שה-subtype שביקשו ב-SetCurrentMediaType לא בהכרח מתקבל כמו שהוא, ולכן בודקים עם GetCurrentMediaType מה באמת יוצא.לא בהכרח מתקבל כמו שהואבקשת subtypeהפלט בפועלבדיקה עם GetCurrentMediaTypeכתיבת ההמשך לפי הערך שנבדק

איור 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 כ”לא צוין”. אם המידע הזה חסר כשעוברים דרך המפענח, גם ה-matrix שמגיע במורד הזרם לא צוין
  • YUV גולמי שהגיע ממכשיר לכידה או ממכל ישן. מסלול שאין בו תיאור מרחב צבע
  • לפעמים אין בכלל תכונה MF_MT_YUV_MATRIX. במקרה כזה GetUINT32 לא מחזיר ערך ונכשל עם MF_E_ATTRIBUTENOTFOUND (בקוד שלמעלה זה נדחה ישירות עם FAILED(hr))

חשוב כאן ש-Unknown פירושו “לא ידוע”, ולא “ידוע שזה BT.709”. אם מיישמים 709 על חומר ברזולוציית SD הצבע יסטה, וההפך גם נכון.

עם הבנה זו, יש שני כיווני מדיניות:

  • דחייה strict (המדיניות במאמר הזה): מחזירים שגיאה על מה שלא נתמך, ומאפשרים ברמה גבוהה יותר להחליט “החומר הזה לא נתמך”. בטוח יותר לומר “לא ניתן לטפל בזה” מאשר לתת לצבע לסטות בשקט
  • קביעת ברירת מחדל והמשך: אם בהכרח צריך להמשיך, רושמים ביומן מה ההנחה שנעשתה כש-Unknown התקבל. וכך מבהירים במפורש ש”נקבע 601 /‏ 709 לפי הרזולוציה”

בכל מקרה, הדבר היחיד שנמנעים ממנו הוא עיגול שקט. סטיית צבע לא קורסת, ולכן היא נכנסת לתפעול בלי לשים לב.

ההסתעפות כש-matrix הוא Unknownתרשים המראה ש-Unknown פירושו לא ידוע ולא BT.709 בוודאות, ולכן יש שתי מדיניות אפשריות: דחייה strict עם שגיאה, או המשך עם רישום ההנחה ביומן, ובכל מקרה נמנעים מעיגול שקט.מדיניות המאמר הזהאם חייבים להמשיךמה שנמנעים ממנוmatrix הוא Unknown = לא ידועדחייה strict עם שגיאההמשך עם רישום ההנחה ביומןעיגול שקט

איור 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, זה נשבר בשקט בהמשך.

סדר העדיפויות של ה-strideתרשים המראה ש-MF_MT_DEFAULT_STRIDE הוא הערך המינימלי, ול-buffer בפועל עלול להיות stride שכולל padding, ולכן אם אפשר להשתמש ב-Lock2D מעדיפים את הערך שלו, ולא קובעים לפי width.אם אפשר — עדיפות ראשונהfallbackנשבר בשקטactual stride שמחזיר Lock2Dהערך שמשתמשים בו ל-buffer accessMF_MT_DEFAULT_STRIDE (ערך מינימלי)קביעה לפי width

איור 15: ה-stride שמשתמשים בו למעבר בין שורות הוא בעדיפות ראשונה הערך שנמדד עם Lock2D, ולא קביעה לפי width.

5.6. כותבים בקוד את נוסחת ההמרה של פיקסל בודד

כאן נעסוק רק ב-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 המתאים
  • מגבילים את התוצאה ל-0..255
  • קובעים את הבייט הרביעי של BGRA ל-255

5.7. ממירים NV12 ל-BGRA32

מכיוון ש-NV12 הוא 4:2:0, 4 הפיקסלים בבלוק 2x2 חולקים אותו U/V. במימוש המינימלי ביותר, פשוט להשתמש באותו chroma משותף ישירות עבור 4 הפיקסלים.

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;

    // תחילת plane ה-UV נמצאת בעמדה שמתקדמת "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
        // plane ה-UV משתמש באותו stride כמו plane ה-Y
        const BYTE* uvRow = uvPlane + static_cast<size_t>(actualStride) * (y / 2);

        // צד הפלט הוא BGRA דחוס בלי 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];

            // ב-plane ה-UV, U ו-V מסודרים לסירוגין.
            // מכיוון ש-2 פיקסלים לרוחב חולקים זוג אחד, קודם מוציאים "איזה זוג" עם (x / 2),
            // ומכיוון שזוג אחד = 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. במראה זה בדרך כלל מספיק מעשי, אבל אם רוצים לשאוף לאיכות התמונה הגבוהה ביותר, תכנון שמבצע קודם upconversion מ-4:2:0 דרך 4:2:2 ל-4:4:4, כמו במאמרי ה-YUV של Microsoft Learn, נקי יותר תיאורטית.

שני התכנונים של chroma upsamplingתרשים המראה שהמימוש המינימלי משתמש בchroma משותף ישירות עבור 4 הפיקסלים בסגנון nearest-neighbor ומעשי מספיק ברוב המקרים, בעוד שהעדפת איכות דורשת upconversion מ-4:2:0 דרך 4:2:2 ל-4:4:4.מימוש מינימלי: שימוש ישיר ב-chroma המשותףמספיק מעשי ברוב המקריםהמרה אחרי upconversionנקי יותר תיאורטית, לטובת איכותבסדר 4:2:0 → 4:2:2 → 4:4:4

איור 16: ההבדל בין המימוש המינימלי שמשתמש ב-chroma המשותף לבין תכנון שמבצע upconversion בהדרגה.

5.8. ממירים YUY2 ל-BGRA32

YUY2 הוא 4:2:2 מסוג packed. מכיוון ששני פיקסלים חולקים זוג U/V אחד, קל מעט יותר לקרוא מ-NV12.

#include <cstddef>

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

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

    BufferLock lock(buffer);

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

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

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

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

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

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

            src += 4;
        }
    }

    lock.Unlock();
    return S_OK;
}

ב-YUY2 הבייטים מסודרים כ-Y0 U Y1 V, כך שהמבנה “משתמשים חוזר ב-U/V כל 2 פיקסלים” נראה ישירות. בזכות זה, קל יותר לבנות מודל מנטלי מ-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
  • ReadSample
  • ConvertSampleToBgra32
ההסתעפות בפונקציית הכניסהתרשים המראה ששולפים buffer רציף מ-sample, ומפצלים לפי ה-subtype - NV12 עובר להמרה לNV12, YUY2 עובר להמרה לYUY2, ואחר מקבל שגיאה.NV12YUY2אחרIMFSampleשליפת buffer רציףהמרה עבור NV12המרה עבור YUY2שגיאה

איור 17: אחרי המרה ל-buffer רציף בכניסה, מפצלים לפי ה-subtype, וכל מה שלא נתמך נופל בפשטות לשגיאה.

צד הקריאה בפועל יכול להיראות כך:

ComPtr<IMFMediaType> currentType;
HRESULT hr = reader->GetCurrentMediaType(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    &currentType);
if (FAILED(hr)) return hr;

DecodedFrameInfo info;
hr = GetStrictDecodedFrameInfo(currentType.Get(), &info);
if (FAILED(hr)) return hr;

DWORD flags = 0;
LONGLONG timestamp = 0;
ComPtr<IMFSample> sample;

hr = reader->ReadSample(
    MF_SOURCE_READER_FIRST_VIDEO_STREAM,
    0,
    nullptr,
    &flags,
    &timestamp,
    &sample);
if (FAILED(hr)) return hr;
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) return MF_E_END_OF_STREAM;
if (!sample) return MF_E_INVALID_STREAM_DATA;

std::vector<BYTE> bgra;
hr = ConvertSampleToBgra32(sample.Get(), info, bgra);
if (FAILED(hr)) return hr;

// bgra ניתן לטיפול כ-top-down / 32bpp BGRA

5.10. איפה ממקמים “כתיבת המרה עצמית”

הקוד עד כאן הוא צורה שבה צד היישום ממיר אחרי Source Reader. זו הצורה הכי ברורה.

עם זאת, אם רוצים להשתלב בתוך צינור העיבוד (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 סיביות, השבר הזה נמחק ומתקבל Y = 81. ה-0.481 שאבד, בזמן ההחזרה הופך לירידה של בערך 0.481 × 1.164383 ≒ 0.56. 255 − 0.56 = 254.44 — זה המקור למספר 254.44 שלמעלה. גם עם מקדמים בדיוק אינסופי מתקבל 254.44 — עיגול ל-6 ספרות משפיע רק מהספרה הרביעית אחרי הנקודה ואילך, ולא מופיע בפלט של 8 סיביות.

גם אופן ההפיכה הסופית למספר שלם משפיע על התוצאה. ClampToByte בפרק 5.6. מגביל ל-[0, 255] ואז מחתך value + 0.5, כלומר עיגול. עם חיתוך פשוט (static_cast<BYTE>(value)), האדום הזה עדיין 254, אבל ערכים קרובים לגבול כמו B = 255.04 בכחול או R = 0.38 באדום ייסטו ב-1. לפני השוואה למימוש אחר, בדקו איזו שיטה הוא משתמש בה.

כלומר, הסיבה לסבילות של הפרש של 1-2 אינה “דיוק המקדמים שונה”, אלא שתי הסיבות: “השבר שנשמט בדגימה” ו”מדיניות ההפיכה למספר שלם שונה בין מימושים”. במילים אחרות, הפרש שלא מוסבר בשתי אלה הוא תקלה אמיתית. אדום שיוצא 250, אדום וכחול שמתחלפים, אזורי צל שדולקים לבד — הפרשים כאלה מצדיקים חשד בהנחות ההמרה (בלבול בין BT.601 ל-BT.709, בלבול בין full range ל-limited range, החלפה בין U ל-V, קריאה שגויה של ה-stride), ולא בדיוק המקדמים. אם מפטרים את זה בתור “בעיית דיוק”, מפספסים תקלה אמיתית שאפשר לתקן.

הגבול בין הפרש סביר לתקלה אמיתיתתרשים המראה שהפרש של פלוס מינוס 1 עד 2 מוסבר על ידי שבר שנשמט בדגימה ומדיניות ההפיכה למספר שלם, וכל הפרש שלא מוסבר בכך הוא תקלה אמיתית שמצדיקה חשד בהנחות ההמרה.הפרש של ±1〜2הפרש שלא מוסברבדיקת ההפרש מהערך הצפוימוסבר על ידי שבר בדגימה ומדיניות ההפיכהתקלה אמיתיתחושדים בהנחות matrix, range, U/V, stride

איור 18: הפרש קטן מוסבר על ידי הדגימה וההפיכה למספר שלם, וכל מה שלא מוסבר בכך מצדיק חשד בהנחות.

לצורך בדיקה, מספיקה הצורה הבאה:

#include <cstdlib>  // std::abs

// בודקים אם ההפרש מהערך הצפוי בתוך tolerance.
// בערך הצפוי כותבים את "הצבע התיאורטי" (אדום = 255, 0, 0). השבר שנשמט בדגימה,
// והשוני במדיניות ההפיכה למספר שלם, נספגים ב-tolerance. דיוק המקדמים אינו הסיבה
static bool CheckPixel(
    BYTE y, BYTE u, BYTE v,
    MFVideoTransferMatrix matrix,
    int expectedR, int expectedG, int expectedB,
    int tolerance = 2)
{
    BYTE bgra[4] = {};
    if (FAILED(ConvertLimitedYuvPixelToBgra(y, u, v, matrix, bgra)))
    {
        return false;
    }

    return std::abs(static_cast<int>(bgra[2]) - expectedR) <= tolerance
        && std::abs(static_cast<int>(bgra[1]) - expectedG) <= tolerance
        && std::abs(static_cast<int>(bgra[0]) - expectedB) <= tolerance
        && bgra[3] == 255;  // ה-alpha חייב להיות אטום
}

// אופן השימוש (BT.601 limited range)
// CheckPixel(16, 128, 128, MFVideoTransferMatrix_BT601, 0, 0, 0);      // שחור
// CheckPixel(235, 128, 128, MFVideoTransferMatrix_BT601, 255, 255, 255); // לבן
// CheckPixel(81, 90, 240, MFVideoTransferMatrix_BT601, 255, 0, 0);     // אדום (בנוסחה R=254)
// CheckPixel(41, 240, 110, MFVideoTransferMatrix_BT601, 0, 0, 255);    // כחול

אפשר לעשות את אותו הדבר גם עם BT.709. המקדמים שונים, ולכן גם הערכים של Y/U/V שונים. לדוגמה, האדום של BT.709 הוא Y=63, U=102, V=240. אם מזרימים את הערכים של 601 ישירות להסתעפות של 709, הצבע יסטה, ולכן שווה לבדוק זאת בשורה נפרדת כדי לתפוס בלבול matrix בו במקום.

בדוגמה ב-GitHub, ההמרה של פיקסל בודד הזו הוצאה ל-header נפרד שאינו תלוי במערכת ההפעלה, כך שהבדיקה הזו רצה גם בלי Windows.

שלב 2: משווים בין הפלט של תבנית A לתבנית B

אחרי שהפיקסל הבודד תואם, השלב הבא הוא כל המסגרת. מאותו זמן באותו וידאו, מחלצים תמונה אחת בכל אחד משני המסלולים:

  1. תבנית A (‏MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING + RGB32)
  2. תבנית B (מקבלים NV12 /‏ YUY2 וממירים בעצמנו)

ומשווים פיקסל-פיקסל.

איך קוראים את ההפרש:
  לוקחים |A.R - B.R|, |A.G - B.G|, |A.B - B.B| של כל פיקסל
  מוצאים את המקסימום, ואת אחוז הפיקסלים שעברו את הסף

אל תצפו כאן להתאמה מלאה. יש לזה שתי סיבות.

  • ייתכן שעיבוד הוידאו בצד Source Reader מבצע chroma upsampling בשיטה שאינה nearest-neighbor. המימוש העצמי בפרק 5.7. הוא מימוש מינימלי שמשתמש ב-chroma המשותף ישירות עבור 4 פיקסלים, ולכן ההפרש בולט יותר באזורי גבול
  • הטיפול בעיגול ובדיוק ביניים שונה

לכן, מה שבודקים הוא לא “האם זה תואם”, אלא התבנית של הופעת ההפרש.

ההפרש שנצפה מה חושדים בו
האזורים השטוחים תואמים, ההפרש רק בגבולות הצבע ההבדל ב-chroma upsampling. צפוי
כל התמונה נסטה באופן אחיד בלבול ב-matrix (601 /‏ 709) או ב-range (16..235 /‏ 0..255)
פסים, סטייה אלכסונית קביעה שגויה של ה-stride. ראו 7.2. ו-7.5.
אדום וכחול מתחלפים בלבול בין BGRA ל-RGBA
הכול נראה שקוף / שחור לגמרי הבייט הרביעי לא מולא ב-0xFF. ראו 7.1.

אם רואים את “הצורה” של ההפרש, קל יותר לצמצם היכן לחשוד. אם ההפרש אחיד על כל התמונה, זה הנוסחה או מידע הצבע; אם הוא מקומי, זה אינדקס או stride.

שני שלבי האימותתרשים המראה שקודם משווים פיקסל בודד עם ערכים ידועים לחישוב ידני, ואם זה תואם, משווים מסגרות מלאות משני המסלולים באותו זמן באותו וידאו, וממקדים חשד לפי צורת ההפרש.שלב 1: השוואת פיקסל בודד לחישוב ידנישלב 2: השוואת מסגרות משני המסלוליםממקדים חשד לפי צורת ההפרשלא נדרש וידאו ולא Media Foundation

איור 19: אחרי אימות פיקסל בודד, השוואת מסגרות שלמות ממקדת את מקור ההפרש לפי צורתו.

6. באיזה תבנית לבחור

בהיסוס, הטבלה הבאה מסייעת מאוד להתמקד.

היבט המרה אוטומטית (‏MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING‏) המרה עצמית
מהירות המימוש
חילוץ כמה תמונות סטילס
עיבוד מסיבי / זמן אמת
שליטה מפורשת ב-matrix / range
שילוב עם GPU / D3D ○〜◎
פלט שאינו RGB32
הבנת העקרונות

כמדד ראשון, נוח לחשוב כך:

  • רוצים שזה פשוט יעבוד ← המרה אוטומטית
  • רוצים לקחת אחריות על הצבע והביצועים ← המרה עצמית

בעבודה מעשית, גם הסדר “קודם לוודא תמונה נכונה עם המרה אוטומטית, ואחר כך להחליף למסלול ידני” יעיל למדי. אם לוקחים על עצמכם הכול מההתחלה, קשה יותר לדעת היכן התמונה נשברה.

הסדר המעשי היעילתרשים המראה שאם קודם מוודאים תמונה נכונה עם ההמרה האוטומטית ורק אז עוברים למסלול הידני, קל יותר לבודד היכן התמונה נשברה, בעוד שנטילת הכול מההתחלה מקשה על כך.קודם מוודאים תמונה נכונה עם המרה אוטומטיתאחר כך עוברים למסלול הידניקל יותר לבודד היכן נשברנטילת הכול מההתחלהקשה לדעת היכן נשבר

איור 20: אם קודם מבטיחים תמונה נכונה ואז עוברים למימוש עצמי, קל יותר לבודד את מקום התקלה.

7. מלכודות נפוצות בעבודה מעשית

7.1. הנחה מוטעית ש-RGB32 הוא RGBA עם alpha

RGB32 בזיכרון הוא B, G, R, Alpha או Don't Care. אם שומרים אותו ישירות כ-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

תקלות צבע קשה לראות. הן גם לא גורמות לקריסה. לכן הן מטרידות.

  • MF_MT_YUV_MATRIX
  • MF_MT_VIDEO_NOMINAL_RANGE

לפחות אלה, כדאי לבדוק. ובנוסף, נכון להתייחס בגישה של להפוך ערכים שהקוד שלכם לא תומך בהם לשגיאה.

7.5. חיתוך plane ה-UV של NV12 לפי width * height

היסט ה-plane נקבע לפי ה-stride וה-height בפועל. לא לפי width * height. אם עושים את זה ברשלנות, הצבע נסטה או שהתמונה נשברת.

איך קובעים את גבול ה-plane ב-NV12תרשים המראה שתחילת plane ה-UV ב-NV12 נקבעת לפי מכפלת ה-stride וה-height בפועל, וחיתוך לפי מכפלת width ו-height מוביל לסטיית צבע ולתמונה שבורה.מתקדמים stride × heightתחילת המאגר (Y plane)תחילת plane ה-UVחיתוך לפי width × heightסטיית צבע / תמונה שבורה

איור 21: גבול plane ה-UV נקבע לפי stride × height, לא לפי width × height.

7.6. טיפול ב-interlaced video בהנחת progressive

הדוגמה הידנית במאמר הזה מניחה progressive. אם קוראים interlaced ישירות כ-field בודד, לפעמים מופיע רעש בצורת מסרק. אם דרוש deinterlace, טבעי יותר לשקול את עיבוד הוידאו האוטומטי של Source Reader או את Video Processor MFT.

7.7. התעלמות מאיכות ה-chroma upsampling ב-4:2:0

ההמרה של NV12 במאמר הזה, לטובת בהירות, היא בצורה של שימוש ישיר ב-chroma המשותף עבור כל פיקסל. לפי צורך זה מספיק, אבל אם רוצים איכות תמונה, כדאי לעקוב אחר תפיסת ה-upconversion שבחומרי הפורמט המומלץ של YUV.

8. סיכום

כשממירים YUV ל-RGB עם Media Foundation, אם מחזיקים את הסידור הבא מראש, קל הרבה יותר לא להתבלבל:

  • מאחורי המפענח, בדרך כלל יוצא לא RGB אלא NV12 או YUY2
  • אם רוצים בנוחות, מבקשים RGB32 עם MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING
  • אם רוצים לשלוט, מקבלים NV12 /‏ YUY2 וממירים בעצמכם ל-BGRA
  • במסלול הידני, לפני הנוסחה עצמה, תופסים sampling /‏ range /‏ matrix / stride
  • אם BT.601 /‏ BT.709,‏ 16..235,‏ 4:2:0 /‏ 4:2:2 נשארים מעורפלים, מתקבלת סטיית צבע או תמונה שבורה

היחס בין YUV ל-RGB קצת קשה להיאחז בו בהתחלה. אבל ברגע שהתמונה הבאה נכנסת לראש:

  • NV12 חולק U/V ב-2x2
  • YUY2 חולק U/V בשני פיקסלים לרוחב
  • מפעילים matrix על אותו U/V ועל Y

זה נעשה הרבה יותר טבעי. רצף הבייטים המסתורי מתחיל להיראות כפיקסלים בעלי משמעות אמיתית.

התמונה שכדאי להחזיק בראשתרשים המראה שNV12 חולק U/V ב-2x2, YUY2 חולק U/V בשני פיקסלים לרוחב, ומפעילים matrix על אותו U/V ועל Y, וכך רצף הבייטים המסתורי נראה כפיקסלים בעלי משמעות.NV12: חלוקת U/V ב-2x2הפעלת matrix על אותו U/V ועל YYUY2: חלוקת U/V בשני פיקסלים לרוחברצף הבייטים נראה כפיקסלים בעלי משמעות

איור 22: עם תמונה של יחידת השיתוף ומקדם ה-matrix, רצף הבייטים של ה-YUV נקרא בטבעיות.

9. מקורות

קוד הדוגמה של המאמר הזה

מאמרים קשורים של KomuraSoft

Microsoft Learn

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

למה המפענח של Media Foundation מוציא YUV ולא RGB?
כי העין האנושית רגישה לפירוט הבהירות יותר מלפירוט הצבע, ולכן בוידאו יעיל תכנון שבו Y (רכיב שמושפע יותר מבהירות) מדויק, ו-U/V (רכיבי הפרש צבע) גסים יותר. לכן בקטגוריית הוידאו של Windows, המסגרות הלא-דחוסות שיוצאות מהמפענח הן בדרך כלל בפורמט מסוג YUV כמו NV12 או YUY2. שווה לציין שבהקשר של וידאו דיגיטלי, YUV למעשה מתייחס ל-Y'CbCr, וכך קל יותר להבין את הנושא.
מה הדרך הפשוטה ביותר לקבל מסגרת RGB?
להפעיל את MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING ב-IMFSourceReader ולבקש MFVideoFormat_RGB32. זו הדרך הקלה ביותר לחילוץ כמה תמונות סטילס או ליצירת thumbnail. אבל ההמרה האוטומטית הזו היא עיבוד בתוכנה שלא מותאם לניגון בזמן אמת, ולכן בעיבוד מסיבי או כשרוצים לשלוט בצבע, מקבלים את ה-YUV כמו שהוא וממירים בעצמכם.
מה כדאי לשים לב אליו כשממירים בעצמנו מ-YUV ל-RGB?
זה לא נגמר במכפלה בשלושה מקדמים — יש מעורבות של דגימת צבע מופחתת (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, שבו אחרי plane ה-Y מגיע plane של UV שבו U ו-V מסודרים לסירוגין, וארבעה פיקסלים בבלוק 2x2 חולקים זוג U/V אחד. YUY2 הוא פורמט 4:2:2, שבו שני פיקסלים לרוחב חולקים U/V. שני הפורמטים נפוצים מאוד בעבודה מעשית, וההבדל ביניהם הוא אופן דילול הצבע (subsampling).

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג