6.1. מדוע מערכים

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

מה שספריית התמונה אינה חושפת הוא שאר העבודה המספרית שיישום OpenMV נתקל בה:

  • חוצצי חיישן שאינם פיקסלים – דגימות ADC, צירים מ-IMU (יחידת מדידה אינרציאלית), שמע ממיקרופון,

  • מספרים נגזרים מהתמונה שאף שיטה מובנית אינה מחזירה – עמודת היסטוגרמה, מיזוג מותאם של שני פריימים, טרנספורם לכל פיקסל שהקטלוג אינו מכסה,

  • אלגברה לינארית קטנה – מטריצת הכיול שמיישרת את העדשה, הסיבוב שמאחד את ה-IMU,

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

כל אלה רוצים את אותה צורה: חוצץ של מספרים עם פעולה אחת המוחלת על כל איבר. לולאת for של Python היא הדרך המתבקשת לכתוב זאת:

for i in range(len(samples)):
    samples[i] = samples[i] * cal

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

תקורה זו נושכת בכל פעם שסקריפט מגיע לחוצץ. פריים QVGA בגווני אפור הוא 76,800 פיקסלים; מד תאוצה ב-100 הרץ מספק מאה דגימות תלת-ציריות בשנייה; מיקרופון ממלא חוצץ בן 1024 דגימות כל 64 אלפיות שנייה. לולאת for ב-Python טהורה על כל אלה הופכת משימה שאמורה לקחת מספר מיקרושניות לכזו שלוקחת עשרות מילישניות – וגדולה פי עשרה בערך על חוצץ בגודל תמונה.

6.1.1. פונקציות ספרייה מהירות מלולאות

התיקון הוא לבטא את הפעולה כקריאת פונקציה אחת כנגד החוצץ כולו, במקום לולאת Python על איבריו. numpy הוא בדיוק זה: ספרייה של מתמטיקת מערכים שבה כל פעולה היא פונקציה אחת כבר ממוטבת שעוברת על החוצץ פעם אחת מתחילתו לסופו. np.multiply(samples, cal) מכפיל כל איבר של samples ב-cal בתוך קריאה אחת – אותה אריתמטיקה שהלולאה עשתה, ללא עלות המפרש לכל איטרציה. אותו כפל בן 1000 איברים שלקח עשרות מילישניות כלולאת Python לוקח עשרות מיקרושניות כקריאת numpy.

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

6.1.2. מדוע רשימה לא תספיק

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

6.1.3. מדוע גם bytearray אינו מספיק

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

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