5.23. תיקון פרספקטיבה

אזהרה

מטריצת ה-transform השרירותית 3 על 3 נתמכת רק על OpenMV Cam N6 – מילת המפתח מתעלמים ממנה בשקט בכל לוח אחר. יישומים שצריכים לרוץ בכל מקום אחר חייבים להשתמש במתודה המוכנה rotation_corr() (עם צורת ה-corners= שלה) או לחשב מראש את התמונה המתוקנת מחוץ ללוח.

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

5.23.1. טרנספורמציות אפיניות והטלתיות

עיוותים גאומטריים מבוטאים ב-קואורדינטות הומוגניות: מיקום הפיקסל (x, y) עם 1 מצורף, מוכפל במטריצת 3 על 3.

הצורה ה-אפינית היא המקום להתחיל בו. השורה התחתונה שלה קבועה ב-\((0, 0, 1)\):

\[\begin{split}\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\end{split}\]

בכתיב מלא, כל קואורדינטת פלט היא צירוף לינארי של קואורדינטות הקלט בתוספת קבוע:

\[x' = a x + b y + c, \qquad y' = d x + e y + f\]

אשר מכסה שינוי קנה מידה, סיבוב, גזירה, והזזה בכל שילוב – ותחת כולם, קווים מקבילים נשארים מקבילים.

הצורה ה-הטלתית (פרספקטיבה) משחררת את השורה התחתונה:

\[\begin{split}\begin{bmatrix} x'' \\ y'' \\ w' \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ g & h & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}, \qquad (x', y') = \left( \frac{x''}{w'}, \; \frac{y''}{w'} \right)\end{split}\]

בכתיב מלא:

\[x' = \frac{a x + b y + c}{g x + h y + 1}, \qquad y' = \frac{d x + e y + f}{g x + h y + 1}\]

החלוקה ב-\(w' = g x + h y + 1\) היא מה שהופך את הטרנספורמציה להטלתית ולא רק אפינית. כאשר \(g\) ו-\(h\) שניהם אפס, \(w'\) נשאר באחד והחלוקה אינה עושה דבר – שוב הצורה האפינית. כאשר אחד מהם אינו אפס, \(w'\) משתנה עם מיקום הקלט ופיקסלים במיקומים שונים מתקצרים במידות שונות, מה שכבר אינו שומר על קווים מקבילים מקבילים – זהו בדיוק אפקט הטרפז של הסתכלות על מישור שטוח מזווית אלכסונית. טרנספורמציה הטלתית היא העיוות הגאומטרי הכללי ביותר שלוקח קווים ישרים לקווים ישרים; שינוי קנה מידה, היפוך, שחלוף, סיבוב, ותיקון סיבוב ארבע-הפינות הם כולם מקרים פרטיים של אחת.

הטרנספורמציות הנקובות בשם נובעות מהצורה האפינית ישירות. טרנספורמציית הזהות היא מטריצת הזהות, ו:

\[\begin{split}\underbrace{\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{translate by } (t_x, \; t_y)} \qquad \underbrace{\begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{scale by } (s_x, \; s_y)} \qquad \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate by } \theta}\end{split}\]

עבור רוב העיוותים הבנויים ידנית, יישום מתחיל באחת מאלה כבסיס ומכפיל בה מטריצות נוספות לכל פעולה נוספת, ומסיים במטריצת 3 על 3 בודדת המתארת את העיוות המורכב. מטריצות מוחלות מימין לשמאל: \(M = T R S\) מריץ קודם את שינוי קנה המידה, ואז את הסיבוב, ואז את ההזזה. ההרכב שכולם זקוקים לו בסופו של דבר הוא סיבוב סביב מרכז התמונה – מטריצת סיבוב חשופה מסובבת את התמונה סביב ראשית הפיקסל בפינה השמאלית-עליונה, ולכן הגרסה הממורכזת מזיזה את המרכז \((c_x, c_y)\) לראשית, מסובבת, ומזיזה אותו בחזרה:

\[\begin{split}M = \underbrace{\begin{bmatrix} 1 & 0 & c_x \\ 0 & 1 & c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre back}} \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate}} \underbrace{\begin{bmatrix} 1 & 0 & -c_x \\ 0 & 1 & -c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre to origin}}\end{split}\]

5.23.2. מילת המפתח transform

המטריצה נכנסת דרך מילת מפתח transform, מסופקת כ-ulab.numpy.ndarray בגודל 3 על 3. המתודה שכדאי לפנות אליה היא draw_image(), שמעוותת את המקור דרך המטריצה תוך כדי ציור שלו על יעד – התוצאה נוחתת בחוצץ (buffer) שהיישום שולט בו, והעיוות מורכב עם כל השאר בקריאה: שינוי קנה המידה, מיזוג האלפא, המיסוך.

import ulab.numpy as np

M = np.array([[1.2,  0.0, -20.0],
              [0.0,  1.2, -15.0],
              [0.0,  0.0,   1.0]])

canvas.draw_image(img, transform=M)

הדוגמה מעוותת את img על canvas בקנה מידה של 1.2 בכל כיוון ומוזזת שמאלה ולמעלה ב-20 וב-15 פיקסלים בהתאמה – עיוות אפיני הבנוי ישירות מערכי המטריצה שתוארו לעיל. אותה מילת מפתח על copy(), crop(), ו-scale() מחילה את העיוות על התמונה עצמה.