5.23. Correção de perspectiva

Aviso

A matriz transform 3-por-3 arbitrária é suportada apenas na OpenMV Cam N6 – o argumento nomeado é silenciosamente ignorado em todas as outras placas. Aplicações que precisam rodar em qualquer outro lugar devem usar o método pronto rotation_corr() (com sua forma corners=) ou pré-calcular a imagem corrigida fora da placa.

O método pronto rotation_corr() empacota uma família específica de deformações de perspectiva por trás de um pequeno conjunto de parâmetros, e roda em todas as placas suportadas. Algumas aplicações precisam de uma deformação que não se encaixa nessa forma: um remapeamento projetivo arbitrário de um quadrilátero para outro, uma correção calibrada para uma montagem conhecida que já foi resolvida off-line, uma matriz de deformação entregue pronta por algum algoritmo anterior. Para esses casos, draw_image() – junto com copy(), crop() e scale() – aceita um argumento nomeado transform que recebe uma matriz 3-por-3 construída manualmente descrevendo a deformação diretamente.

5.23.1. Transformações afins e projetivas

As deformações geométricas são expressas em coordenadas homogêneas: a posição do pixel (x, y) com um 1 anexado, multiplicada por uma matriz 3-por-3.

A forma afim é o ponto de partida. Sua linha inferior é fixada em \((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}\]

Escrita por extenso, cada coordenada de saída é uma combinação linear das coordenadas de entrada mais uma constante:

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

o que abrange escala, rotação, cisalhamento e translação em qualquer combinação – e, sob todas elas, linhas paralelas permanecem paralelas.

A forma projetiva (perspectiva) libera a linha inferior:

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

Escrita por extenso:

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

A divisão por \(w' = g x + h y + 1\) é o que torna a transformação projetiva, em vez de meramente afim. Quando \(g\) e \(h\) são ambos zero, \(w'\) permanece em um e a divisão não faz nada – a forma afim novamente. Quando qualquer um deles é diferente de zero, \(w'\) varia com a posição de entrada e pixels em posições diferentes sofrem escorço por quantidades diferentes, o que não mantém mais linhas paralelas paralelas – é exatamente o efeito trapezoidal (keystone) de olhar para um plano achatado de um ângulo oblíquo. Uma transformação projetiva é a deformação geométrica mais geral que leva linhas retas a linhas retas; escalar, espelhar, transpor, rotacionar e a correção de rotação por quatro cantos são todos casos especiais dela.

As transformações nomeadas derivam diretamente da forma afim. A transformação identidade é a matriz identidade, e:

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

Para a maioria das transformações construídas manualmente, uma aplicação começa com uma dessas como base e multiplica matrizes adicionais para cada operação extra, terminando com uma única matriz 3-por-3 que descreve a deformação composta. As matrizes são aplicadas da direita para a esquerda: \(M = T R S\) executa primeiro a escala, depois a rotação, depois a translação. A composição que todos acabam precisando é a rotação em torno do centro da imagem – uma matriz de rotação pura gira a imagem em torno da origem do pixel no canto superior esquerdo, então a versão centralizada move o centro \((c_x, c_y)\) para a origem, rotaciona e o move de volta:

\[\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. O argumento nomeado transform

A matriz é fornecida por meio de um argumento nomeado transform, passado como um ulab.numpy.ndarray 3-por-3. O método a ser usado é draw_image(), que deforma a origem através da matriz enquanto a desenha sobre um destino – o resultado cai em um buffer que a aplicação controla, e a deformação se compõe com tudo o mais na chamada: a escala, a mesclagem alfa, o mascaramento.

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)

O exemplo deforma img sobre canvas escalado por 1,2 em cada direção e deslocado para a esquerda e para cima em 20 e 15 pixels, respectivamente – uma deformação afim construída diretamente a partir das entradas da matriz descritas acima. O mesmo argumento nomeado em copy(), crop() e scale() aplica a deformação à própria imagem.