איך ממירים YUV ל-RGB עם Media Foundation
· עודכן בתאריך: · Go Komura · 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.libmfreadwrite.libmfuuid.libole32.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 וממירים בעצמכם. אלה שתי האפשרויות.
flowchart TB
accTitle: שתי האפשרויות של המאמר הזה
accDescr: תרשים המראה שאם רוצים בנוחות, נותנים ל-Source Reader להוציא RGB32, ואם רוצים עיבוד מסיבי ושליטה בצבע, מקבלים YUV וממירים בעצמכם.
want1["רוצים מסגרת RGB"] -->|"רוצים בנוחות"| pa1["תבנית A: Source Reader מוציא RGB32"]
want1 -->|"עיבוד מסיבי / שליטה בצבע"| pb1["תבנית 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.
flowchart LR
accTitle: מפת הידע של המרת YUV ל-RGB ב-Media Foundation
accDescr: תרשים המראה שני מסלולים להמרת פריימים מסוג Y'CbCr כמו NV12/YUY2, שיוצא מה-decoder של Media Foundation, ל-RGB, ואת הקשר למכשולי היישום: chroma subsampling, matrix, nominal range ו-stride.
yuv_to_rgb_conversion["המרת YUV ל-RGB"]
nv12_pixel_format["פורמט הפיקסלים NV12"]
yuy2_pixel_format["פורמט הפיקסלים YUY2"]
chroma_subsampling["דגימת צבע מופחתת (4:4:4/4:2:2/4:2:0)"]
yuv_matrix_attribute["מאפיין MF_MT_YUV_MATRIX"]
nominal_range_attribute["מאפיין MF_MT_VIDEO_NOMINAL_RANGE"]
ycbcr["Y'CbCr"]
imfsourcereader["IMFSourceReader(Source Reader)"]
mf_video_processing_auto_conversion["MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING"]
mfvideoformat_rgb32["MFVideoFormat_RGB32"]
video_processor_mft["Video Processor MFT"]
video_stride["stride (מספר הבייטים בשורת תמונה)"]
imf2dbuffer_lock2d["IMF2DBuffer::Lock2D"]
imfmediatype["IMFMediaType"]
imfattributes["IMFAttributes"]
bt601["BT.601"]
bt709["BT.709"]
media_foundation["Media Foundation"]
yuv_to_rgb_conversion -->|"מחייב"| chroma_subsampling
yuv_to_rgb_conversion -.->|"מוגדר באמצעות"| yuv_matrix_attribute
yuv_to_rgb_conversion -.->|"מוגדר באמצעות"| nominal_range_attribute
nv12_pixel_format -->|"משתמש ב"| chroma_subsampling
yuy2_pixel_format -->|"משתמש ב"| chroma_subsampling
nv12_pixel_format -->|"משתמש ב"| ycbcr
yuy2_pixel_format -->|"משתמש ב"| ycbcr
imfsourcereader -.->|"משתמש ב"| mf_video_processing_auto_conversion
mf_video_processing_auto_conversion -->|"מממש את"| yuv_to_rgb_conversion
mf_video_processing_auto_conversion -->|"משתמש ב"| mfvideoformat_rgb32
video_processor_mft -.->|"מענה מומלץ ל"| yuv_to_rgb_conversion
yuv_to_rgb_conversion -->|"מחייב"| video_stride
video_stride -.->|"נבדק באמצעות"| imf2dbuffer_lock2d
imf2dbuffer_lock2d -->|"מענה מומלץ ל"| yuv_to_rgb_conversion
imfsourcereader -->|"משתמש ב"| imfmediatype
imfmediatype -->|"משתמש ב"| imfattributes
video_stride -.->|"מוגדר באמצעות"| imfmediatype
nv12_pixel_format -->|"מחייב"| video_stride
yuv_matrix_attribute -->|"משתמש ב"| bt601
yuv_matrix_attribute -->|"משתמש ב"| bt709
media_foundation -.->|"משתמש ב"| imfsourcereader
yuv_to_rgb_conversion -.->|"משתמש ב"| nv12_pixel_format
yuv_to_rgb_conversion -.->|"משתמש ב"| yuy2_pixel_format
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 23, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
2. קודם רואים בציור
תחילה, נוח יותר לראות בציור מה קורה בתוך Media Foundation.
flowchart LR
accTitle: מסלול ה-YUV אל ה-RGB
accDescr: תרשים המראה שקובץ מקור עובר דרך המפענח למסגרת YUV, ומשם מפוצל למסלול A דרך עיבוד וידאו של Source Reader ל-RGB32, או למסלול B דרך קוד המרה עצמי ל-BGRA/RGB.
File["MP4 / H.264 / HEVC"] --> Decoder["מפענח"]
Decoder --> YUV["מסגרת YUV מסוג NV12 / YUY2 / YV12 וכדומה"]
YUV -->|תבנית A| SRVP["עיבוד הוידאו של Source Reader"]
SRVP --> RGB1["RGB32"]
YUV -->|תבנית B| App["קוד המרה עצמי"]
App --> RGB2["BGRA / RGB"]
איור 2: מה שהמפענח מוציא הוא מסגרת YUV, ומשם הדרך אל RGB מתפצלת לתבנית A ולתבנית B.
אם תוכן קובץ הוידאו הוא פורמט דחוס כמו H.264 או HEVC, המפענח הופך אותו קודם למסגרת לא-דחוסה. המסגרת הלא-דחוסה הזו לא בהכרח 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הם רכיבי הפרש צבעRGBהוא כזה שבו כל פיקסל מחזיק ישירות Red / Green / Blue
זה היחס.
העין האנושית רגישה יותר לפירוט הבהירות מאשר לפירוט הצבע. לכן בוידאו, תכנון של Y מדויק, ו-U/V גסים מעט יעיל. זו הסיבה שפורמטים מסוג YUV נפוצים כל כך.
flowchart TB
accTitle: הסיבה לשימוש בפורמטים מסוג YUV
accDescr: תרשים המראה שהעין האנושית רגישה יותר לפירוט הבהירות מאשר לפירוט הצבע, ולכן תכנון שבו Y מדויק ו-U/V גסים יעיל, וזו הסיבה שפורמטים מסוג YUV נפוצים.
eye1["העין האנושית רגישה לבהירות"] --> dsn1["Y מדויק, U/V גסים"]
dsn1 --> why1["הסיבה לשימוש בפורמטים מסוג YUV"]
איור 3: בהתאם לתכונת העין שרגישה לבהירות, התכנון של YUV מבוסס על Y מדויק ו-U/V גסים.
3.2. 4:4:4 / 4:2:2 / 4:2:0 זה “עד כמה מדללים את הצבע”
זו הנקודה המרכזית בקריאת YUV.
| סימון | משמעות | דוגמה מייצגת |
|---|---|---|
| 4:4:4 | לכל פיקסל יש Y/U/V משלו | AYUV, I444 |
| 4:2:2 | שני פיקסלים לרוחב חולקים U/V | YUY2, UYVY, I422 |
| 4:2:0 | 2x2 פיקסלים חולקים U/V | NV12, YV12, I420 |
נוח לראות תחילה את הצורה של שני הפורמטים הנפוצים ביותר בעבודה מעשית.
לפני זה נקבע מונח אחד. stride (בשם אחר, pitch) הוא מספר הבייטים לשורה אחת. זה לא רוחב התמונה עצמו, אלא “כמה בייטים מתקדמים עד תחילת השורה הבאה”, כולל 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 המשותף מוקצה לאיזה פיקסל.
flowchart TB
accTitle: השוואת יחידת השיתוף בין NV12 ל-YUY2
accDescr: תרשים המראה ש-NV12 מחלק זוג U/V אחד בין 4 פיקסלים בבלוק 2x2, ו-YUY2 מחלק זוג U/V בין 2 פיקסלים לרוחב, ולכן צריך לקבוע מראש איזה פיקסל מקבל איזה U/V.
nv1["NV12 (4:2:0)"] --> sh1["4 פיקסלים ב-2x2 חולקים זוג U/V"]
yy1["YUY2 (4:2:2)"] --> sh2["2 פיקסלים לרוחב חולקים זוג U/V"]
sh1 --> asn1["קובעים איזה פיקסל מקבל איזה U/V"]
sh2 --> asn1
איור 4: בשני הפורמטים ה-U/V משותף, ולכן ההמרה לא מסתכמת בהחלפה פר-פיקסל.
3.3. YUV -> RGB זה “המרת מרחב צבע + המרת דגימה”
אם מסתכלים ב-Extended Color Information של Media Foundation, המרת הצבע המדויקת כוללת הרבה שלבים. inverse quantization, chroma upsampling, YUV -> RGB, transfer function, המרת primaries ועד quantization.
עם זאת, כקוד מעשי ל-SDR ב-8 סיביות, נוח יותר להבין דרך חלוקה לשלוש שכבות:
- החזרת הדגימה הרחבת ה-U/V הדללים של 4:2:0 או 4:2:2 לצורה שכל פיקסל יכול לגשת אליה
- החזרת ה-range ה-Y בוידאו בדרך כלל 16..235, וה-U/V בדרך כלל 16..240, ולכן צריך להחזיר את הסקאלה
- הפעלת ה-matrix
המרה ל-RGB באמצעות מקדמים כמו
BT.601אוBT.709
כלומר, המרת YUV -> RGB בעבודה מעשית היא תהליך שקובע:
- איזה U/V הוא הצבע של אותו פיקסל
- באילו מקדמים מחזירים את אותו Y/U/V ל-RGB
flowchart TB
accTitle: שלוש השכבות שקוד מעשי צריך לתפוס
accDescr: תרשים המראה שקוד מעשי ל-SDR ב-8 סיביות מבוסס על שלוש שכבות: החזרת הדגימה, החזרת ה-range, והפעלת ה-matrix, וכך קל יותר להבין את ההמרה מ-YUV ל-RGB.
y1["מסגרת YUV"] --> up1["החזרת הדגימה"]
up1 --> rg1["החזרת ה-range (למשל 16..235)"]
rg1 --> mx1["הפעלת ה-matrix (601 / 709)"]
mx1 --> rgb2["RGB"]
איור 5: ההמרה לא מסתכמת בשלושה מקדמים — היא מורכבת משלוש שכבות: דגימה, range ו-matrix.
3.4. טיפול רשלני ב-BT.601 וב-BT.709 מסיט את הצבע בהדרגה
בתיעוד של Media Foundation, BT.601 מוסבר כמתאים ל-SDTV ומטה, ו-BT.709 כמועדף בוידאו שמעל SD.
עם זאת, לא כדאי לנחש בשקט “הרזולוציה גדולה אז זה בטח 709”. סטיית צבע לא גורמת לקריסה, ולכן קל להישאר לא מודעים אליה ולהכניס אותה לתפעול.
ב-Media Foundation אפשר להחזיק מידע על מרחב הצבע בתוך תכונות סוג המדיה. לכל הפחות, כדאי לבדוק את שני אלה:
MF_MT_YUV_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
אם בודקים את שני אלה, ומעבירים במפורש רק את השילובים שהקוד שלנו תומך בהם, פחות סיכוי לתקלה שקטה בהמשך.
flowchart TB
accTitle: הזרימה שנמנעת מניחוש מרחב הצבע
accDescr: תרשים המראה שניחוש בשקט אם זה 601 או 709 לפי הרזולוציה מוביל לכניסת סטיית צבע לתפעול בלי לשים לב, ולכן בודקים את התכונות MF_MT_YUV_MATRIX ו-MF_MT_VIDEO_NOMINAL_RANGE ומעבירים רק שילוב תואם.
gs1["ניחוש שקט לפי הרזולוציה"] --> sl1["סטיית צבע לא קורסת"]
sl1 --> op1["נכנסת לתפעול בלי לשים לב"]
at1["בדיקת התכונות matrix ו-range"] --> ps1["מעבירים רק שילוב תואם"]
ps1 -.-> sf1["מונע תקלה שקטה"]
איור 6: לא מנחשים את מרחב הצבע — בודקים את התכונות, ומעבירים רק שילובים תואמים במפורש.
3.5. הנוסחה הראשונה לזכור היא 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.
flowchart TB
accTitle: המבנה של נוסחת ההמרה
accDescr: תרשים המראה שמפחיתים 16 מ-Y (רמת שחור), מפחיתים 128 מ-U ו-V (מרכז), מכפילים במקדמי ה-matrix, ומגבילים את התוצאה — זה המבנה של נוסחת ההמרה.
yy2["מפחיתים 16 מ-Y (רמת שחור)"] --> co1["מכפילים במקדמי ה-matrix"]
uv1["מפחיתים 128 מ-U / V (מרכז)"] --> co1
co1 --> cl1["מגבילים ל-0..255"]
איור 7: מה שזוכרים זה לא המקדמים, אלא את מבנה הנוסחה: רמת שחור 16 ומרכז 128.
4. תבנית A: לתת ל-Media Foundation להמיר אוטומטית
4.1. באילו מצבים זה מתאים
השיטה הזו מתאימה למשל למצבים הבאים:
- רוצים לחלץ תמונה אחת מ-MP4
- רוצים ליצור כמה תמונות thumbnail
- רוצים להפוך לתמונת RGB ולהעביר ל-WIC
- לא ניגון בזמן אמת, אלא שימוש ב-batch או בכלי
ל-Source Reader יש יכולת להריץ עיבוד וידאו מוגבל של YUV -> RGB32 באמצעות MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING.
עם זאת, כפי שכתוב גם ב-Microsoft Learn, זה עיבוד בתוכנה ולא מותאם ל-playback. אם רוצים לעבד מאות מסגרות בשנייה, לא מומלץ להישען על זה.
flowchart TB
accTitle: מתאים ולא מתאים להמרה האוטומטית
accDescr: תרשים המראה שההמרה האוטומטית של Source Reader היא עיבוד בתוכנה שלא מותאם ל-playback, ולכן מתאימה לשימושי batch כמו חילוץ תמונת סטילס וthumbnail, אך לא מומלץ להישען עליה בעיבוד מאות מסגרות בשנייה.
auto1["ההמרה האוטומטית של Source Reader"] --> sw1["עיבוד בתוכנה"]
sw1 -->|"מתאים"| bat1["תמונת סטילס / thumbnail / batch"]
sw1 -.->|"לא נשענים על זה"| rt1["עיבוד מאות מסגרות בשנייה בזמן אמת"]
איור 8: ההמרה האוטומטית היא עיבוד בתוכנה, ולכן מגבילים אותה לשימושי כלי במספר קטן של מסגרות.
4.2. מה מגדירים כדי לקבל RGB32
הזרימה טבעית למדי.
- ב-attributes שמעבירים ל-
MFCreateSourceReaderFromURL, מגדיריםMF_SOURCE_READER_ENABLE_VIDEO_PROCESSING = TRUE - בוחרים stream וידאו
- ב-
SetCurrentMediaTypeמבקשיםMFMediaType_Video/MFVideoFormat_RGB32 - קוראים sample עם
ReadSample
רק בזה, עיבוד הוידאו המוגבל שנכנס אחרי המפענח עושה עבורנו YUV -> RGB32.
flowchart TB
accTitle: ארבעת השלבים להפעלת ההמרה האוטומטית
accDescr: תרשים המראה שיוצרים Reader עם הפעלת עיבוד וידאו ב-attributes, בוחרים stream וידאו, מבקשים RGB32, וקוראים עם ReadSample, ואז ההמרה מתבצעת אחרי המפענח.
a1["הפעלת עיבוד וידאו ב-attributes"] --> a2["בחירת stream וידאו"]
a2 --> a3["בקשת RGB32"]
a3 --> a4["קריאה עם ReadSample"]
a4 -.-> a5["ההמרה ל-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,
×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
- מספיק מעשי לעיבוד כמה מסגרות
בכלים לחילוץ תמונת סטילס, טבעי מאוד להתחיל מכאן.
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 כדי לקבל אטימות.
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.
הנקודה הזו הוזכרה גם במאמר הקודם על חילוץ תמונת סטילס, כאחת ממלכודות נפוצות.
5. תבנית B: כותבים בעצמנו את עיבוד ההמרה
5.1. באילו מצבים זה מתאים
המרה עצמית מתאימה, למשל, למקרים הבאים:
- מעבדים מסיבי של מסגרות, ורוצים לייעל את ההמרה בעצמנו
- רוצים להזרים ישירות את
NV12ל-GPU או ל-SIMD - רוצים לטפל במפורש ב-
BT.601/BT.709/ ה-range - רוצים ליצור פורמט פלט שאינו
RGB32 - ההמרה האוטומטית המוגבלת של Source Reader לא מספיקה
אפשר לתאר את זה כתבנית שבה לוקחים על עצמכם את אחריות נפח העיבוד והצבע, בתמורה לחופש פעולה.
flowchart TB
accTitle: הסחר של ההמרה העצמית
accDescr: תרשים המראה שבחירת המרה עצמית פירושה לקחת אחריות על נפח העיבוד והצבע, בתמורה לחופש בייעול, בחיבור ל-GPU/SIMD, בשליטה במפורש ב-matrix וב-range ובפורמט הפלט.
own1["בחירת המרה עצמית"] --> res1["לקיחת אחריות על נפח העיבוד והצבע"]
res1 --> fr1["חופש בייעול, GPU / SIMD, פורמט הפלט"]
res1 --> fr2["שליטה מפורשת ב-matrix / range"]
איור 11: המרה עצמית היא בחירה שלוקחת אחריות בתמורה לחופש בביצועים ובצבע.
5.2. הזרימה הכוללת של ההמרה העצמית
השלבים הם כדלהלן:
- הגדרת הפלט של Source Reader ל-
NV12או ל-YUY2 - קבלת ה-subtype והתכונות בפועל עם
GetCurrentMediaType - בדיקת
MF_MT_FRAME_SIZE,MF_MT_DEFAULT_STRIDE,MF_MT_YUV_MATRIX,MF_MT_VIDEO_NOMINAL_RANGE - שליפת buffer מה-sample ונעילתו
- חישוב ה-Y/U/V שכל פיקסל מפנה אליו
- הפעלת ה-matrix וכתיבה ל-BGRA
הקוד במאמר הזה מוגבל ל-SDR ב-8 סיביות / progressive / NV12 או YUY2 / limited range.
הגבלת ההנחות כאן היא לא קיצור דרך, אלא דבר חשוב. הסיבה היא שהמרת YUV, אם מיישמים אותה כ”מקבל הכול בלי הבחנה”, נוטה לקלקל צבעים בשקט.
flowchart TB
accTitle: הזרימה הכוללת של ההמרה העצמית
accDescr: תרשים המראה שמבקשים NV12 או YUY2, בודקים את סוג המדיה והתכונות בפועל, נועלים את ה-buffer, מחשבים את ה-Y/U/V לכל פיקסל, ומפעילים matrix לכתיבה ל-BGRA, כאשר הגבלת ההנחות מקטינה סיכוי לקלקול צבע.
f1["בקשת NV12 / YUY2"] --> f2["בדיקת ה-subtype והתכונות בפועל"]
f2 --> f3["נעילת ה-buffer"]
f3 --> f4["חישוב ה-Y/U/V לכל פיקסל"]
f4 --> f5["הפעלת matrix וכתיבה ל-BGRA"]
f2 -.-> nar1["הגבלת ההנחות מקטינה סיכוי לקלקול צבע"]
איור 12: ההמרה העצמית מתקדמת בסדר של בקשה, בדיקה, נעילה, הפניה והמרה, ומצטמצם הסיכון ככל שההנחות מוגבלות יותר.
5.3. תחילה מגדירים במפורש את 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כ”לא צוין”. אם המידע הזה חסר כשעוברים דרך המפענח, גם ה-matrix שמגיע במורד הזרם לא צוין - YUV גולמי שהגיע ממכשיר לכידה או ממכל ישן. מסלול שאין בו תיאור מרחב צבע
- לפעמים אין בכלל תכונה
MF_MT_YUV_MATRIX. במקרה כזהGetUINT32לא מחזיר ערך ונכשל עםMF_E_ATTRIBUTENOTFOUND(בקוד שלמעלה זה נדחה ישירות עםFAILED(hr))
חשוב כאן ש-Unknown פירושו “לא ידוע”, ולא “ידוע שזה BT.709”. אם מיישמים 709 על חומר ברזולוציית SD הצבע יסטה, וההפך גם נכון.
עם הבנה זו, יש שני כיווני מדיניות:
- דחייה strict (המדיניות במאמר הזה): מחזירים שגיאה על מה שלא נתמך, ומאפשרים ברמה גבוהה יותר להחליט “החומר הזה לא נתמך”. בטוח יותר לומר “לא ניתן לטפל בזה” מאשר לתת לצבע לסטות בשקט
- קביעת ברירת מחדל והמשך: אם בהכרח צריך להמשיך, רושמים ביומן מה ההנחה שנעשתה כש-
Unknownהתקבל. וכך מבהירים במפורש ש”נקבע 601 / 709 לפי הרזולוציה”
בכל מקרה, הדבר היחיד שנמנעים ממנו הוא עיגול שקט. סטיית צבע לא קורסת, ולכן היא נכנסת לתפעול בלי לשים לב.
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. כותבים בקוד את נוסחת ההמרה של פיקסל בודד
כאן נעסוק רק ב-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, נקי יותר תיאורטית.
flowchart TB
accTitle: שני התכנונים של chroma upsampling
accDescr: תרשים המראה שהמימוש המינימלי משתמש בchroma משותף ישירות עבור 4 הפיקסלים בסגנון nearest-neighbor ומעשי מספיק ברוב המקרים, בעוד שהעדפת איכות דורשת upconversion מ-4:2:0 דרך 4:2:2 ל-4:4:4.
min1["מימוש מינימלי: שימוש ישיר ב-chroma המשותף"] --> pr1["מספיק מעשי ברוב המקרים"]
hq1["המרה אחרי upconversion"] --> pr2["נקי יותר תיאורטית, לטובת איכות"]
hq1 -.-> steps1["בסדר 4:2:0 → 4:2:2 → 4:4:4"]
איור 16: ההבדל בין המימוש המינימלי שמשתמש ב-chroma המשותף לבין תכנון שמבצע upconversion בהדרגה.
5.8. ממירים YUY2 ל-BGRA32
YUY2 הוא 4:2:2 מסוג packed.
מכיוון ששני פיקסלים חולקים זוג U/V אחד, קל מעט יותר לקרוא מ-NV12.
#include <cstddef>
HRESULT ConvertYuy2ToBgra32(
IMFMediaBuffer* buffer,
const DecodedFrameInfo& info,
std::vector<BYTE>& dstBgra)
{
if (!buffer) return E_POINTER;
if (info.subtype != MFVideoFormat_YUY2) return MF_E_INVALIDMEDIATYPE;
if ((info.width & 1u) != 0) return MF_E_INVALIDMEDIATYPE;
dstBgra.resize(static_cast<size_t>(info.width) * info.height * 4);
BufferLock lock(buffer);
BYTE* scanline0 = nullptr;
LONG actualStride = 0;
HRESULT hr = lock.Lock(
info.defaultStride,
info.height,
&scanline0,
&actualStride);
if (FAILED(hr)) return hr;
for (UINT32 y = 0; y < info.height; ++y)
{
const BYTE* src =
scanline0 +
static_cast<ptrdiff_t>(actualStride) * static_cast<ptrdiff_t>(y);
BYTE* dstRow =
dstBgra.data() + static_cast<size_t>(info.width) * 4 * y;
for (UINT32 x = 0; x < info.width; x += 2)
{
const BYTE Y0 = src[0];
const BYTE U = src[1];
const BYTE Y1 = src[2];
const BYTE V = src[3];
hr = ConvertLimitedYuvPixelToBgra(
Y0,
U,
V,
info.matrix,
dstRow + static_cast<size_t>(x) * 4);
if (FAILED(hr))
{
lock.Unlock();
return hr;
}
hr = ConvertLimitedYuvPixelToBgra(
Y1,
U,
V,
info.matrix,
dstRow + static_cast<size_t>(x + 1) * 4);
if (FAILED(hr))
{
lock.Unlock();
return hr;
}
src += 4;
}
}
lock.Unlock();
return S_OK;
}
ב-YUY2 הבייטים מסודרים כ-Y0 U Y1 V, כך שהמבנה “משתמשים חוזר ב-U/V כל 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
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 סיביות, השבר הזה נמחק ומתקבל 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), ולא בדיוק המקדמים. אם מפטרים את זה בתור “בעיית דיוק”, מפספסים תקלה אמיתית שאפשר לתקן.
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, ההמרה של פיקסל בודד הזו הוצאה ל-header נפרד שאינו תלוי במערכת ההפעלה, כך שהבדיקה הזו רצה גם בלי Windows.
שלב 2: משווים בין הפלט של תבנית A לתבנית B
אחרי שהפיקסל הבודד תואם, השלב הבא הוא כל המסגרת. מאותו זמן באותו וידאו, מחלצים תמונה אחת בכל אחד משני המסלולים:
- תבנית A (
MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING+RGB32) - תבנית 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.
flowchart TB
accTitle: שני שלבי האימות
accDescr: תרשים המראה שקודם משווים פיקסל בודד עם ערכים ידועים לחישוב ידני, ואם זה תואם, משווים מסגרות מלאות משני המסלולים באותו זמן באותו וידאו, וממקדים חשד לפי צורת ההפרש.
st5["שלב 1: השוואת פיקסל בודד לחישוב ידני"] --> st6["שלב 2: השוואת מסגרות משני המסלולים"]
st6 --> shp1["ממקדים חשד לפי צורת ההפרש"]
st5 -.-> osf1["לא נדרש וידאו ולא Media Foundation"]
איור 19: אחרי אימות פיקסל בודד, השוואת מסגרות שלמות ממקדת את מקור ההפרש לפי צורתו.
6. באיזה תבנית לבחור
בהיסוס, הטבלה הבאה מסייעת מאוד להתמקד.
| היבט | המרה אוטומטית (MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING) |
המרה עצמית |
|---|---|---|
| מהירות המימוש | ◎ | △ |
| חילוץ כמה תמונות סטילס | ◎ | ○ |
| עיבוד מסיבי / זמן אמת | △ | ◎ |
| שליטה מפורשת ב-matrix / range | △ | ◎ |
| שילוב עם GPU / D3D | △ | ○〜◎ |
פלט שאינו RGB32 |
△ | ◎ |
| הבנת העקרונות | ○ | ◎ |
כמדד ראשון, נוח לחשוב כך:
- רוצים שזה פשוט יעבוד ← המרה אוטומטית
- רוצים לקחת אחריות על הצבע והביצועים ← המרה עצמית
בעבודה מעשית, גם הסדר “קודם לוודא תמונה נכונה עם המרה אוטומטית, ואחר כך להחליף למסלול ידני” יעיל למדי. אם לוקחים על עצמכם הכול מההתחלה, קשה יותר לדעת היכן התמונה נשברה.
flowchart TB
accTitle: הסדר המעשי היעיל
accDescr: תרשים המראה שאם קודם מוודאים תמונה נכונה עם ההמרה האוטומטית ורק אז עוברים למסלול הידני, קל יותר לבודד היכן התמונה נשברה, בעוד שנטילת הכול מההתחלה מקשה על כך.
step1["קודם מוודאים תמונה נכונה עם המרה אוטומטית"] --> step2["אחר כך עוברים למסלול הידני"]
step2 --> good1["קל יותר לבודד היכן נשבר"]
allin1["נטילת הכול מההתחלה"] -.-> lost1["קשה לדעת היכן נשבר"]
איור 20: אם קודם מבטיחים תמונה נכונה ואז עוברים למימוש עצמי, קל יותר לבודד את מקום התקלה.
7. מלכודות נפוצות בעבודה מעשית
7.1. הנחה מוטעית ש-RGB32 הוא RGBA עם alpha
RGB32 בזיכרון הוא B, G, R, Alpha או Don't Care.
אם שומרים אותו ישירות כ-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_MATRIXMF_MT_VIDEO_NOMINAL_RANGE
לפחות אלה, כדאי לבדוק. ובנוסף, נכון להתייחס בגישה של להפוך ערכים שהקוד שלכם לא תומך בהם לשגיאה.
7.5. חיתוך plane ה-UV של NV12 לפי width * height
היסט ה-plane נקבע לפי ה-stride וה-height בפועל. לא לפי width * height.
אם עושים את זה ברשלנות, הצבע נסטה או שהתמונה נשברת.
flowchart TB
accTitle: איך קובעים את גבול ה-plane ב-NV12
accDescr: תרשים המראה שתחילת plane ה-UV ב-NV12 נקבעת לפי מכפלת ה-stride וה-height בפועל, וחיתוך לפי מכפלת width ו-height מוביל לסטיית צבע ולתמונה שבורה.
head1["תחילת המאגר (Y plane)"] -->|"מתקדמים stride × height"| uv2["תחילת plane ה-UV"]
wr1["חיתוך לפי width × height"] -.-> brk1["סטיית צבע / תמונה שבורה"]
איור 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
זה נעשה הרבה יותר טבעי. רצף הבייטים המסתורי מתחיל להיראות כפיקסלים בעלי משמעות אמיתית.
flowchart TB
accTitle: התמונה שכדאי להחזיק בראש
accDescr: תרשים המראה שNV12 חולק U/V ב-2x2, YUY2 חולק U/V בשני פיקסלים לרוחב, ומפעילים matrix על אותו U/V ועל Y, וכך רצף הבייטים המסתורי נראה כפיקסלים בעלי משמעות.
im2["NV12: חלוקת U/V ב-2x2"] --> ap1["הפעלת matrix על אותו U/V ועל Y"]
im3["YUY2: חלוקת U/V בשני פיקסלים לרוחב"] --> ap1
ap1 --> see1["רצף הבייטים נראה כפיקסלים בעלי משמעות"]
איור 22: עם תמונה של יחידת השיתוף ומקדם ה-matrix, רצף הבייטים של ה-YUV נקרא בטבעיות.
9. מקורות
קוד הדוגמה של המאמר הזה
מאמרים קשורים של 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
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
איך לחלץ תמונת סטילס מ-MP4 בזמן נתון עם Media Foundation
מסכמים את שלבי המימוש לחילוץ מסגרת קרובה לזמן נתון מתוך MP4 עם Source Reader, יישור ה-stride ו-alpha של RGB32, ושמירה כ-PNG.
מבוא ל-Media Foundation — מבינים את ה-API מנקודת המבט של COM
המאמר מסביר מה זה Media Foundation, יחד עם המונחים הבסיסיים של ה-API למדיה ב-Windows כמו COM, HRESULT, IMFSourceReader ו-MFT, בסדר שכדא...
צריבת תמונה וטקסט על MP4 עם Media Foundation
המאמר מסדר את הגישה לצריבת תמונה וטקסט על כל מסגרת בסרטון MP4 ויצירת MP4 חדש עם Media Foundation - חלוקת התפקידים בין Source Reader, ציור...
איך קוראים ל-DLL של C# Native AOT מ-C/C++
פרסום ספריית מחלקות C# כ-DLL נייטיבי באמצעות Native AOT, וקריאה לנקודות הכניסה מסוג UnmanagedCallersOnly מ-C/C++ — סקירה לפי מקום השימוש...
רשימת בדיקה לטיפול בטוח בתהליכי ילד ביישום Windows
כדי לטפל בבטחה בתהליכי ילד ביישום Windows, תכנון הבעלות על עץ התהליכים ונוהל הסיום חשוב יותר מבחירת ה-API להפעלה. המאמר מסדר את Job Objec...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
זהו נושא מימוש בעיבוד מדיה ב-Windows שכולל Media Foundation, Source Reader, שמירת תמונות והמרת מסגרות וידאו, ולכן הוא מתאים היטב לפיתוח יישומי Windows.
ייעוץ טכני וסקירת תכנון
אם רוצים לסדר מראש את חלוקת האחריות בין YUV ל-RGB, את מרחב הצבע, ה-stride ותכנון מסלול ההמרה, זה נושא שקל להתקדם בו כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- למה המפענח של 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).