5.4. קריאה וכתיבה של פיקסלים

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

5.4.1. מיעון לפי קואורדינטה

הצורה הטבעית ביותר היא זו שעבורה Coordinates כבר פיתח את אוצר המילים: לקרוא לפיקסל לפי הקואורדינטות הקרטזיות שלו (x, y). ‏get_pixel() מקבלת (x, y) ומחזירה את הערך באותו מיקום; ‏set_pixel() מקבלת את אותם (x, y) יחד עם ערך וכותבת אותו.

מה שקריאות אלה מחזירות או מקבלות תלוי בפורמט של התמונה. תמונות גווני אפור, בינריות ו-Bayer נושאות ערך בודד לכל פיקסל – בהירות עבור גווני אפור, 0 או 1 עבור בינרי, דגימת ערוץ צבע בודד עבור Bayer – ולכן get_pixel() מחזירה מספר שלם בודד. ‏RGB565 נושא שלושה ערוצי צבע ארוזים ב-16 ביט, ו-get_pixel מפרק אותם כברירת מחדל לטאפל (r, g, b), כאשר כל ערוץ ממופה לטווח 0255.

ניתן להפוך את התנהגות ברירת המחדל בכל אחד מהקצוות. העברת rgbtuple=False ל-get_pixel על תמונת RGB565 חוזרת למילה הארוזה הגולמית בת 16 הביט – אותה צורה שהאינדקס הליניארי מחזיר, והצורה היעילה כאשר היישום עומד לכתוב חזרה את אותו ערך ארוז ישירות. העברת rgbtuple=True על תמונה חד-ערוצית עושה את ההפך: הערך המאוחסן מומר לטאפל RGB888 לפני ההחזרה, כאשר תמונות Bayer עוברות שלב debayer מיידי. הארגומנט קיים כך שהקוד הקורא יוכל לבקש פיקסלים במרחב צבע אחיד ללא תלות באופן שבו התמונה הבסיסית מאחסנת אותם.

תמונות דחוסות – JPEG ו-PNG – אינן נתמכות על ידי get_pixel או set_pixel. הבתים שלהן אינם מייצגים פיקסלים במיקומים ידועים, והמתודות מעלות שגיאה במקום להחזיר ערך שלא היה לו כל משמעות.

בפועל התבניות נראות כך:

v = img.get_pixel(40, 30)            # grayscale: int 0..255
img.set_pixel(40, 30, 255)           # write white

r, g, b = img.get_pixel(40, 30)      # RGB565: defaults to (r, g, b) tuple
img.set_pixel(40, 30, (255, 0, 0))   # write red

אם ה-(x, y) המבוקש נמצא מחוץ לתמונה, get_pixel מחזירה None ו-set_pixel אינה עושה דבר. זוהי גישה סלחנית בכוונה תחילה: אלגוריתמים רבים פוסעים קרוב לקצוות התמונה ומתייחסים לרגע למיקומים מחוץ לטווח, ואי-פעולה שקטה פחות מפריעה מחריגה בכל פעם שזה קורה.

5.4.2. מיעון לפי אינדקס ליניארי

הצורה האחרת היא למען פיקסלים לפי מיקומם בחוצץ (buffer) הבסיסי. נזכיר את מבנה החוצץ: הפיקסלים מאוחסנים שורה אחר שורה, תחילה כל הפיקסלים של השורה העליונה, אחר כך כל הפיקסלים של השורה הבאה, וכך הלאה עד התחתית. סידור זה אומר שלכל פיקסל יש אינדקס שלם בודד הסופר מ-0 בפינה השמאלית-עליונה ומתקדם לאורך כל שורה בתורה. הפיקסל בקואורדינטה (x, y) בעל אינדקס ליניארי y * width + x.

A 4-by-3 grid of cells. Each cell carries a large linear index from 0 in the top-left through 11 in the bottom-right, plus a small (x, y) tuple underneath. Columns are labelled x equals 0, 1, 2, 3 across the top; rows are labelled y equals 0, 1, 2 along the left edge. A caption underneath gives the relation: linear index equals y times width plus x.

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

מודול image חושף אינדקס זה דרך סימון מנוי (subscript) רגיל של Python: ‏img[i] קורא את הפיקסל באינדקס הליניארי i, ‏img[i] = value כותב אחד. מה שצורת האינדקס מחזירה הוא הערך הגולמי המאוחסן עבור הפורמט, ולא הטאפל המפורק ש-get_pixel() מחזירה כברירת מחדל. הבחנה זו חשובה כי הפורמט שנבחר קודם לכן קובע כיצד נראה הערך הגולמי:

  • פיקסלים של גווני אפור ו-Bayer חוזרים כמספרים שלמים בני 8 ביט.

  • פיקסלים של RGB565 ו-YUV422 חוזרים כמספרים שלמים בני 16 ביט – המילה הארוזה.

  • פיקסלים בינריים חוזרים כ-0 או 1.

  • פיקסלים של JPEG ו-PNG חוזרים כמספרים שלמים בני 8 ביט, בית אחד בכל פעם מהזרם הדחוס. ערכים אלה אטומים – הם חלקים מקידוד דחוס ולא פיקסלים במובן הרגיל כלשהו.

צורת האינדקס מתאימה לקוד שכבר חושב במונחים של היסטי חוצץ: לולאה הפוסעת על כל פיקסל פעם אחת, אלגוריתם הצריך לקפוץ שורה בכל פעם, או קטע קוד המתרגם בין מבני חוצץ. קוד שחושב במונחים של קואורדינטות x ו-y משורת טוב יותר על ידי get_pixel ו-set_pixel; שתי הצורות ממענות את אותם פיקסלים דרך מודלים מנטליים שונים.

ה-Image הוא גם איטרבילי. ‏for v in img: פוסע בחוצץ באותו סדר שורות-ראשי, מניב את הערכים הגולמיים פיקסל אחד בכל פעם, ו-len(img) הוא מספר הפיקסלים עבור פורמטים לא דחוסים או מספר הבתים עבור זרמים דחוסים.

5.4.3. מדוע עיבוד פיקסל-אחר-פיקסל ב-Python הוא המסלול האיטי

הערה מעשית שכדאי להיות כנים לגביה. פסיעה על תמונה פיקסל אחד בכל פעם מ-Python היא איטית. תמונת גווני אפור בגודל 320 × 240 מכילה 76,800 פיקסלים; קריאה ל-get_pixel() על כל אחד מהם בלולאת for מריצה מיליוני הוראות bytecode של MicroPython כדי לבצע עבודה שמתודת נייטיב מקבילה הייתה משלימה בכמה מאות מיקרו-שניות. זה אינו גורם קטן. זהו ההבדל בין סקריפט שמעבד פריימים בזמן אמת ובין כזה שזוחל הרבה מתחת לקצב הפריימים של המצלמה.

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

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