Transformaciones afines en Python con Numpy, Pillow y OpenCV (Parte 1)

Transformaciones afines en Python con Numpy, Pillow y OpenCV (Parte 1)

Juan Gabriel Gomila Juan Gabriel Gomila
9 minutos

Leer el artículo
Audio generated by DropInBlog's Blog Voice AI™ may have slight pronunciation nuances. Learn more

En este artículo voy a describir lo que significa aplicar transformaciones afines a una imagen y cómo hacerlo en Python. Esta es la primera parte de una serie de dos artículos, donde en ella  demostraremos las operaciones de bajo nivel en NumPy para dar una implementación geométrica detallada. En la segunda parte, pasaremos a demostrar un uso más práctico de las librerías Python Pillow y OpenCV.

Este artículo fue escrito usando un cuaderno Jupyter y la fuente se puede encontrar en mi repositorio GitHub así que, por favor, siéntete libre de clonar / realizar fork del repositorio y experimentar con el código.

Este artículo es una traducción y adaptación del post original en inglés disponible aquí.

Qué es una transformación afín

Según Wikipedia, una transformación afín es un mapeo funcional entre dos espacios geométricos (afines) que conserva puntos, líneas rectas y paralelas, así como relaciones entre puntos. En pocas palabras, se trata de una transformación lineal que, al menos en el contexto del tratamiento de imágenes, permite realizar una o varias manipulaciones como rotar, voltear, escalar o cizallar mediante la aplicación de una matriz de transformación.

Lo bueno es que, al tratarse esencialmente de una operación geométrica 2D, podemos visualizarla. Empezaré dando una tabla de transformaciones afines que describen cada tipo de manipulación geométrica.

Transformaciones Afines y sus versiones matriciales

La transformación afín utiliza ángulos de rotación en el sentido de las agujas del reloj, lo que contrasta con el típico círculo geométrico unitario en el que los ángulos se miden en el sentido contrario a las agujas del reloj, con 0 empezando en el eje X positivo.

Aquí la notación se refiere a la coordenada de salida transformada de x o y, no a la notación de cálculo para una derivada.

Para una simple demostración aplicaré un par de transformaciones para manipular las coordenadas x e y de los siguientes puntos que tienen componentes tridimensionales de x, y e índice de caracteres ASCII de forma similar a como un píxel de una imagen tiene componentes tridimensionales de x, y, y frecuencia (o intensidad).

a = (0, 1, 0)

b = (1, 0, 1)

c = (0, -1, 2)

d = (-1, 0, 3)

Los puntos de ejemplo para la transformación afines

Las transformaciones para este ejemplo serán Escalado por 2 en todas las direcciones y rotación de 90 grados en el sentido de las agujas del reloj. Primero realizaré las transformaciones individualmente para mostrar el efecto directo que cada una tiene al mover los puntos, luego combinaré las transformaciones y las aplicaré en una sola acción.

Para empezar quiero construir una matriz NumPy (algunos pueden llamar a esto una matriz) con cada fila que representa el punto donde la primera columna es la x, la segunda la y, y la tercera es el índice de su letra en el conjunto de caracteres ASCII similar a la tabla que se muestra a continuación. Seguidamente, utilizo Matplotlib para trazar los puntos (después de aplicar la transformación de identidad inmutable) para dar una línea de base visual de dónde estamos.

Punto x (fila) y (columna) índice ascii
a 0 1 0
b 1 0 1
c 0 -1 2
d -1 0 3
 
import matplotlib.pyplot as plt
import numpy as np
import string

# points a, b and, c
a, b, c, d = (0, 1, 0), (1, 0, 1), (0, -1, 2), (-1, 0, 3)

# matrix with row vectors of points
A = np.array([a, b, c, d])

# 3x3 Identity transformation matrix
I = np.eye(3)
color_lut = 'rgbc'
fig = plt.figure()
ax = plt.gca()
xs = []
ys = []
for row in A:
    output_row = I @ row
    x, y, i = output_row
    xs.append(x)
    ys.append(y)
    i = int(i) # convert float to int for indexing
    c = color_lut[i]
    plt.scatter(x, y, color=c)
    plt.text(x + 0.15, y, f"{string.ascii_letters[i]}")
xs.append(xs[0])
ys.append(ys[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()

Los tres puntos a, b y c trazados en una cuadrícula después de aplicarles la transformación Identidad mediante un simple producto punto de matrices vectoriales que los deja inalterados.

Escalado

Ahora voy a pasar a la creación de una matriz de transformación de escala Ts , como se muestra a continuación, que escala la colocación de los puntos en todas las direcciones.

Ahora pasaremos a representar los puntos transformados de forma similar a lo que se hizo con los puntos originales inalterados por la transformación de Identidad pero, esta vez aplicaré la matriz de transformación de escala definida anteriormente. Para una mejor visualización, trazo una línea punteada conectando los puntos.

# create the scaling transformation matrix
T_s = np.array([[2, 0, 0], [0, 2, 0], [0, 0, 1]])

fig = plt.figure()
ax = plt.gca()
xs_s = []
ys_s = []
for row in A:
    output_row = T_s @ row
    x, y, i = row
    x_s, y_s, i_s = output_row
    xs_s.append(x_s)
    ys_s.append(y_s)
    i, i_s = int(i), int(i_s) # convert float to int for indexing
    c, c_s = color_lut[i], color_lut[i_s] # these are the same but, it's good to be explicit
    plt.scatter(x, y, color=c)
    plt.scatter(x_s, y_s, color=c_s)
    plt.text(x + 0.15, y, f"{string.ascii_letters[int(i)]}")
    plt.text(x_s + 0.15, y_s, f"{string.ascii_letters[int(i_s)]}'")

xs_s.append(xs_s[0])
ys_s.append(ys_s[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
plt.plot(xs_s, ys_s, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()

En el gráfico anterior se ve claramente que las dimensiones x e y se han multiplicado por dos, mientras que la tercera dimensión, responsable del índice de letras ASCII, no se ha modificado. De hecho, los que estén familiarizados con el álgebra matricial se habrán dado cuenta de que para todas las transformaciones afines enumeradas en la primera tabla, el valor representado en la tercera dimensión siempre se deja inalterado, como indica el valor todo ceros y uno solo en el índice de la tercera dimensión de la última columna.

Rotación

Ahora voy a describir cómo interpretar la transformación de rotación. Empezaré resolviendo las dos funciones trigonométricas para el ángulo de rotación deseado de 90 grados, luego simplemente las introduzco en la matriz de transformación de rotación que aparece en la tabla anterior.

Ahora todo lo que necesito hacer es aplicar la misma lógica para transformar y trazar los puntos, así:

# create the rotation transformation matrix
T_r = np.array([[0, 1, 0], [-1, 0, 0], [0, 0, 1]])

fig = plt.figure()
ax = plt.gca()
for row in A:
    output_row = T_r @ row
    x_r, y_r, i_r = output_row
    i_r = int(i_r) # convert float to int for indexing
    c_r = color_lut[i_r] # these are the same but, it's good to be explicit
    letter_r = string.ascii_letters[i_r]
    plt.scatter(x_r, y_r, color=c_r)
    plt.text(x_r + 0.15, y_r, f"{letter_r}'")

plt.plot(xs, ys, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()

Espero que puedas ver en el gráfico que todos los puntos se han girado 90 grados alrededor de un eje de rotación en el origen.

Combinación de transformaciones afines

Lo bueno de que las transformaciones afines sean esencialmente transformaciones lineales es que puedes combinar las transformaciones y aplicarlas en un solo paso. Para demostrar esto voy a aplicar el producto punto (multiplicación de matrices) de mis dos matrices de transformación, como:

Ahora puedo aplicar esta matriz de transformación combinada a los puntos y volver a representarlos para mostrar una combinación de escalado por dos y rotación por 90 grados.

# create combined transformation matrix
T = T_s @ T_r

fig = plt.figure()
ax = plt.gca()

xs_comb = []
ys_comb = []
for row in A:
    output_row = T @ row
    x, y, i = row
    x_comb, y_comb, i_comb = output_row
    xs_comb.append(x_comb)
    ys_comb.append(y_comb)
    i, i_comb = int(i), int(i_comb) # convert float to int for indexing
    c, c_comb = color_lut[i], color_lut[i_comb] # these are the same but, it's good to be explicit
    letter, letter_comb = string.ascii_letters[i], string.ascii_letters[i_comb]
    plt.scatter(x, y, color=c)
    plt.scatter(x_comb, y_comb, color=c_comb)
    plt.text(x + 0.15 , y, f"{letter}")
    plt.text(x_comb + 0.15, y_comb, f"{letter_comb}'")
xs_comb.append(xs_comb[0])
ys_comb.append(ys_comb[0])
plt.plot(xs, ys, color="gray", linestyle='dotted')
plt.plot(xs_comb, ys_comb, color="gray", linestyle='dotted')
ax.set_xticks(np.arange(-2.5, 3, 0.5))
ax.set_yticks(np.arange(-2.5, 3, 0.5))
plt.grid()
plt.show()

Conclusión

En esta primera parte hemos cubierto las diferentes transformaciones afines en Python utilizando la librería Numpy. Recuerda que en la segunda parte analizamos las mismas transformaciones afines dentro del mundo de la visión por computador aplicadas a imágenes con las librerías OpenCV y Pillow, así que ¡no dejes de visitar nuestro blog! 

Y para mejorar en tus habilidades de programación con Python, recuerda que nuestro curso estrella de Python de la A a la Z te enseña a programar ¡viviendo una aventura de piratas! ¡Al abordaje, grumete!

¡Nos vemos en clase!

« Volver al Blog

Obtener mi regalo ahora