5.16. גרעיני קונבולוציה מותאמים אישית

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

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

5.16.1. מתודת morph

החתימה נראית כמו מסנני השכנוּת האחרים עם ארגומנט נוסף אחד:

img.morph(size, kernel, mul=1.0, add=0.0)

size הוא הרדיוס באותו אופן כמו בכל מקום אחר, ולכן הגרעין חייב להיות בדיוק (2 * size + 1) שורות על (2 * size + 1) עמודות. הגרעין עצמו הוא רשימת Python שטוחה של אותו מספר מספרים, בסדר שורה-תחילה (row-major) – (2 * size + 1) הערכים הראשונים הם השורה העליונה, (2 * size + 1) הבאים הם השורה השנייה, וכן הלאה, עד לשורה התחתונה. mul מתאים בקנה מידה את סכום-המכפלות לפני שהוא נכתב אל פיקסל הפלט, ו-add מוסיף קבוע. ברירת המחדל mul=1.0 ו-add=0.0 משאירה את פלט הקונבולוציה ללא שינוי.

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

הצמד threshold=True / offset=N מתחום הסף האדפטיבי פועל גם על morph(), ולכן אותה מסגרת של גרעין מותאם אישית יכולה להפיק סף בינארי שנקודת החיתוך שלו מחושבת על ידי סטטיסטי מותאם אישית.

5.16.2. פריסת הגרעין

גרעין 3 על 3 (size=1) הוא רשימה שטוחה של תשעה מספרים הפרוסים משמאל לימין, מלמעלה למטה. המוסכמה נקראת באופן טבעי אם הרשימה נשברת על פני שלוש שורות Python:

sobel_x = [-1,  0,  1,
           -2,  0,  2,
           -1,  0,  1]

זהו אופרטור הגרדיאנט Sobel-x – הגרעין הסטנדרטי הראשון שכל יישום ירצה, ושימושי לעבור עליו מתחילתו ועד סופו. התבנית פשוטה: משקלים שליליים בעמודה השמאלית, משקלים חיוביים בעמודה הימנית, כאשר עמודת המרכז היא אפס. משקלי השורה -1, -2, -1 (או 1, 2, 1 בצד ימין) גבוהים יותר באמצע מאשר בפינות, מה שמעניק לשורת המרכז השפעה רבה יותר על התוצאה מאשר לשורות הפינה.

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

הרצת הגרעין:

img.morph(1, sobel_x, mul=0.25)

גרעין Sobel מסתכם באפס – כל משקל שלילי בצד שמאל מתאזן על ידי משקל חיובי שווה מימין – ולכן החלוקה האוטומטית אינה מחלקת בכלום, ו-mul הוא קנה המידה היחיד על סכום-המכפלות. mul=0.25 שומר את התגובה בטווח: הסכום המוחלט הגדול ביותר ש-Sobel-x יכול להפיק מרצועת 3 על 3 הוא בערך 4 * 255 = 1020 (שמונה פיקסלים בהירים משוקללים עד 2), וחלוקתו בארבע ממקמת את המקרים הקיצוניים על 255, שם הפורמט גוזם אותם בצורה נקייה.

גרעין Sobel-y התואם מזהה קצוות אופקיים על ידי סיבוב אותה תבנית משקלים ב-90 מעלות:

sobel_y = [-1, -2, -1,
            0,  0,  0,
            1,  2,  1]

יישומים שרוצים לזהות כל קצה, ללא קשר לכיוון, בדרך כלל מריצים את שני ה-Sobel ומשלבים את התגובות.

5.16.3. היסט הפלט

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

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

5.16.4. גרעינים גדולים יותר

כל מה שבעמוד זה תואר באמצעות גרעיני 3 על 3 (size=1), משום שזה הגודל שבו משתמש הקטלוג הסטנדרטי ומשום שפריסת שורה-תחילה קלה לכתיבה ידנית בגודל זה. אולם דבר במנגנון אינו מגביל את הגרעין ל-3 על 3. size=2 מריץ גרעין 5 על 5, עם עשרים וחמישה ערכים ברשימה השטוחה; size=3 מריץ 7 על 7 עם ארבעים ותשעה; וכן הלאה, עד לכל רדיוס שהיישום מוכן לשלם עבורו. המסגרת מטפלת בפריסת רשימה-שטוחה או שורות-מקוננות בכל גודל אי-זוגי.

הסיבה לבחור בגרעין גדול יותר היא אותה סיבה לבחור בשכנוּת גדולה יותר בכל אחד מהמסננים המובנים: יותר ממוצע, זיהוי מאפיינים רחב יותר, פחות רגישות לרעש בפיקסל בודד. העלות גדלה כריבוע הרדיוס – 5 על 5 מבצע בערך פי 2.8 מהעבודה לכל פיקסל של 3 על 3, ו-7 על 7 בערך פי 5.4 – ומכפיל זה בא ישירות על חשבון קצב הפריימים.

הדפוס המעשי הוא להישאר ב-size=1 עבור הקטלוג הסטנדרטי ולבחור בגדלים גדולים יותר רק כאשר האלגוריתם זקוק לשכנוּת הגדולה יותר. מזהי קצוות נהנים לעיתים רחוקות מעבר ל-3 על 3; מסנני החלקה לעיתים כן; הגודל הנכון תלוי בקנה המידה של המאפיינים שהיישום מנסה להדגיש או לדכא.

5.16.5. מתי לבחור ב-morph

להחלקה יומיומית, mean(), gaussian(), ו-bilateral() מהירים ונקיים יותר. לזיהוי קצוות, laplacian() ו-find_edges() בנויים למטרה זו. ההצדקה לפנייה ישירה אל morph() היא כאשר היישום זקוק לקונבולוציה מסוימת שהמסננים המובנים אינם חושפים – Sobel כיווני, תבנית קצה מותאמת אישית, גרעין המכוון למרקם מסוים שיתר הצינור עומד לחפש, או כל אחד מהקטלוג הסטנדרטי של גרעינים שימושיים שעיבוד תמונה קלאסי בנה לאורך העשורים. מלוא הגמישות של גרעינים שרירותיים זמין; המחיר הוא שהיישום אחראי לבחירת ערכי הגרעין שמפיקים את התוצאה הרצויה לו.