Como fazer uma transformação de perspetiva no OpenCV, explicado com imagens e coordenadas

How to Do a Perspective Transform in OpenCV, Explained with Images and Coordinates

Written by

in

Este artigo usa Python e OpenCV para uma transformação de perspetiva: passar as quatro esquinas de uma imagem para outras quatro. Em síntese, getPerspectiveTransform calcula a matriz e warpPerspective deforma os píxeis. O exemplo é uma grelha 600×600 com uma silhueta. (Os rótulos das figuras estão em inglês.)

Figura de transformações de perspetiva do OpenCV, segundo uma grelha e uma silhueta. Mostra que emparelhar quatro esquinas faz um trapézio ou um paralelogramo. Os painéis são frente, corte, trapézio largo em baixo e um que recua à direita.
A mesma entrada, só com os destinos das quatro esquinas alterados

Pontos-chave

Olhe istoIdeia
O que estamos a fazerFixe para onde vão as quatro esquinas e o plano estica-se em trapézio
Código e resultadoExecute o script abaixo com a mesma pasta da figura
PastasColoque source_pics e save_pics ao lado do script
CoordenadasCanto superior esquerdo é [0, 0]. Ordem: sup.-esq. → inf.-esq. → inf.-dir. → sup.-dir.
As restantes linhasLeitura, tamanho, matriz, deformação, gravação
Tamanho de saídaOs píxeis fora do terceiro argumento são recortados
A sua imagemAltere só a ordem dos pontos e o tamanho de saída para o retângulo que quer
Câmara limitadaMesmo de lado, emparelhar as quatro esquinas do plano real continua a dar um retângulo de dados

Uma transformação de perspetiva fixa para onde vão as quatro esquinas

Uma transformada afim (warpAffine) mantém as paralelas paralelas. Uma de perspetiva (getPerspectiveTransform e warpPerspective) move as quatro esquinas em separado, por isso consegue um trapézio largo à frente e estreito atrás. Endireitar um cartão de visita para um retângulo visto de cima, ou olhar um chão em planta, pertencem a estas funções.

Passe as esquinas de entrada e as de saída na mesma ordem. Este artigo não segue a álgebra da matriz. Se souber para onde vão as quatro esquinas, pode usar as funções.

O script e o antes/depois

Primeiro um script que funciona. As secções seguintes seguem as coordenadas e as restantes linhas.

test_1.py
import numpy as np
import cv2

image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

height, width, channels = image.shape[:3]

source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

mat = cv2.getPerspectiveTransform(source_points, target_points)

perspective_image = cv2.warpPerspective(image, mat, (width, height))
cv2.imwrite("save_pics/perspective_image.png", perspective_image)
Antes e depois de uma transformação de perspetiva, comparando ficheiro de entrada e de saída. Mostra que aproximar as esquinas de cima faz um trapézio estreito em cima. Esquerda: test_1.png quadrado; direita: perspective_image.png.
test_1.png antes e perspective_image.png depois

Este exemplo aproxima o bordo superior, por isso o resultado é um trapézio estreito em cima e largo em baixo. A margem preta é a parte da tela de saída que fica fora do quadrilátero deformado.

Coloque as pastas ao lado do script

Disposição para executar o script, segundo onde estão o script e as pastas de entrada e saída. Liga os caminhos do código a onde se lê e se escreve. test_1.py fica ao lado de source_pics e save_pics.
source_pics e save_pics ao lado de test_1.py

Os caminhos no código são relativos ao test_1.py que executa. A imagem lida é source_pics/test_1.png; o ficheiro escrito é save_pics/perspective_image.png. Com os seus ficheiros, iguale esses nomes ou altere as cadeias de caminho.

💡 Dica

Se o imread falhar, image é None e a linha seguinte, image.shape, pára. Antes de executar, confira a ortografia das pastas e o diretório de trabalho atual.

As coordenadas começam em [0, 0] no canto superior esquerdo

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

Os arrays seguem esta ordem. A origem não está em baixo, como no papel milimétrico.

Ordemsource_pointstarget_points
Superior esquerdo[0, 0][200, 0]
Inferior esquerdo[0, height][0, 600]
Inferior direito[width, height][600, 600]
Superior direito[width, 0][400, 0]

Este exemplo é 600×600, por isso height e width valem 600. Preenchidos, os arrays ficam assim.

Python
source_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
Mapa de quatro esquinas para uma transformação de perspetiva, segundo source_points e target_points. Com origem no canto superior esquerdo, aproximar o bordo superior encurta o topo. [0, 0] e [600, 0] passam a [200, 0] e [400, 0].
Aproxime o bordo superior e o topo fica um trapézio estreito

Os dois pontos de cima aproximam-se, por isso o bordo superior encurta. Os dois de baixo não se mexem. Altere os números e o mesmo emparelhamento das quatro esquinas faz outro trapézio.

⚠️ Armadilha comum

Se a ordem de entrada e a de saída não coincidirem, o mapeamento torce. Escreva ambas como sup.-esq. → inf.-esq. → inf.-dir. → sup.-dir. Se tomar pontos num eixo y matemático (cima é positivo), a imagem parece invertida.

O que fazem as restantes linhas

Importações

Python
import numpy as np
import cv2

O NumPy guarda os arrays de pontos. O cv2 do OpenCV lê, escreve e deforma imagens.

Leitura da imagem

Python
image = cv2.imread("source_pics/test_1.png", cv2.IMREAD_COLOR)

cv2.imread carrega test_1.png de source_pics, ao lado do script, a cores, em image.

Tamanho da imagem

Python
height, width, channels = image.shape[:3]

shape dá altura, largura e número de canais. Este exemplo não usa os canais, mas o número de valores que desempacota tem de coincidir, por isso tomam-se os três.

💡 Dica

Iguale o número de variáveis ao número de valores que desempacota.

Pontos para a matriz

Python
source_points = np.array([[0, 0], [0, height], [width, height], [width, 0]], dtype=np.float32)
target_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)

Quatro pontos XY em cada lado. Nesta imagem de origem [height, width] é [600, 600], por isso os arrays contêm:

Python
source_points =  [[  0.   0.]
 [  0. 600.]
 [600. 600.]
 [600.   0.]]
target_points =  [[200.   0.]
 [  0. 600.]
 [600. 600.]
 [400.   0.]]

São as coordenadas da figura. A deformação usa esse emparelhamento.

A matriz de transformação

Python
mat = cv2.getPerspectiveTransform(source_points, target_points)

cv2.getPerspectiveTransform calcula a matriz a partir das quatro esquinas de entrada e das quatro de saída. Neste exemplo os valores são:

Python
mat =  [[ 3.33333333e-01 -3.33333333e-01  2.00000000e+02]
 [ 0.00000000e+00  3.33333333e-01  0.00000000e+00]
 [ 0.00000000e+00 -1.11111111e-03  1.00000000e+00]]

O significado de cada entrada não é preciso aqui. Se as esquinas forem para onde pretendia, passe o valor de retorno à linha seguinte.

A deformação

Python
perspective_image = cv2.warpPerspective(image, mat, (width, height))

cv2.warpPerspective recebe a imagem de origem, a matriz e a largura e altura de saída. Este exemplo só encurta um bordo, por isso o tamanho de saída fica (width, height), igual à origem. Escolha o tamanho real a olhar para as coordenadas transformadas.

Gravação

Python
cv2.imwrite("save_pics/perspective_image.png", perspective_image)

cv2.imwrite escreve perspective_image.png em save_pics, ao lado do script. Crie a pasta primeiro; se faltar, a escrita falha.

Se a saída for demasiado pequena, o excesso é recortado

Deixe a saída em 600×600 e tome pontos como estes, e o topo alargado sai da tela.

Python
source_points = np.array([[200, 0], [0, 600], [600, 600], [400, 0]], dtype=np.float32)
target_points = np.array([[0, 0], [0, 600], [600, 600], [600, 0]], dtype=np.float32)
Recorte quando a tela de saída é demasiado pequena, segundo pontos fora da moldura. Explica que o terceiro argumento de warpPerspective é o tamanho escrito. O [0, 0] e [600, 0] originais ficam fora de 600×600, por isso o topo é cortado.
Se a saída ficar em 600×600, o topo alargado é recortado

O terceiro argumento é o tamanho da imagem que escreve, não o tamanho do mundo deformado. Os píxeis cujos pontos ficam fora desse retângulo desaparecem. Para os guardar, aumente primeiro a saída, ou escolha pontos target que caibam todos.

⚠️ Armadilha comum

Se um x ou y de target passar a largura ou a altura de saída, e deixar o terceiro argumento no tamanho de origem, recorta. Quando parece cortado, compare primeiro os pontos com o tamanho de saída.

Os mesmos passos servem nas suas próprias fotos

Mantenha a ordem das quatro esquinas e as mesmas duas funções servem em fotos que não são uma grelha. Altere só os números de target e o tamanho de saída para o retângulo que quer.

Mesmo com a câmara de lado, emparelhar quatro esquinas continua a dar um retângulo

O que fez é uma cadeia curta: emparelhar as esquinas de entrada com as de saída, resolver a matriz e escrever na tela de saída. As funções são as mesmas na grelha e numa foto do chão. No local, só mudam a forma de tomar os pontos e o tamanho de saída.

Quando a posição da câmara é fixada pela máquina

Em equipamentos e bancadas de inspeção muitas vezes não consegue pôr a câmara mesmo por cima da peça. Coberturas, tubagens, proteções e o alcance do robot decidem-se primeiro; a lente entra depois. Medir ou ler essa imagem tal qual deixa um trapézio largo à frente e estreito atrás, por isso comprimentos e posições saem enviesados.

Emparelhe as quatro esquinas e recupera o retângulo do plano que quer, sem reconstruir o quadro. A recolha de dados continua possível quando a pose da câmara está limitada pela estrutura. O truque real é pôr os pontos nas quatro esquinas do plano físico, não nas quatro do ecrã. Um canto de peça, um furo de dispositivo, uma junta do chão — qualquer ponto que possa apontar outra vez mais tarde — e os passos deste artigo aplicam-se tal como estão.

Não precisa de escrever a matemática sozinho

O OpenCV é uma implementação para esse trabalho. Dispor os pontos com NumPy e passá-los a getPerspectiveTransform e warpPerspective do OpenCV é escolher uma implementação mantida em vez de escrever a álgebra sozinho. Essa escolha não se limita ao OpenCV. Em torno do processamento de imagem, do cálculo numérico, das comunicações e do controlo, o mesmo trabalho já tem implementações, mantidas e versionadas por outras pessoas. Ler e usar esse código devolve-o mais cedo às limitações da máquina do que derivar a matemática projetiva do zero.

Usar chega. Se encontrar um buraco ou algo pouco claro, pode reportar, corrigir a documentação ou enviar um patch. Quanto devolve varia. Mesmo zero ainda o coloca do lado que mantém esse trabalho, à espera da versão seguinte. Ajuste a ferramenta à sua máquina e, se precisar, devolva um pouco. Esse ciclo gasta melhor o tempo de engenharia do que carregar com uma reescrita interna completa.

Mesmo com uma câmara de lado, quatro esquinas continuam a fazer um retângulo de dados. Depois, o trabalho verdadeiro é medir, ler e guardar o que está nesse retângulo.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *