Siguiendo con la tercera y última parte de nuestro artículo Transformaciones afines en Python con Numpy, Pillow y OpenCV (Parte 1 y Parte 2), vamos en esta ocasión a aplicar las transformaciones afines que aprendimos en las partes anterior al mundo de la visión por computador a través de imágenes con un uso más práctico a través de aplicar transformaciones afines con Python a imágenes vectoriales usando Pillow y OpenCV2.
Transformaciones afines con Pillow
En esta sección voy a cubrir brevemente cómo utilizar la excelente biblioteca de procesamiento de imágenes de Python Pillow para realizar transformaciones afines.
En primer lugar, es necesario instalar Pillow. Usé pip para lograrlo, así:
$ pip install pillow
Ahora el primer paso es importar la clase Image del módulo PIL (PIL es el nombre del módulo de Python asociado a Pillow) y leer mi imagen.
from PIL import Image
Para leer la imagen de ejemplo letterR.jpg llamo al método de clase Image.open(...), pasándole el nombre del archivo, que devuelve una instancia de la clase Image, 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 clase Image de Pillow tiene un práctico método 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 del mismo. El método transform(...) comienza con dos parámetros obligatorios que representan el tamaño como una tupla de altura y anchura, seguido del método de transformación a aplicar, que en este caso será Image.AFFINE.
El resto de parámetros son argumentos opcionales que controlan cómo se va a realizar la transformación. En el caso de este ejemplo utilizaré el parámetro data, 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 sólo las dos primeras filas tiene este aspecto:
El último parámetro que utilizaré con el método transform(...) es resample, que se utiliza para indicar el tipo de algoritmo de interpolación de píxeles a aplicar de entre las posibles opciones de Image.NEAREST (vecino más cercano), Image.BILINEAR, o Image.BICUBIC. Esta elección suele variar en función de la transformación que se aplique. Sin embargo, bilineal y bicúbica suelen dar mejores resultados que vecina más cercana, pero como ya se ha demostrado en este ejemplo vecina más cercana funciona bastante bien.
La primera vez que utilicé el método Image.transform(...) me encontré con algunas peculiaridades, sobre todo 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 pasar algún tiempo repasando por qué las cosas funcionan como lo hacen porque es un poco de un proceso.
Lo primero que hay que hacer es trasladar la imagen para que el origen (0, 0) esté en el centro 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 las x y en las y.
A continuación muestro la matriz de transformación de traslación genérica Ttranslate y la que utilizaré en el ejemplo Tneg500:
Luego están las escalas 2X Tscale y rotación de 90 grados Trotate las matrices de antes. Sin embargo, la librería Pillow en realidad decidió utilizar ángulos geométricos estándar (es decir, en sentido contrario a las agujas del reloj) en lugar de las rotaciones en sentido horario que he descrito anteriormente, por lo que los signos de las funciones sen cambian. A continuación se muestran las matrices de transformación individuales resultantes.
A continuación hay que aplicar otra matriz de traslación que actúa para reposicionar el dominio espacial de los píxeles negando esencialmente la primera que centraba el origen. En este caso necesito una traslación positiva de 1000 en x e y, donde 1000 proviene del doble del original porque ha sido escalado por dos.
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í:
De acuerdo, hay una última rareza. El método Image.transform(...) en realidad requiere que la inversa de la matriz de transformación sea suministrada al parámetro data como un array aplanado (o tupla) excluyendo la última fila.
En 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 librería de procesamiento de imágenes y visión por computador OpenCV. Utilizo la palabra breve aquí porque es en gran parte lo mismo que se requiere en la demostración anterior utilizando Pillow.
Lo primero es lo primero, debe instalar así:
$ pip install opencv-python
Como he mencionado anteriormente, hay un solapamiento significativo en la metodología entre el enfoque de Pillow y el uso de OpenCV. Por ejemplo, sigues creando una matriz de transformación que primero centra la matriz de píxeles en el origen y sólo utilizas las dos primeras filas de la matriz de transformación. La mayor diferencia es que con OpenCV le das la matriz estándar en lugar de la inversa.
Por lo tanto, con ese entendimiento establecido voy a saltar en el 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 método cv2.imread(...), pasando el nombre del archivo como argumento. Esto devuelve los datos de la imagen en forma de un array numpy 3D, similar a como funciona matplotlib pero, los datos de píxel en la 3ª dimensión se componen de un array de canales en el orden de azul, verde, rojo en lugar de rojo, verde, azul, alfa como era en el caso de la lectura con matplotlib.
Por lo tanto, con el fin de representar los datos de imagen numpy procedentes de la biblioteca OpenCV hay que invertir el orden de los canales de píxeles. Afortunadamente, OpenCV proporciona un método conveniente cvtColor(...) que se puede utilizar para hacer esto como se muestra a continuación (aunque los puristas de numpy probablemente sabrán que img[:,:,::-1] hará lo mismo).
img = cv2.imread('letterR.jpg')
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
Unos últimos puntos a mencionar son que OpenCV requiere que los datos en la matriz de transformación sean de tipo float de 32 bits en lugar del float de 64 bits por defecto, así que asegúrate de convertir a 32 bits con numpy.float32(...). Además, la API de cv2.warpAffine(...) no ofrece la posibilidad de especificar qué tipo de algoritmo de interpolación de píxeles aplicar y no he podido determinar en la documentación cuál se utiliza. Si lo sabes o lo averiguas, por favor publícalo en los comentarios.
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 en tres partes hemos cubierto lo que es una transformación afín y cómo se puede aplicar al procesamiento de imágenes utilizando Python. Se utilizó numpy puro 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 librerías populares de Python: Pillow y OpenCV.
Gracias por leer y como siempre no seas tímido para comentar o criticar a continuación.