Fundamentos de edición con PIL. Imagen científica con Python

PIL para manipulación de imágenes

Gracias a su sencillez y versatilidad, Python se ha convertido en uno de los lenguajes de programación con más crecimiento en los últimos años, tanto para aficionados a la informática como para profesionales e investigadores. Actualmente, Python es el estándar en el desarrollo de machine learning e inteligencia artificial y al mismo tiempo es accesible a curiosos interesados en proyectos caseros.

Numerosas bibliotecas especializadas como PIL y OpenCV dotan a Python de la capacidad de leer y trabajar con imágenes. Desde una sencilla adición de marcas de agua hasta algoritmos complejos de reconocimiento facial, Python permite programar y automatizar toda clase de rutinas de análisis y manipulación de imágenes digitales.

PIL (Python Imaging Library) es una biblioteca gratuita que implementa de manera sencilla todas las funciones típicas de cualquier software de edición de imágenes. La mala noticia es que fue desarrollada para Python 2.x y poco después se dejó de actualizar… la buena es que existe PILLOW, una biblioteca extendida basada en PIL y totalmente funcional en versiones de Python 3.x.

Lo primero es instalar PILLOW e importar el módulo Image en nuestro IDE de Python preferido. Image es el módulo principal de PIL y contiene las funciones básicas para tratar imágenes. Para empezar a trastear con las posibilidades de PIL, vamos a utilizar un bodegón del pintor danés William Hammer (Wikimedia commons) y una fotografía de una polilla tropical tomada por un servidor. Lo más cómodo es alojar los archivos en el  mismo directorio que nuestro script de Python. Cargar imágenes es tan sencillo como usar la función open() del módulo Image y asignar el resultado a una variable. PIL guarda las imágenes como objetos Image para trabajar con ellas.  El método show() abre un visor externo y nos muestra la imagen.

from PIL import Image  # El módulo Image contiene las funciones que necesitamos

# cargamos las imágenes

bodegon = Image.open('bodegon.jpg')
polilla = Image.open('polilla.jpg')

# echamos un vistazo con el método .show()

bodegon.show()
polilla.show()

Características de la imagen y metadatos

Lo primero que hacemos, como en cualquier rutina de manipulación de datos, es explorar la materia prima.  Las dimensiones están contenidas en el atributo size de la variable, pero también se puede acceder por separado a la anchura (width) y la altura (height).  Los atributos mode y format devuelven el modo y el formato de la imagen respectivamente. El modo por defecto para imágenes JPG a color es RGB de 3×8 bits, pero el módulo Image admite también blanco y negro, escala de grises, CMYK, HSV y LAB.

# exploramos polilla

print('Datos de polilla.jpg')
print('dimensiones: ', polilla.size)
print('altura: ', polilla.height)
print('anchura: ', polilla.width)
print('formato: ', polilla.format)
print('modo: ', polilla.mode)

Si nos interesa explorar los metadatos, el método _getexif() nos devuelve un diccionario un tanto críptico con todos los datos exif que haya podido extraer de nuestra imagen. Echando un vistazo los metadatos de “polilla”, podemos identificar rápidamente la marca y el modelo de la cámara utilizada, así como el software de revelado. Sin embargo, como ocurre con muchas imágenes extraídas de internet, “bodegon” carece de metadatos.

print('Metadatos de polilla: ', polilla._getexif()) # devuelve un diccionario con los metadatos de la imagen
print('Metadatos de bodegon: ', bodegon._getexif()) # devuelve 'None' porque la imágen original no contiene metadatos.

Exploración de la imagen

Una vez observadas las características de las imágenes, pasamos a explorar su contenido. Quienes estén familiarizados con la imagen digital sabrán que cada píxel en una imagen en escala de grises tiene una de 256 tonalidades posibles (8 bits). Estas tonalidades están representadas por números enteros donde el 0 corresponde al negro y el 255 al blanco. De la misma manera, las imágenes RGB representan los colores con tres valores similares que indican la cantidad de rojo, verde y azul que forman el color en cuestión.

Con el método histogram(), podemos elaborar el famoso histograma, tan valorado por los fotógrafos… pero cuidado, que tiene truco.

Al ejecutar histogram() en una imagen en escala de grises, Python nos devuelve una lista de 256 números, que corresponden a la cantidad de píxeles que tienen la tonalidad correspondiente. Cuando la imagen es RGB, la liste contiene 768 valores: primero los 256 del canal rojo, luego los del verde y por último los del azul. Para visualizar estos datos en condiciones, tenemos que importar un paquete gráfico como matplotlib y representar los valores bien ordenados en un gráfico de barras. Podemos escribir una función personalizada para esto y aplicarla luego a las imágenes que queramos.

# Definición de una función para visualizar el histograma.

import matplotlib.pyplot as plt  # importamos el paquete gráfico matplotlib con su alias convencional plt

def ver_histograma(img):
    """Esta función muestra un histograma a partir de un obejeto de imagen del módulo PIL.Image haciendo uso del gráfico
    de barras de matplotlib.pyplot"""

    x = range(256)  # serie de enteros que representan las 256 tonalidades posibles y que usaremos como eje x
    h = img.histogram()  # valores del histogram

    if len(h) == 256:  # para imágenes en blanco y negro o escala de grises
        plt.bar(x, h, width = 1)
        plt.show()

    elif len(h) == 768:  # para imágenes a color
        r = h[:256]
        g = h[256:512]
        b = h[512:] # separamos los 768 valores en 3 grupo de 256, una por cada color primari

        plt.bar(x, r, color=(1, 0, 0, 1/3), width = 1)
        plt.bar(x, g, color=(0, 1, 0, 1/3), width = 1)
        plt.bar(x, b, color=(0, 0, 1, 1/3), width = 1) #graficamos cada banda en el color correspondiente con 1/3 de opacidad

        plt.show()

    else:
        return print('imagen incorrecta')  # para imágenes en un formato no admitido


ver_histograma(polilla)  # a ver qué tal han quedado 
ver histograma(bodegon)

Aquí tenemos el resultado de aplicar nuestra función de histograma a “polilla”. En cada canal de color hay un pico muy marcado que corresponde al color ocre liso predominante en el fondo:

A lo que vinimos: edición de imágenes

PIL ofrece una serie de métodos y funciones que ejecutan de manera muy sencilla las tareas típicas de cualquier rutina de manipulación de imágenes. Para redimensionar, rotar o recortar las imágenes no hay más que aplicar el método correspondiente especificando los parámetros necesarios. El método convert() nos permite cambiar el modo de nuestra imagen.

Cabe apuntar que estos métodos no modifican la imagen original, sino que crean una copia a la que se aplica la modificación. Esta copia puede reasignarse a la variable original o a una nueva.

# Tareas comunes de edición

polilla_chica = polilla.resize((100,100)) # redimensiona la imagen a 100 x 100 píxeles
polilla_ancha = polilla.resize((polilla.width*2, polilla.height)) # duplica la anchura, manteniendo la altura
polilla_rotacion = polilla.rotate(45) # gira la imagen 45 grados en sentido antihorario (se mantienen las dimensiones)
polilla_recorte = polilla.crop((400, 250, 500, 350)) # recorta la imagen en el rectángulo especificado
polilla_bn = polilla.convert('L') # El modo L transforma la imagen a escala de grises


ver_histograma(polilla_bn) # podemos aprovechar para probar nuestra función en una imagen en blanco y negro

Interacciones entre imágenes

Otra serie de funciones muy útiles que presenta el módulo Image son las que sirven para combinar imágenes entre sí.

Introduciendo a la función blend() dos imágenes y un índice de transparencia entre 0 y 1, obtenemos una superposición de la segunda imagen sobre la primera con la transparencia indicada. En este caso es imprescindible que ambas imágenes sean del mismo tamaño. Si las imágenes ya poseían transparencia, la función alpha_composite() hace lo mismo que blend() pero respetando los índices de transparencia originales.

# Combinamos imágenes con blend()

polilla2 = polilla.resize(bodegon.size)  # ajustamos polilla al tamaño de bodegon
mezcla = Image.blend(polilla2, bodegon, 0.5) # superpone las imágenes con un 50% de transparencia
mezcla.show()

Para colocar simplemente una imagen sobre otra, podemos utilizar el método paste() en la imagen inferior. Este método requiere una imagen y las coordenadas donde debe ser insertada (esquina superior izquierda). Si en su lugar indicamos un color RGB y las coordenadas de un rectángulo, paste() rellenará dicho rectángulo de ese color sobre la imagen. ¡Cuidado! Esta acción sí modifica la imagen permanentemente, así que es buena idea hacer trabajar sobre una copia, que hacemos con el método copy().

# pegamos bodegon sobre polilla

polilla3 = polilla.copy() # hacemos una copia de polilla para no modificar el original
polilla3.paste(bodegon, (100,100)) # la esquina superior izquierda de bodegon estará en el punto (100,100)
polilla3.show()

polilla4 = polilla.copy()
polilla4.paste((255, 255, 0), (400, 250, 500, 350)) # colocamos un cuadrado amarillo sobre la imagen
polilla4.show()

Para finalizar

Una vez completada la edición, podemos guardar cualquier objeto de imagen de PIL como un archivo en nuestro directorio de trabajo, especificando el nombre que queramos al método save(). El archivo debe llevar también la extensión correspondiente a algún formato admitido por PIL (entre los que están todos los tradicionales: JPG, PNG, TIFF, GIF y PDF)

# A mí me ha gustado la imagen mezclada, así que guardo esa

mezcla.save('mezcla.jpg') # ¡ojo al poner nombres, los archivos se sobreescriben sin previo aviso!

Si te has quedado con ganas de más, en la documentación oficial de PILLOW puedes encontrar toda la información sobre las posibilidades que ofrece el módulo PIL para la manipulación de imágenes.