5.23. Korekcja perspektywy

Ostrzeżenie

Dowolna macierz transform o wymiarach 3 na 3 jest obsługiwana tylko na OpenMV Cam N6 – na każdej innej płytce to słowo kluczowe jest po cichu ignorowane. Aplikacje, które muszą działać gdziekolwiek indziej, muszą używać gotowej metody rotation_corr() (z jej postacią corners=) lub wstępnie obliczyć skorygowany obraz poza płytką.

Gotowa metoda rotation_corr() pakuje pewną rodzinę zniekształceń perspektywicznych za niewielkim zestawem parametrów i działa na każdej obsługiwanej płytce. Niektóre aplikacje potrzebują zniekształcenia, które nie pasuje do tej postaci: dowolne rzutowe odwzorowanie z jednego czworokąta na inny, skalibrowana korekcja dla znanego montażu, która została już wypracowana offline, gotowa macierz zniekształcenia przekazana przez jakiś wcześniejszy algorytm. Dla nich draw_image() – wraz z copy(), crop() oraz scale() – przyjmuje słowo kluczowe transform, które przyjmuje ręcznie zbudowaną macierz 3 na 3 opisującą zniekształcenie bezpośrednio.

5.23.1. Przekształcenia afiniczne i rzutowe

Zniekształcenia geometryczne wyraża się we współrzędnych jednorodnych: pozycja piksela (x, y) z dołączoną 1, pomnożona przez macierz 3 na 3.

Postać afiniczna to miejsce, od którego należy zacząć. Jej dolny wiersz jest ustalony na \((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}\]

Rozpisana, każda współrzędna wyjściowa jest kombinacją liniową współrzędnych wejściowych plus stała:

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

co obejmuje skalowanie, obrót, ścinanie i translację w dowolnej kombinacji – i przy wszystkich z nich linie równoległe pozostają równoległe.

Postać rzutowa (perspektywiczna) uwalnia dolny wiersz:

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

Rozpisana:

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

Dzielenie przez \(w' = g x + h y + 1\) jest tym, co czyni przekształcenie rzutowym, a nie jedynie afinicznym. Gdy \(g\) i \(h\) są oba zerowe, \(w'\) pozostaje równe jeden, a dzielenie nic nie zmienia – znów postać afiniczna. Gdy któreś z nich jest niezerowe, \(w'\) zmienia się wraz z pozycją wejściową, a piksele w różnych pozycjach ulegają skróceniu perspektywicznemu o różne wartości, co przestaje utrzymywać linie równoległe równoległymi – jest to dokładnie efekt trapezu wynikający z patrzenia na płaską płaszczyznę pod skośnym kątem. Przekształcenie rzutowe jest najbardziej ogólnym zniekształceniem geometrycznym, które przekształca proste linie w proste linie; skalowanie, odbijanie, transponowanie, obracanie oraz korekcja obrotu z czterema narożnikami są szczególnymi przypadkami tego jednego.

Nazwane przekształcenia wynikają bezpośrednio z postaci afinicznej. Przekształcenie tożsamościowe to macierz jednostkowa, oraz:

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

Dla większości ręcznie budowanych przekształceń aplikacja zaczyna od jednej z tych macierzy jako podstawy i domnaża kolejne macierze dla każdej dodatkowej operacji, kończąc na pojedynczej macierzy 3 na 3 opisującej złożone zniekształcenie. Macierze stosuje się od prawej do lewej: \(M = T R S\) wykonuje najpierw skalowanie, potem obrót, a następnie translację. Złożeniem, którego każdy w końcu potrzebuje, jest obrót wokół środka obrazu – sama macierz obrotu obraca obraz wokół początku pikseli w lewym górnym narożniku, więc wyśrodkowana wersja przenosi środek \((c_x, c_y)\) do początku, obraca i przenosi go z powrotem:

\[\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. Słowo kluczowe transform

Macierz przekazuje się przez słowo kluczowe transform, podane jako ulab.numpy.ndarray o wymiarach 3 na 3. Metodą, po którą warto sięgnąć, jest draw_image(), która zniekształca źródło przez macierz, gdy rysuje je na obrazie docelowym – wynik trafia do bufora kontrolowanego przez aplikację, a zniekształcenie składa się ze wszystkim innym w tym wywołaniu: skalowaniem, mieszaniem alfa, maskowaniem.

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)

Przykład zniekształca img na canvas przeskalowany o 1,2 w każdym kierunku i przesunięty w lewo i w górę odpowiednio o 20 i 15 pikseli – zniekształcenie afiniczne zbudowane bezpośrednio z opisanych powyżej wpisów macierzy. To samo słowo kluczowe w copy(), crop() oraz scale() stosuje zniekształcenie do samego obrazu.