5.33. זרמי ImageIO

המתודות save() ו-to_jpeg() מכסות את מקרה הקלט/פלט של פריים בודד: יישום לוכד פריים, מקודד אותו, ודוחף אותו למקום כלשהו. סוג אחר של יישום זקוק למקרה של רצף: הקלטת פריימים רבים ברצף בקצב הלכידה הטבעי, אחסונם במקום שממנו ניתן לאחזר אותם מאוחר יותר, והשמעתם חזרה במהירות הנכונה. סקריפט לאיסוף נתוני אימון לוכד כמה מאות פריימים לדוגמה עבור צינור של למידת מכונה; יומן של תחנת בדיקה מתעד כל חלק שנלכד לצורך מעקב; סקריפט פיתוח משמיע חזרה רצף מאוחסן כדי לבדוק אלגוריתם חדש מול נתונים שנלכדו קודם לכן בזמן אמת.

המחלקה ImageIO היא המקליט / הנגן של מודול התמונה. זרם בודד מחזיק רצף של פריימים מסוג Image – אולי בגדלים ובפורמטי פיקסל שונים – יחד עם המרווח בין-פריימי של כל אחד מהם, כך שההשמעה יכולה לשחזר את קצב הפריימים המקורי. שני מאגרי גיבוי זמינים: קובץ על מערכת הקבצים או חוצץ (buffer) בגודל קבוע ב-RAM.

5.33.1. שני מאגרי הגיבוי

זרם קובץ משמר את ההקלטה לאורך מחזורי הפעלה וכבייה וגודלו מוגבל רק על ידי האחסון שמגבה אותו. הוא מתחיל בכותרת קסם (magic header) בגודל 16 בתים OMV IMG STR Vx.y ולאחריה מקטע אחד לכל פריים; הכותב הנוכחי מפיק V2.0 והקורא עדיין מקבל קבצי V1.0 ו-V1.1 לצורך תאימות לאחור. נתיב הקובץ הוא ארגומנט הבנאי; המצב הוא מצב פתיחת הקובץ ('r' לקריאת זרם קיים, 'w' לקיצוץ וכתיבה מחדש).

# Recording to /sdcard/run.bin
stream = image.ImageIO("/sdcard/run.bin", "w")
for _ in range(120):
    img = csi0.snapshot()
    stream.write(img)
stream.close()

זרם זיכרון שוכן בחוצץ (buffer) RAM המוקצה בעת הבנייה. הבנאי מקבל שלשה (w, h, pixformat) במקום נתיב, וארגומנט ה-mode הופך למספר משבצות הפריימים המוקצות מראש. החוצץ מתואם בגודלו בדיוק לכמות פריימים זו במידות שסופקו ואינו רשאי לגדול לאחר ההקצאה – כתיבה מעבר למשבצת האחרונה מעלה EOFError, וכתיבת פריים גדול מחוצץ המשבצת מעלה ValueError. זרמי זיכרון הם הכלי הנכון כאשר היישום צריך למסור הקלטה לשלב במורד הזרם מבלי לעבור דרך מערכת הקבצים (חוצץ טבעת קצר של פריימים אחרונים עבור תבנית של הפעלה-והשמעה-חוזרת, למשל).

# Pre-allocate space for 32 QVGA RGB565 frames in RAM
stream = image.ImageIO((320, 240, image.RGB565), 32)
for _ in range(32):
    stream.write(csi0.snapshot())

עבור פורמטי הפיקסל הדחוסים (image.JPEG, image.PNG) גודל המשבצת מוערך ב-2 ביטים לפיקסל; פריים מקודד גדול מההערכה מעלה ValueError בזמן הכתיבה, כך שיישום שמצפה לאחסן JPEG-ים באיכות גבוהה צריך או להקצות יותר מדי משבצות או לקודד באיכות נמוכה יותר תחילה.

המתודה type() מחזירה image.ImageIO.FILE_STREAM או image.ImageIO.MEMORY_STREAM כך שקוד במורד הזרם יכול להסתגל לכל מאגר גיבוי שניתן לו.

5.33.2. הקלטה

המתודה write() מצרפת Image שנלכד לזרם קובץ (או מאחסנת אותו במשבצת הנוכחית של זרם זיכרון) ומקדמת את ההיסט באחד. אותה קריאה מתעדת את המרווח הבין-פריימי מאז הכתיבה האחרונה, כך שצד ההשמעה יכול להשתהות למשך הזמן הנכון בין פריימים וקצב הפריימים הטבעי של ההקלטה נשמר.

פריימים הטרוגניים מותרים בתוך זרם קובץ יחיד: הקלטה יכולה לערבב בחופשיות לכידות RGB565, חיתוכי גווני אפור, ותמונות ממוזערות מקודדות-JPEG, והקורא יפענח כל אחת בגודל ובפורמט המקוריים שלה. זרמי זיכרון הם הומוגניים (כל המשבצות חולקות את ה-(w, h, pixformat) שסופק לבנאי), כך שהקלטת זיכרון מוגבלת לתצורת פריים אחת.

המתודה write() מחזירה את אובייקט הזרם כך שניתן לשרשר קריאות. כתיבה בהיסט שאינו הסוף של זרם קובץ מקצצת את שאר הקובץ – שימושי לעריכת רצף מאוחסן, מסוכן אם מיקום הכתיבה הבא הוזז ללא כוונה על ידי seek() קודם.

המתודה sync() מנקזת כתיבות ממתינות לדיסק עבור זרמי קבצים (היא פעולת no-op בזרמי זיכרון) וצריך לקרוא לה מעת לעת כאשר ההקלטה ארוכת-טווח, כדי להימנע מאובדן זנב ההקלטה אם המצלמה מאתחלת מחדש לפני שהקובץ נסגר. ההורס סוגר את הזרם אוטומטית כאשר ה-ImageIO יוצא מהתחום, אך close() מפורש הוא המשמעת הנכונה.

5.33.3. השמעה

המתודה read() קוראת את הפריים בהיסט הנוכחי, מקדמת את ההיסט, ומחזירה את ה-Image החדש. המקבל נשאר בחוצץ הפריימים (frame buffer) כאשר copy_to_fb=True (ברירת המחדל) כך שהתמונה המוחזרת ניתנת לציור דרך התצוגה המקדימה של ה-IDE; כאשר copy_to_fb=False הפריים נוחת על ערימת ה-MicroPython.

# Loop a recorded stream at its natural frame rate
stream = image.ImageIO("/sdcard/run.bin", "r")
while True:
    img = stream.read()
    # img is now in the frame buffer; the IDE shows it
    # and the script can run any analysis it likes

שתי מילות מפתח שולטות בהתנהגות ההשמעה. loop=True (ברירת המחדל לזרמי קבצים) מחזיר את מצביע הקריאה להתחלה כאשר מגיעים לסוף ההקלטה, כך שהקריאה לעולם אינה מחזירה None; loop=False מחזיר None ברגע שההקלטה מוצתה ולולאת הקורא מסתיימת. pause=True (ברירת המחדל) חוסם את הקריאה עד שהמרווח הבין-פריימי שתועד בזמן הכתיבה חלף, כך שקצב הפריימים של ההשמעה תואם את קצב הפריימים של הלכידה המקורית; pause=False מחזיר מיד, שימושי עבור צינורות ניתוח שרוצים ללעוס את ההקלטה במהירות האפשרית מבלי לכבד את התזמון המקורי.

אותה תבנית לולאה עובדת עבור זרמי זיכרון פרט לכך ש-loop מתעלמים ממנו – קריאה מעבר לסוף זרם זיכרון מעלה EOFError. התבנית הצפויה עבור טבעת זיכרון היא לבצע seek() חזרה לאפס במפורש כאשר רוצים גלישה מסביב.

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

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

שני מודולים נפרדים מכסים את המקרה הניתן להשמעה במארח. המודול mjpeg מקליט Motion JPEG: רצף של פריימים דחוסי-JPEG ארוזים למיכל בסגנון AVI יחיד ש-VLC, QuickTime, ffmpeg, ותגית הווידאו התקנית של הרשת כולם משמיעים ישירות. המודול gif מקליט GIF מונפש: רצף של פריימים בלתי-דחוסים (או דחוסי-לוח) עם השהיות מפורשות לכל פריים, הניתנים להשמעה בכל דפדפן רשת או מציג תמונות שמטפל ב-GIF-ים מונפשים.

המודול mjpeg הוא הבחירה הטבעית עבור הקלטות ארוכות. דחיסת JPEG שומרת על גודל קובץ נשלט – בדומה ל-to_jpeg() באיכות המוגדרת, פריים אחר פריים – כך שמפגש לכידה מורחב נשאר בתוך תקציב כרטיס ה-SD. השימוש משקף מקרוב את הקלטת ImageIO:

import mjpeg

m = mjpeg.Mjpeg("/sdcard/run.mjpeg")
while running:
    m.add_frame(csi0.snapshot(), quality=85)
m.close()

המחלקה mjpeg.Mjpeg מקבלת את אותן מילות מפתח מיקומיות ומילות מפתח לקנה-מידה בסגנון ציור שמתודות תמונה אחרות מקבלות, כך שניתן לשנות קנה מידה, לחתוך, או למפות-לוח הקלטה לכל פריים בדרך פנימה. ארגומנטי ה-width וה-height של הבנאי מוגדרים כברירת מחדל למידות חוצץ הפריימים (frame buffer) הראשי וקובעים את רזולוציית הפלט; כל פריים מצורף משונה בקנה מידה (תוך שמירת יחס גובה-רוחב) כדי להתאים. המתודה sync() מנקזת את הקובץ לדיסק במהלך הקלטה ארוכה, והמתודה close() משלימה את המיכל – קובץ Motion JPEG שלא נסגר בצורה נקייה אינו ניתן להשמעה, כך שהמשמעת חשובה.

המודול gif הוא הבחירה הטבעית עבור הקלטות קצרות המשותפות כפי שהן עם צופה לא-טכני – כמה שניות של פעולה שנלכדו עבור הדגמה, איור מונפש לתיעוד, קליפ אירוע מוטמע בהודעת צ’אט. פריימי GIF מאוחסנים בלתי-דחוסים (או דחוסי-לוח בעומק צבע של 7 ביטים), מה שהופך את הקבצים לגדולים בהרבה לשנייה מאשר Motion JPEG ופוסל את הפורמט עבור הקלטות ארוכות מכמה שניות, אך התוצאה נופלת ישירות לתוך כל דפדפן:

import gif

g = gif.Gif("/sdcard/clip.gif")
while running:
    g.add_frame(csi0.snapshot(), delay=10)
g.close()

הארגומנט delay במתודה add_frame() הוא זמן התצוגה לכל פריים בסנטי-שניות (10 הוא 100 ms לכל פריים, או 10 fps), שהוא בקרת השמעת ה-GIF התקנית. מילת המפתח loop של הבנאי קובעת אם הקליפ המתקבל מתנגן בלולאה אוטומטית במציגים (ברירת המחדל היא True, התואמת את הציפייה המקובלת של ”GIF מונפש“).

שלושת נתיבי ההקלטה מכסים יחד את המקרים הנפוצים: ImageIO לעיבוד-מחדש על-המצלמה, Motion JPEG להקלטות ארוכות הניתנות להשמעה במארח, GIF מונפש לקליפים קצרים הניתנים להשמעה במארח. הבחירה ביניהם מסתכמת במי משמיע את ההקלטה חזרה. שלב במורד הזרם שרץ על המצלמה עצמה קורא ImageIO; תחנת עבודה מארחת או צופה רשת קוראים MJPEG או GIF.

5.33.6. תבנית של הפעלה-והשמעה-חוזרת

תבנית שימושית משלבת זרם זיכרון עם תנאי הפעלה. המצלמה מקליטה ברציפות לתוך חוצץ טבעת זיכרון בעל count משבצות, ומשכתבת את המשבצת הישנה ביותר בכל סבב. כאשר תנאי הפעלה נורה (רכיב/כתם (blob) נכנס לפריים, אירוע תנועה חורג מהסף, נלחץ כפתור) היישום מצלם תמונת בזק (snapshot) של תוכן הטבעת – count הפריימים האחרונים – וכותב אותם לזרם קובץ על כרטיס ה-SD. התוצאה היא הקלטה טרום-הפעלה הלוכדת את השניות שלפני האירוע שהמצלמה למעשה הבחינה בו, לא רק את השניות שאחריו, שהיא המגבלה הקלאסית של מקליט נאיבי של ”לכוד-בעת-הפעלה“.

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