Introducción al procesamiento digital de imágenes con Python

Contenidos

Imágenes digitales

Podemos definir una imagen como una función bidimensional $f(x_1,x_2)$ donde $x=(x_1,x_2)$ son las coordenadas espaciales, y el valor de $f$ en cualquier $x$ es la intensidad de la imagen en dicho punto.

Desde este punto de vista, una imagen puede considerarse como una función continua definida sobre un conjunto continuo (imagen analógica) o como una función discreta definida sobre un dominio discreto (imagen digital). Ambos puntos de vista resultan útiles en el procesamiento de imágenes.

Convertir una imagen analógica a digital requiere que tanto las coordenadas como la intensidad sean digitalizadas. Digitalizar las coordenadas se llama muestrear, mientras que digitalizar la intensidad se denomina cuantizar. Entonces, cuando todas las cantidades son discretas, llamamos a la imagen una imagen digital.

El camino opuesto, de digital a analógico, es también posible y se denomina interpolación.

Coordenadas

El resultado de muestrear y cuantizar es una matriz de números. El tamaño de la imagen es el número de filas por el número de columnas, $M\times N$. La indexación de la imagen en Python sigue la convención habitual

$$\left(\begin{array}{cccc}a(0,0) & a(0,1) & \cdots & a(0,N-1) \\a(1,0) & a(1,1) & \cdots & a(1,N-1) \\\cdots & \cdots & \cdots & \cdots \\ a(M-1,0) & a(M-1,1) & \cdots & a(M-1,N-1)\end{array}\right)$$

Módulos de Python

In [2]:
from __future__ import division   # impone aritmética no entera en la división
from PIL import Image             # funciones para cargar y manipular imágenes
import numpy as np                # funciones numéricas (arrays, matrices, etc.)
import matplotlib.pyplot as plt   # funciones para representación gráfica

Esta línea configura Matplotlib para que muestre figuras en el cuaderno IPython en lugar de en una ventana nueva. No hay que usarla en los scripts de Python.

In [3]:
 %matplotlib inline

Lectura, visualización y escritura de imágenes con el módulo Image

Python soporta los formatos de imagen más habituales. Cargemos la image lena.jpg La sintaxis de lectura es

In [4]:
I = Image.open("lena.jpg")

El tipo de dato habitual para una imagen es uint8, es decir, un entero sin signo representado en 8 bits. Esto nos da $2^8=256$ valores que se distribuyen en el rango de $[0,255]$ para cada pixel.

La variable, I, no es una matriz, sino un objeto. Podemos visualizarla y realizar algunas operaciones estándar con ella. Por ejemplo, podemos visualizar la imagen con el programa correspondiente del sistema

In [5]:
I.show()

Para ver la imagen con ipython,

In [6]:
plt.imshow(np.asarray(I))
plt.show()

Podemos obtener información sobre la imagen, en este caso el tamaño, tipo (escala de grises, RGB, etc.), y formato:

In [7]:
print I.size, I.mode, I.format
(512, 512) RGB JPEG

Podemos convertirla a otro formato, en este caso a una imagen de escala de grises, que son con las que trabajaremos en este curso:

In [8]:
I1 = I.convert('L') # convierte a escala de grises 
I1.show()
print I1.size, I1.mode, I1.format
(512, 512) L None
In [9]:
plt.imshow(np.asarray(I1), cmap='gray')
plt.show()

O grabar una imagen al disco:

In [10]:
I1.save('lena_gris.tif')

Tipos de imágenes y conversiones

Existen tres tipos principales de imágenes:

  • La imagen de intensidad es una matriz de datos cuyos valores han sido escalados para que representen intensidades de una escala de grises. Los elementos de una imagen de intensidad son de clase uint8 (enteros almacenados en 8 bits) o de clase uint16 (enteros almacenados en 16 bits) y pueden almacenar, respectivamente, $2^8=256$ valores en el rango $[0,255]$, o $2^{16}=65536$ valores en el rango $[0,65535]$. Cuando la imagen es de clase float32, los valores son números en punto flotante (que se almacenan en 32 bits). En este último caso, los valores suelen tomarse en el rango $[0,1]$ o en el rango $[0,255]$, indistintamente.
  • La imagen binaria es una imagen en blanco y negro. Cada pixel tiene asignado un valor lógico de 0 ó 1.
  • La imagen en color es como la imagen de intensidad pero tiene tres canales, es decir, a cada pixel le corresponden tres valores de intensidad (RGB) en lugar de uno.

Cuando realizamos transformaciones matemáticas de imágenes, normalmente necesitamos que la imagen sea de tipo float. Pero cuando la leemos y almacenamos ahorramos espacio usando codificación entera sin signo. Podemos usar las órdenes siguientes:

In [11]:
a = np.asarray(I1,dtype=np.float32)

convierte el objeto I1 en una matriz de tipo float32.

In [12]:
Image.fromarray(a.astype(np.uint8)).save("prueba.jpg")

primero convierte la matriz a a tipo uint8, y luego a un objeto "imagen".

Una vez que tenemos definida la imagen como una matriz con elementos float, podemos comenzar a trabajar con ella.

Ejemplo

Vamos a

  • seleccionar una parte de la imagen, mediante la restricción de los índices de la misma,
  • crear un plot,
  • guardar el resultado.

Comenzamos seleccinando una parte de la imagen:

In [13]:
ojo_de_Lena = a[251:283,317:349]

Seguimos con la creación de los plots

In [14]:
plt.subplot(121)
plt.imshow(a,cmap='gray',interpolation='nearest')
plt.title('Lena'),plt.axis('off') 

plt.subplot(122)
plt.imshow(ojo_de_Lena,cmap='gray',interpolation='nearest')
plt.title('El ojo derecho de Lena'),plt.axis('off')

plt.show()

Y terminamos guardando la imagen

In [15]:
Image.fromarray(ojo_de_Lena.astype(np.uint8)).save("OjoLena.jpg")

Ejercicios


Ejercicio 1

Escribir una función con

  • Entrada: una imagen de cualquier tipo y el rango para los píxeles $(x,y)$ a extraer.

  • Salida: una matriz de datos float32 correspondiente a los índices indicados de la imagen original y una figura de ella.

Aplicar la función para extraer la cabeza del cameraman de la imagen cameraman.tif.

In [16]:
%run Ejercicio1.py

Ejercicio 2

Las máscaras son filtros geométricos de una imagen. Por ejemplo, si queremos seleccionar una región de una imagen, podemos hacerlo multiplicando la matriz de la imagen original por una matriz de igual tamaño que contenga unos en la región que queremos conservar y ceros en el resto. En este ejercicio seleccionaremos una región circular de la imagen lena_gray_512.tif de radio 150. Seguir los pasos siguientes:

  • Leer la imagen y convertirla a float.
  • Crear una matriz de la misma dimensión rellena de ceros.
  • Modificar esta matriz de forma que contenga unos en un círculo de radio $150$, es decir, si $(i−c_y)^2+(j−c_x)^2 < 150^2$, con $(c_x,c_y)$ como centro (píxel arriba, píxel abajo) de la imagen.
  • Multiplicar la imagen por la máscara.
  • Mostrar el resultado.

Cuando se multiplica por cero, se convierten a negro los píxeles de fuera del círculo. Modifica el programa para hacer visible esos píxeles con la mitad de su intensidad.

In [17]:
%run Ejercicio2.py

Ejercicio 3

El degradado lineal es un efecto en el que se oscurece una imagen desde una parte de la misma hasta la parte opuesta alterando la intensidad original de un modo proporcional. Por ejemplo, en la degradación vertical, podemos implementar este filtro mediante una máscara que sea constante por columnas pero tome un valor decreciente por filas, desde 1 en la primera fila a cero en la última.

Construir dicha matriz y crear el degradado de la imagen de Lena. Visualizar la imagen original y la filtrada.

Nota: un modo de resolverlo es usando bucles y condicionales. Pero vectorizar ahorra tiempo de ejecución: con el comando numpy.linspace puede definirse la degradación, y mediante numpy.tile puede construirse, repitiendo el vector obtenido con numpy.linspace, la matriz máscara.

In [18]:
%run Ejercicio3.py

Ejercicio 4

Construir, como array numpy, la imagen de un tablero de ajedrez, donde cada casilla tiene un tamaño de $250 \times 250$ pixels. Mostrar el resultado en el terminal iPython. Se puede usar la orden numpy.tile.

In [19]:
%run Ejercicio4.py

Ejercicio 5

Construir, como array numpy, la imagen de círculos concéntricos mostrada debajo. La imagen tiene un tamaño de $500 \times 500$ pixels. Cada circunferencia tiene una anchura aproximada de $4$ ó $5$ pixels. Mostrar el resultado en el terminal iPython.

In [20]:
%run Ejercicio5.py

Ejercicio 6

Construir, como array numpy, la imagen de la servilleta mostrada debajo. Cada casilla tiene un tamaño de $10 \times 10$ pixels. Mostrar el resultado en el terminal iPython. Se puede usar la orden numpy.tile.

In [21]:
%run Ejercicio6.py

Ejercicio 7

Construir, como array numpy, la imagen mostrada debajo. La imagen tiene un tamaño de $500 \times 500$ pixels. Cada círculo tiene un radio de $10$ pixels y los centros de los círculos están separados $50$ píxeles. Mostrar el resultado en el terminal iPython.

In [22]:
%run Ejercicio7.py

Referencias