5.23. Perspektivkorrektur

Warnung

Die beliebige 3-mal-3-transform-Matrix wird nur auf der OpenMV Cam N6 unterstützt – auf jedem anderen Board wird das Schlüsselwort stillschweigend ignoriert. Anwendungen, die anderswo laufen müssen, müssen die vorgefertigte Methode rotation_corr() (mit ihrer corners=-Form) verwenden oder das korrigierte Bild außerhalb des Boards vorberechnen.

Die vorgefertigte Methode rotation_corr() verpackt eine bestimmte Familie perspektivischer Verzerrungen hinter einem kleinen Satz von Parametern und läuft auf jedem unterstützten Board. Manche Anwendungen benötigen eine Verzerrung, die nicht in diese Form passt: eine beliebige projektive Neuabbildung von einem Viereck auf ein anderes, eine kalibrierte Korrektur für eine bekannte Montage, die bereits offline ausgearbeitet wurde, eine fertige Verzerrungsmatrix, die von einem vorgelagerten Algorithmus übergeben wird. Für diese Fälle akzeptiert draw_image() – ebenso wie copy(), crop() und scale() – ein Schlüsselwort transform, das eine von Hand erstellte 3-mal-3-Matrix entgegennimmt, die die Verzerrung direkt beschreibt.

5.23.1. Affine und projektive Transformationen

Geometrische Verzerrungen werden in homogenen Koordinaten ausgedrückt: die Pixelposition (x, y) mit einer angehängten 1, multipliziert mit einer 3-mal-3-Matrix.

Die affine Form ist der Ausgangspunkt. Ihre unterste Zeile ist auf \((0, 0, 1)\) fixiert:

\[\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}\]

Ausgeschrieben ist jede Ausgabekoordinate eine Linearkombination der Eingabekoordinaten plus eine Konstante:

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

was Skalierung, Rotation, Scherung und Translation in jeder Kombination abdeckt – und unter allen davon bleiben parallele Linien parallel.

Die projektive (perspektivische) Form gibt die unterste Zeile frei:

\[\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}\]

Ausgeschrieben:

\[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}\]

Die Division durch \(w' = g x + h y + 1\) ist es, die die Transformation projektiv statt bloß affin macht. Wenn \(g\) und \(h\) beide null sind, bleibt \(w'\) bei eins und die Division bewirkt nichts – wieder die affine Form. Ist eine der beiden ungleich null, variiert \(w'\) mit der Eingabeposition, und Pixel an verschiedenen Positionen werden unterschiedlich stark verkürzt, was parallele Linien nicht länger parallel hält – genau der Trapezeffekt, der entsteht, wenn man eine ebene Fläche aus einem schrägen Winkel betrachtet. Eine projektive Transformation ist die allgemeinste geometrische Verzerrung, die gerade Linien auf gerade Linien abbildet; Skalierung, Spiegelung, Transponierung, Rotation und die Vier-Ecken-Rotationskorrektur sind allesamt Spezialfälle davon.

Die benannten Transformationen ergeben sich unmittelbar aus der affinen Form. Die identische Transformation ist die Einheitsmatrix, und:

\[\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}\]

Für die meisten von Hand erstellten Transformationen beginnt eine Anwendung mit einer dieser Matrizen als Basis und multipliziert für jede zusätzliche Operation weitere Matrizen hinzu, was mit einer einzelnen 3-mal-3-Matrix endet, die die zusammengesetzte Verzerrung beschreibt. Matrizen wirken von rechts nach links: \(M = T R S\) führt zuerst die Skalierung aus, dann die Rotation, dann die Translation. Die zusammengesetzte Transformation, die letztlich jeder braucht, ist die Rotation um die Bildmitte – eine bloße Rotationsmatrix dreht das Bild um den Pixelursprung in der oberen linken Ecke, deshalb verschiebt die zentrierte Version den Mittelpunkt \((c_x, c_y)\) zum Ursprung, dreht und verschiebt ihn zurück:

\[\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. Das transform-Schlüsselwort

Die Matrix wird über ein Schlüsselwort transform übergeben, bereitgestellt als 3-mal-3-ulab.numpy.ndarray. Die Methode, zu der man greift, ist draw_image(), die die Quelle beim Zeichnen auf ein Ziel durch die Matrix verzerrt – das Ergebnis landet in einem Puffer, den die Anwendung kontrolliert, und die Verzerrung setzt sich mit allem anderen im Aufruf zusammen: der Skalierung, dem Alpha-Blending, der Maskierung.

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)

Das Beispiel verzerrt img auf canvas, skaliert um 1,2 in jeder Richtung und um 20 bzw. 15 Pixel nach links und nach oben verschoben – eine affine Verzerrung, die direkt aus den oben beschriebenen Matrixeinträgen aufgebaut ist. Dasselbe Schlüsselwort bei copy(), crop() und scale() wendet die Verzerrung auf das Bild selbst an.