Cómo hacer una transformación de perspectiva en OpenCV, explicado con imágenes y coordenadas

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

Written by

in

Este artículo usa Python y OpenCV para una transformación de perspectiva: pasar las cuatro esquinas de una imagen a otras cuatro. En síntesis, getPerspectiveTransform calcula la matriz y warpPerspective deforma los píxeles. El ejemplo es una cuadrícula 600×600 con una silueta. (Las etiquetas de las figuras están en inglés.)

Figura de perspectiva OpenCV, según una cuadrícula y una silueta. Muestra que emparejar cuatro esquinas hace un trapecio o un paralelogramo. Los paneles son frente, cizalla, trapecio ancho abajo y uno que se aleja a la derecha.
La misma entrada, con solo los destinos de las cuatro esquinas cambiados

Puntos clave

Mira estoIdea
Qué estamos haciendoFijas a dónde van las cuatro esquinas y el plano se estira en trapecio
Código y resultadoEjecuta el script de abajo con la misma carpeta que la figura
CarpetasPon source_pics y save_pics junto al script
CoordenadasArriba a la izquierda es [0, 0]. El orden es arriba-izq. → abajo-izq. → abajo-der. → arriba-der.
El resto de líneasLectura, tamaño, matriz, deformación, guardado
Tamaño de salidaLos píxeles fuera del tercer argumento se recortan
Tu propia imagenCambia solo el orden de los puntos y el tamaño de salida al rectángulo que quieres
Cámara limitadaAunque esté en ángulo, emparejar las cuatro esquinas del plano real sigue dando un rectángulo de datos

Una transformación de perspectiva fija a dónde van las cuatro esquinas

Una transformada afín (warpAffine) deja las paralelas paralelas. Una de perspectiva (getPerspectiveTransform y warpPerspective) mueve las cuatro esquinas por separado, así que puedes hacer un trapecio ancho delante y estrecho detrás. Poner una tarjeta de visita en un rectángulo visto desde arriba, o mirar un suelo en planta, son usos de estas funciones.

Pasa las esquinas de entrada y las de salida en el mismo orden. Este artículo no sigue el álgebra de la matriz. Si sabes a dónde van las cuatro esquinas, puedes usar las funciones.

El script y el antes/después

Primero un script que funciona. Las secciones siguientes siguen las coordenadas y el resto de líneas.

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 y después de una transformación de perspectiva, comparando archivo de entrada y de salida. Muestra que acercar las esquinas de arriba hace un trapecio estrecho arriba. Izquierda: test_1.png cuadrado; derecha: perspective_image.png.
test_1.png antes y perspective_image.png después

Este ejemplo acerca el borde superior, así que el resultado es un trapecio estrecho arriba y ancho abajo. El margen negro es la parte del lienzo de salida que queda fuera del cuadrilátero deformado.

Pon las carpetas junto al script

Disposición para ejecutar el script, según dónde están el script y las carpetas de entrada y salida. Relaciona las rutas del código con dónde se lee y se escribe. test_1.py está junto a source_pics y save_pics.
source_pics y save_pics junto a test_1.py

Las rutas del código son relativas al test_1.py que ejecutas. La imagen que lees es source_pics/test_1.png; el archivo que escribes es save_pics/perspective_image.png. Con tus archivos, iguala esos nombres o cambia las cadenas de ruta.

💡 Tip

Si imread falla, image es None y la línea siguiente, image.shape, se detiene. Antes de ejecutar, revisa la ortografía de las carpetas y el directorio de trabajo actual.

Las coordenadas empiezan en [0, 0] arriba a la izquierda

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)

Los arrays van en este orden. El origen no está abajo, como en un papel milimetrado.

Ordensource_pointstarget_points
Arriba izquierda[0, 0][200, 0]
Abajo izquierda[0, height][0, 600]
Abajo derecha[width, height][600, 600]
Arriba derecha[width, 0][400, 0]

Este ejemplo es 600×600, así que height y width valen 600. Con números, los arrays quedan así.

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 cuatro esquinas para una transformación de perspectiva, según source_points y target_points. Con origen arriba a la izquierda, acercar el borde superior acorta la parte de arriba. [0, 0] y [600, 0] pasan a [200, 0] y [400, 0].
Si acercas el borde superior, arriba queda un trapecio estrecho

Los dos puntos de arriba se acercan, así que el borde superior se acorta. Los dos de abajo no se mueven. Cambia los números y el mismo emparejado de cuatro esquinas hace otro trapecio.

⚠️ Error común

Si el orden de entrada y el de salida no coinciden, la correspondencia se tuerce. Escribe ambos como arriba-izq. → abajo-izq. → abajo-der. → arriba-der. Si tomas puntos en un eje y matemático (arriba es positivo), la imagen se ve al revés.

Qué hace el resto de líneas

Importaciones

Python
import numpy as np
import cv2

NumPy guarda los arrays de puntos. El cv2 de OpenCV lee, escribe y deforma imágenes.

Lectura de la imagen

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

cv2.imread carga test_1.png desde source_pics, junto al script, en color, en image.

Tamaño de la imagen

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

shape da altura, ancho y número de canales. Este ejemplo no usa los canales, pero la cantidad de valores que desempaquetas tiene que coincidir, así que se toman los tres.

💡 Tip

Iguala el número de variables al número de valores que desempaquetas.

Puntos para la 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)

Cuatro puntos XY en cada array. En esta imagen de origen [height, width] es [600, 600], así que los arrays contienen:

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

Esas son las coordenadas de la figura. La deformación usa ese emparejado.

La matriz de transformación

Python
mat = cv2.getPerspectiveTransform(source_points, target_points)

cv2.getPerspectiveTransform calcula la matriz a partir de las cuatro esquinas de entrada y las cuatro de salida. En este ejemplo los valores son:

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]]

Aquí no hace falta el significado de cada entrada. Si las esquinas van donde querías, pasa el valor de retorno a la línea siguiente.

La deformación

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

cv2.warpPerspective toma la imagen de origen, la matriz y el ancho y alto de salida. Este ejemplo solo acorta un borde, así que el tamaño de salida sigue en (width, height), igual que el origen. Elige el tamaño real mirando las coordenadas transformadas.

Guardado

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

cv2.imwrite escribe perspective_image.png en save_pics, junto al script. Crea la carpeta antes; si falta, la escritura falla.

Si la salida es demasiado pequeña, el excedente se recorta

Deja la salida en 600×600 y toma puntos como estos, y la parte superior ensanchada se sale del lienzo.

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 si el lienzo de salida es demasiado pequeño, según puntos que salen del marco. Explica que el tercer argumento de warpPerspective es el tamaño escrito. El [0, 0] y [600, 0] originales quedan fuera de 600×600; se corta arriba.
Si la salida sigue en 600×600, la parte superior ensanchada se recorta

El tercer argumento es el tamaño de la imagen que escribes, no el tamaño del mundo deformado. Los píxeles cuyos puntos quedan fuera de ese rectángulo desaparecen. Para conservarlos, agranda primero la salida, o elige puntos target que quepan todos.

⚠️ Error común

Si un x o y de target pasa el ancho o el alto de salida, y dejas el tercer argumento en el tamaño de origen, se recorta. Cuando parece cortado, compara primero los puntos con el tamaño de salida.

Los mismos pasos valen para tus propias fotos

Mantén el orden de las cuatro esquinas y las mismas dos funciones sirven en fotos que no son una cuadrícula. Cambia solo los números de target y el tamaño de salida al rectángulo que quieres.

Aunque la cámara esté en ángulo, emparejar cuatro esquinas sigue dando un rectángulo

Lo que hiciste es una cadena corta: emparejar las esquinas de entrada con las de salida, resolver la matriz y escribir en el lienzo de salida. Las funciones son las mismas en la cuadrícula y en una foto del suelo. En la instalación, solo cambian cómo tomas los puntos y el tamaño de salida.

Cuando la posición de la cámara la fija la máquina

En equipos y bancos de inspección a menudo no puedes poner la cámara justo encima de la pieza. Cubiertas, tuberías, resguardos y el alcance del robot se deciden antes; la lente se encaja después. Si mides o lees esa imagen tal cual, sigues teniendo un trapecio ancho delante y estrecho detrás, así que longitudes y posiciones salen sesgadas.

Empareja las cuatro esquinas y recuperas el rectángulo del plano que quieres, sin reconstruir el bastidor. La recogida de datos sigue siendo posible cuando la pose de la cámara está limitada por la estructura. El truco de verdad es poner los puntos en las cuatro esquinas del plano físico, no en las cuatro del monitor. Una esquina de pieza, un agujero de utillaje, una junta del suelo — cualquier punto que puedas señalar otra vez después — y los pasos de este artículo valen tal cual.

No hace falta escribir las matemáticas tú

OpenCV es una implementación para ese trabajo. Ordenar los puntos con NumPy y pasarlos a getPerspectiveTransform y warpPerspective de OpenCV es elegir una implementación mantenida en vez de escribir el álgebra tú. Esa elección no se limita a OpenCV. En torno al procesado de imagen, el cálculo numérico, las comunicaciones y el control, el mismo trabajo ya tiene implementaciones, y hay quien las mantiene y sube de versión. Leer y usar ese código te devuelve antes a las limitaciones de la máquina que derivar las matemáticas proyectivas desde cero.

Usarlo basta. Si das con un agujero o algo poco claro, puedes reportarlo, arreglar la documentación o enviar un parche. Cuánto devuelves varía. Aunque sea cero, esperar la siguiente versión ya te pone del lado que mantiene ese trabajo. Ajusta la herramienta a tu máquina y, si hace falta, devuelve un poco. Ese bucle gasta mejor el tiempo de ingeniería que cargar con una reescritura interna completa.

Aunque la cámara esté en ángulo, cuatro esquinas siguen haciendo un rectángulo de datos. Después, el trabajo de verdad es medir, leer y guardar lo que hay en ese rectángulo.

Comments

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *