Transformaciones afines de imágenes en Python con Numpy, Pillow y OpenCV

T

En este artículo describiré lo que significa aplicar una transformación afín a una imagen y cómo hacerlo en Python. Primero demostraré las operaciones de bajo nivel en Numpy para dar una implementación geométrica detallada. Luego, los pasaré a un uso más práctico de las bibliotecas Python Pillow y OpenCV .

Este artículo se escribió usando un cuaderno de Jupyter y la fuente se puede encontrar en mi repositorio de GitHub , así que no dude en clonarlo / bifurcarlo y experimentar con el código.

¿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 conservan puntos, líneas rectas y paralelas, así como relaciones entre puntos. Toda esa redacción abstracta matemática se reduce a una transformación lineal vagamente hablando que da como resultado, al menos en el contexto del procesamiento de imágenes, una o más manipulaciones como rotar, voltear, escalar o cortar aplicando una matriz de transformación.

Una cosa buena es que, dado que se trata esencialmente de una operación geométrica 2D, podemos visualizarla. Permítanme comenzar dando una tabla de transformaciones afines que describen cada tipo de manipulación geométrica.

Tipo de transformación Matriz de transformación Ecuación de mapeo de píxeles

Identidad$$
begin {bmatrix}
1 & 0 & 0 \
0 & 1 & 0 \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = x$$
$$y^{‘} = y$$
Escalada$$
begin {bmatrix}
c_ {x} & 0 & 0 \
0 & c_ {y} & 0 \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = c_{x} * x$$
$$y^{‘} = c_{y} * y$$
Rotación*$$
begin {bmatrix}
cos Theta & sin Theta & 0 \
-sin Theta & cos Theta & 0 \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = x * cos Theta – y * sin Theta$$
$$y^{‘} = x * cos Theta + y * sin Theta$$
Traducción$$
begin {bmatrix}
1 & 0 & t_ {x} \
0 & 1 & t_ {y} \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = x + t_{x}$$
$$y^{‘} = y + t_{y}$$
Cizalla horizontal$$
begin {bmatrix}
1 & s_ {h} & ​​0 \
0 & 1 & 0 \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = x + s_{v} * y$$
$$y^{‘} = y$$
Cizalla vertical$$
begin {bmatrix}
1 & 0 & 0 \
s_ {v} & 1 & 0 \
0 & 0 & 1
end {bmatrix}
$$
$$x^{‘} = x$$
$$y^{‘} = x * s_{h} + y$$

* La transformación afín utiliza un ángulo de rotación en el sentido de las agujas del reloj, que contrasta con el círculo de ángulos de la unidad de geometría típica que se mide en rotación en sentido contrario a las agujas del reloj con 0 comenzando desde el eje X positivo, por lo que verá que a menudo se aplica el negativo del ángulo. .

' la notación aquí solo se refiere a la coordenada de salida transformada de xoy no a la notación de cálculo para una derivada

Para una demostración simple, aplicaré un par de transformaciones para manipular las coordenadas xey de los siguientes puntos que tienen componentes tridimensionales de x, y y un índice de caracteres ascii similar a la forma en que un píxel de 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)

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

Para comenzar, quiero construir una matriz Numpy (algunos pueden llamar a esto una matriz) con cada fila representando 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. A continuación, uso Matplotlib para trazar los puntos (después de aplicar la transformación de identidad invariable) para dar una imagen de referencia de dónde nos encontramos.

Pointx (fila) y (columna) índice ascii

a010
b101
c0-12
re-103
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, byc se trazaron en una cuadrícula después de aplicarles la transformación de identidad a través de un producto escalar de matriz vectorial simple dejándolos sin cambios.

Ahora pasaré a crear una matriz de transformación de escala (T_s), como se muestra a continuación, que escala la ubicación de los puntos en todas las direcciones.

$$
T_s = begin {bmatrix}
2 & 0 & 0 \
0 & 2 & 0 \
0 & 0 & 1
end {bmatrix}
$$

Ahora pasaré a trazar los puntos transformados de manera 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 de puntos que conecta 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, its 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()

A partir del gráfico anterior, debe quedar muy claro que las dimensiones xey simplemente se ampliaron en un factor de dos, mientras que la tercera dimensión responsable del índice de letras ASCII se mantuvo sin cambios. De hecho, aquellos familiarizados con el álgebra matricial habrán notado que para todas las transformaciones afines enumeradas en la primera tabla, el valor representado en la tercera dimensión siempre se deja sin alterar como lo indican los ceros y un valor solitario en la tercera dimensión. índice de la última columna.

Ahora permítanme describir cómo interpretar la transformación de rotación. Comenzaré resolviendo las dos funciones trigonométricas para el ángulo de rotación deseado de 90 grados, luego simplemente las conecto a la matriz de transformación de rotación que se muestra en la tabla anterior.

$$
sin (90^{o}) = 1
$$

$$
cos (90 o) = 0
$$

$$
T_r = begin {bmatrix}
0 & 1 & 0 \
-1 & 0 & 0 \
0 & 0 & 1
end {bmatrix}
$$

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, its 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()

Con suerte, puede deducir del gráfico que todos los puntos giraron 90 grados alrededor de un eje de rotación en el origen.

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

$$
T_ {comb} = begin {bmatrix}
0 & 1 & 0 \
-1 & 0 & 0 \
0 & 0 & 1
end {bmatrix} *
begin {bmatrix}
2 & 0 & 0 \
0 & 2 & 0 \
0 & 0 & 1
end {bmatrix} =
begin {bmatrix}
0 & 2 & 0 \
-2 & 0 & 0 \
0 & 0 & 1
end {bmatrix}
$$

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

# create combined tranformation 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, its 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()

Trabajar con una imagen

A estas alturas, espero haber podido desarrollar algo de intuición sobre cómo se usan las transformaciones afines para simplemente mover puntos en el espacio 2D, así que con eso fuera del camino, me gustaría comenzar a trabajar con algunos datos de imágenes reales para dar una demostración más concreta de cómo funciona todo esto.

Esto también me permite cubrir otro tema importante de las transformaciones afines que se ocupa de la tercera dimensión. La tercera dimensión de los datos en una imagen representa el valor de píxel real, o algunas veces se denomina dominio de intensidad, mientras que la ubicación física 2D de los píxeles en las otras dos dimensiones se denomina dominio espacial.

Para comenzar, leeré y mostraré una imagen usando matplotlib, que es simplemente una gran letra mayúscula R.

img = plt.imread('letterR.jpg')
img.shape #  (1000, 1000, 4)

Usando el imread(...)método, puedo leer en la imagen JPG, que representa la letra mayúscula R, en un ndarray numpy. Luego muestro las dimensiones de la matriz, que son 1000 filas por 1000 columnas, que juntas forman 1,000,000 de ubicaciones de píxeles en el dominio espacial. Los datos de los píxeles individuales tienen entonces la forma de una matriz de 4 números enteros sin signo que representan un canal (o muestra) rojo, verde, azul y alfa que juntos proporcionan los datos de intensidad de cada píxel.

plt.figure(figsize=(5, 5))
plt.imshow(img)

A continuación, me gustaría aplicar la escala y la rotación anteriores al dominio espacial de los datos de la imagen, transformando así las ubicaciones de los píxeles de manera similar a lo que demostré anteriormente con los datos de los puntos. Sin embargo, necesito adoptar un enfoque ligeramente diferente porque los datos de la imagen están organizados de una manera diferente a la de las filas de puntos de datos con los que trabajé anteriormente. Con los datos de la imagen, necesito mapear los índices para cada píxel de los datos de entrada a los índices de salida transformados usando la matriz de transformación T, definida anteriormente.

# 2x scaling requires a tranformation image array 2x the original image
img_transformed = np.empty((2000, 2000, 4), dtype=np.uint8)
for i, row in enumerate(img):
    for j, col in enumerate(row):
        pixel_data = img[i, j, :]
        input_coords = np.array([i, j, 1])
        i_out, j_out, _ = T @ input_coords
        img_transformed[i_out, j_out, :] = pixel_data

plt.figure(figsize=(5, 5))
plt.imshow(img_transformed)

Trazar la imagen después de aplicar la transformación muestra claramente que la imagen original se ha girado 90 grados en el sentido de las agujas del reloj y se ha ampliado 2X. Sin embargo, el resultado ahora está obviamente disminuido, ya que puede ver fácilmente la discontinuidad en las intensidades de píxeles.

Para comprender la razón de esto, volveré a utilizar un diagrama de cuadrícula simple para la demostración. Considere una gráfica de 4 cuadrados en una cuadrícula de 2×2 similar al dominio espacial de una imagen de 2×2.

def plot_box(plt, x0, y0, txt, w=1, h=1):
    plt.scatter(x0, y0)
    plt.scatter(x0, y0 + h)
    plt.scatter(x0 + w, y0 + h)
    plt.scatter(x0 + w, y0)
    plt.plot([x0, x0, x0 + w, x0 + w, x0], [y0, y0 + h, y0 + h, y0, y0], color="gray", linestyle="dotted")
    plt.text(x0 + (.33 * w), y0 + (.5 * h), txt)

#             x0, y0, letter
a = np.array((0,  1,  0))
b = np.array((1,  1,  1))
c = np.array((0,  0,  2))
d = np.array((1,  0,  3))

A = np.array([a, b, c, d])
fig = plt.figure()
ax = plt.gca()
for pt in A:
    x0, y0, i = I @ pt
    x0, y0, i = int(x0), int(y0), int(i)
    plot_box(plt, x0, y0, f"{string.ascii_letters[int(i)]} ({x0}, {y0})")

ax.set_xticks(np.arange(-1, 5, 1))
ax.set_yticks(np.arange(-1, 5, 1))
plt.grid()
plt.show()

Ahora observe lo que sucede cuando aplico una transformación de escala 2X como se muestra a continuación. Recordar que:

$$
T_s = begin {bmatrix}
2 & 0 & 0 \
0 & 2 & 0 \
0 & 0 & 1
end {bmatrix}
$$

Notarás que tal transformación espacial da como resultado … bueno, “huecos” para ponerlo en términos simples, que he hecho obvio al trazar signos de interrogación junto con las coordenadas. La cuadrícula de 2×2 se transforma en una cuadrícula de 3×3 y los cuadrados originales se reposicionan en función de la transformación lineal aplicada. Esto significa que (0,0) * (T_s) permanece (0,0) debido a sus propiedades como vector 0, pero todos los demás se escalan por dos, como (1,1) * (T_s) -> (2 , 2).

fig = plt.figure()
ax = plt.gca()
for pt in A:
    xt, yt, i = T_s @ pt
    xt, yt, i = int(xt), int(yt), int(i)
    plot_box(plt, xt, yt, f"{string.ascii_letters[i]}' ({xt}, {yt})")

delta_w, delta_h = 0.33, 0.5
plt.text(0 + delta_w, 1 + delta_h, "? (0, 1)")
plt.text(1 + delta_w, 0 + delta_h, "? (1, 0)")
plt.text(1 + delta_w, 1 + delta_h, "? (1, 1)")
plt.text(1 + delta_w, 2 + delta_h, "? (1, 2)")
plt.text(2 + delta_w, 1 + delta_h, "? (2, 1)")

ax.set_xticks(np.arange(-1, 5, 1))
ax.set_yticks(np.arange(-1, 5, 1))
plt.grid()
plt.show()

Queda la pregunta de qué hacer con esas lagunas que se han introducido. Un pensamiento intuitivo sería simplemente buscar la respuesta en la imagen original. Da la casualidad de que si aplicamos la inversa de la transformación a una coordenada en la salida, obtendré la ubicación correspondiente de la entrada original.

En operaciones matriciales como el mapeo hacia atrás se ve así:

$$
(x, y, 1) = T_s^{-1} * (x’ y’ 1)
$$

donde x ‘, y’ son las coordenadas en la cuadrícula 3×3 transformada anterior, específicamente la ubicación que falta, como (2, 1), (T_s ^ {- 1}) (los valores reales se muestran a continuación) es el inverso de 2x matriz de escala (T_s) y x, y son las coordenadas que se encuentran en la cuadrícula original de 2×2.

$$
T_s ^ {- 1} = comenzar {bmatrix}
1/2 & 0 & 0 \
0 & 1/2 & 0 \
0 & 0 & 1
end {bmatrix} ^ {- 1}
$$

Sin embargo, pronto se dará cuenta de que hay un pequeño problema que aún debe resolverse debido al hecho de que cada una de las coordenadas de la brecha se asigna a valores fraccionarios del sistema de coordenadas 2×2. En el caso de los datos de imagen, no se puede tener una fracción de píxel. Esto será más claro con un ejemplo de mapeo de la brecha (2, 1) al espacio original de 2×2, así:

$$
T_s ^ {- 1} * (2, 1, 1) = (1, 1/2, 1)
$$

En este caso, redondearé y ‘= 1/2 a 0 y diré que se corresponde con (1, 0). En el sentido general, este método de seleccionar un valor en la cuadrícula original de 2×2 para colocar en los espacios de la cuadrícula transformada de 3×3 se conoce como interpolación, y en este ejemplo específico estoy usando una versión simplificada del método de interpolación del vecino más cercano.

Ok, ahora volvamos a los datos de la imagen. Debería quedar bastante claro qué se debe hacer ahora para corregir esos huecos en la versión escalada y rotada de la letra R.Debo desarrollar una implementación de la interpolación del vecino más cercano basada en el mapeo hacia atrás, usando la inversa de la matriz de transformación T, de las coordenadas de píxeles en la imagen transformada para encontrar la coincidencia exacta o el vecino más cercano en la imagen original.

T_inv = np.linalg.inv(T)

# nearest neighbors interpolation
def nearest_neighbors(i, j, M, T_inv):
    x_max, y_max = M.shape[0] - 1, M.shape[1] - 1
    x, y, _ = T_inv @ np.array([i, j, 1])
    if np.floor(x) == x and np.floor(y) == y:
        x, y = int(x), int(y)
        return M[x, y]
    if np.abs(np.floor(x) - x) < np.abs(np.ceil(x) - x):
        x = int(np.floor(x))
    else:
        x = int(np.ceil(x))
    if np.abs(np.floor(y) - y) < np.abs(np.ceil(y) - y):
        y = int(np.floor(y))
    else:
        y = int(np.ceil(y))
    if x > x_max:
        x = x_max
    if y > y_max:
        y = y_max
    return M[x, y,]

img_nn = np.empty((2000, 2000, 4), dtype=np.uint8)
for i, row in enumerate(img_transformed):
    for j, col in enumerate(row):
        img_nn[i, j, :] = nearest_neighbors(i, j, img, T_inv)

plt.figure(figsize=(5, 5))
plt.imshow(img_nn)

No está mal, ¿verdad?

Debo señalar que en la mayoría de los casos el método del vecino más cercano no será suficiente. Hay otros dos métodos de interpolación más comunes conocidos como interpolación bilineal y bicúbica que generalmente proporcionan resultados mucho mejores. Hablaré más sobre estos otros algoritmos de interpolación cuando presente las bibliotecas Pillow y OpenCV en las últimas secciones. El propósito de esta sección es simplemente desarrollar una comprensión intuitiva de cómo funcionan las cosas.

Transformaciones afines con almohada

En esta sección, cubriré brevemente cómo utilizar la excelente biblioteca de procesamiento de imágenes de Python Pillow para realizar transformaciones afines.

En primer lugar, será necesario instalar Pillow. Usé pip para lograr esto, así:

$ pip install pillow

Ahora el primer paso es importar la Imageclase del módulo PIL (PIL es el nombre del módulo Python asociado con Pillow) y leer mi imagen.

from PIL import Image

Para leer el nombre del archivo de imagen de muestra “letterR.jpg”, llamo al método de la clase Image.open(...), pasándole el nombre del archivo, que devuelve una instancia de la Imageclase, que luego convierto en una matriz numpy y muestro con matplotlib.

img = Image.open('letterR.jpg')
plt.figure(figsize=(5, 5))
plt.imshow(np.asarray(img))

La Imageclase Pillow tiene un método útil llamado transform(...)que le permite realizar transformaciones afines de grano fino, pero hay algunas rarezas que debo discutir primero antes de saltar a una demostración. El transform(...)método comienza con dos parámetros requeridos representados sizecomo una tupla de alto y ancho, seguido del methodde transformación a aplicar, que será Image.AFFINEen este caso.

Los parámetros restantes son argumentos de palabras clave opcionales que controlan cómo se realizará la transformación. En el caso de este ejemplo, usaré el dataparámetro, que toma las dos primeras filas de una matriz de transformación afín.

Por ejemplo, la matriz de transformación de escala 2x con la que he estado trabajando recortada a solo las dos primeras filas se ve así:

$$
T_s = begin {bmatrix}
2 & 0 & 0 \
0 & 2 & 0
end {bmatrix}
$$

El último parámetro que usaré con el transform(...)método es resample, que se usa para indicar el tipo de algoritmo de interpolación de píxeles que se aplicará entre las posibles opciones de Image.NEAREST(vecino más cercano) Image.BILINEAR, o Image.BICUBIC. Esta elección a menudo variará según la transformación que se aplique. Sin embargo, bilineal y bicubic generalmente dan mejores resultados que el vecino más cercano, pero como ya se demostró en este ejemplo, el vecino más cercano funciona bastante bien.

Hay algunas peculiaridades que me sirvieron como verdaderas trampas la primera vez que utilicé el Image.transform(...)método, particularmente en la construcción de la matriz de transformación afín con la última fila extrañamente truncada. Por lo tanto, me gustaría dedicar un tiempo a repasar por qué las cosas funcionan de la manera en que lo hacen porque es un proceso.

Lo primero que debe suceder es que la imagen debe ser traducida para que el origen (0, 0) esté en el medio de la imagen. En el caso de la imagen de 1000 x 1000 de la letra R en este ejemplo, eso significa una traslación de -500 en x e y.

A continuación, muestro la matriz de transformación de traducción genérica (T_ {translate}) y la que usaré en el ejemplo (T_ {neg500}).

$$
T_ {translate} = begin {bmatrix}
1 & 0 & t_x \
0 & 1 & t_y \
0 & 0 & 1
end {bmatrix}
$$

$$
T_ {neg500} = begin {bmatrix}
1 & 0 & -500
0 & 1 & -500
0 & 0 & 1
end {bmatrix}
$$

Luego están las matrices de escala 2X (T_ {escala}) y rotación de 90 grados (T_ {rotar}) de antes. Sin embargo, la biblioteca Pillow en realidad decidió usar ángulos geométricos estándar (es decir, en sentido antihorario) en lugar de las rotaciones en sentido horario que describí anteriormente, por lo que los signos de las funciones sin cambian. A continuación se muestran las matrices de transformación individuales resultantes.

$$
T_ {rotate} = begin {bmatrix}
0 & -1 & 0 \
1 & 0 & 0 \
0 & 0 & 1
end {bmatrix}
$$

$$
T_ {scale} = begin {bmatrix}
2 & 0 & 0
0 & 2 & 0
0 & 0 & 1
end {bmatrix}
$$

A continuación, se debe aplicar otra matriz de traducción que actúa para reposicionar el dominio espacial de los píxeles negando esencialmente el primero que centró el origen. En este caso, necesito una traducción positiva de 1000 en x e y, donde 1000 proviene del doble del original porque se ha ampliado en dos.

$$
T_ {pos1000} = begin {bmatrix}
1 & 0 & 1000 \
0 & 1 & 1000 \
0 & 0 & 1
end {bmatrix}
$$

Estos constituyen los pasos de transformación individuales que se requieren, por lo que todo lo que queda es multiplicar las matrices en orden (es decir, de derecha a izquierda), así:

$$
T = T_ {pos1000} * T_ {rotate} * T_ {scale} * T_ {neg500}
$$

Ok, en realidad hay una última rareza. En Image.transform(...)realidad, el método requiere que se suministre al dataparámetro la inversa de la matriz de transformación como una matriz plana (o tupla) excluyendo la última fila.

$$
T_ {inv} = T ^ {- 1}
$$

En el código, todo esto funciona de la siguiente manera:


# recenter resultant image
T_pos1000 = np.array([
    [1, 0, 1000],
    [0, 1, 1000],
    [0, 0, 1]])
# rotate - opposite angle
T_rotate = np.array([
    [0, -1, 0],
    [1, 0, 0],
    [0, 0, 1]])
# scale
T_scale = np.array([
    [2, 0, 0],
    [0, 2, 0],
    [0, 0, 1]])
# center original to 0,0
T_neg500 = np.array([
    [1, 0, -500],
    [0, 1, -500],
    [0, 0, 1]])
T = T_pos1000 @ T_rotate @ T_scale @ T_neg500
T_inv = np.linalg.inv(T)
img_transformed = img.transform((2000, 2000), Image.AFFINE, data=T_inv.flatten()[:6], resample=Image.NEAREST)
plt.imshow(np.asarray(img_transformed))

Transformaciones afines con OpenCV2

Continuando, me gustaría describir brevemente cómo llevar a cabo estas transformaciones afines con la popular biblioteca de procesamiento de imágenes y visión por computadora OpenCV. Utilizo la palabra breve aquí porque es en gran medida la misma que se requiere en la demostración anterior con Pillow.

Lo primero es lo primero, debes instalar así:

$ pip install opencv-python

Como mencioné anteriormente, existe una superposición significativa en la metodología entre el enfoque Pillow y el uso de OpenCV. Por ejemplo, aún crea una matriz de transformación que primero centra la matriz de píxeles en el origen y solo usa las dos primeras filas de la matriz de transformación. La principal diferencia es que con OpenCV le da la matriz estándar en lugar de la inversa.

Entonces, con esa comprensión establecida, saltaré al código comenzando con la importación del módulo opencv-python, que se llama cv2.

import cv2

Leer la imagen es tan simple como llamar al cv2.imread(...)método, pasando el nombre del archivo como argumento. Esto devuelve los datos de la imagen en forma de una matriz numérica 3D, similar a cómo funciona matplotlib, pero los datos de píxeles en la tercera dimensión se componen de una matriz de canales en el orden de azul, verde, rojo en lugar de rojo, verde, azul, alfa como en el caso de la lectura con matplotlib.

Por lo tanto, para trazar los numerosos datos de imagen que se originan en la biblioteca OpenCV, se debe invertir el orden de los canales de píxeles. Afortunadamente, OpenCV proporciona un método convincente cvtColor(...)que puede usarse para hacer esto como se muestra a continuación (aunque es probable que los puristas sepan que img[:,:,::-1]hará lo mismo).

img = cv2.imread('letterR.jpg')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))

Algunos últimos elementos a mencionar son que OpenCV requiere que los datos en la matriz de transformación sean de tipo flotante de 32 bits en lugar del flotante predeterminado de 64 bits, así que asegúrese de convertirlos a 32 bits con numpy.float32(...). Además, la API cv2.warpAffine(...)no proporciona la capacidad de especificar qué tipo de algoritmo de interpolación de píxeles aplicar y no pude determinar a partir de los documentos qué se usa. Si lo sabe o lo averigua, publique en los comentarios a continuación.

T_opencv = np.float32(T.flatten()[:6].reshape(2,3))
img_transformed = cv2.warpAffine(img, T_opencv, (2000, 2000))
plt.imshow(cv2.cvtColor(img_transformed, cv2.COLOR_BGR2RGB))

Conclusión

En este artículo, he cubierto qué es una transformación afín y cómo se puede aplicar al procesamiento de imágenes usando Python. Se utilizó puro numpy y matplotlib para dar una descripción intuitiva de bajo nivel de cómo funcionan las transformaciones afines. Concluí demostrando cómo se puede hacer lo mismo usando dos bibliotecas populares de Python, Pillow y OpenCV.

Gracias por leer y, como siempre, no dude en comentar o criticar a continuación.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad