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(), который деформирует источник через матрицу по мере его отрисовки в приёмник – результат попадает в буфер, которым управляет приложение, и деформация комбинируется со всем остальным в вызове: масштабированием, альфа-смешиванием, маскированием.

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() применяет деформацию к самому изображению.