%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['figure.dpi'] = 120
from scipy import fftpack

def color2bw(img):
    return np.dot(img, [0.299, 0.587, 0.114])

6. Filtrado de imágenes

En esta lección veremos:

  • Filtrado con máscaras en frecuencia

  • Filtrado con convolución 2D

6.1. Filtro en frecuencia

Recordemos que

Una multiplicación en el espacio de frecuencia equivale a una convolución en el espacio original

Podemos usar esta propiedad para hacer filtrado

  • Calculamos el espectro de la imagen usando la DFT

  • Multiplicamos el espectro por una ventana

  • Aplicamos DFT inversa para obtener la imagen filtrada

Ejemplo: Filtro pasa-bajos

Sea una imagen de ejemplo.

Usamos scipy.fftpack.fft2 para calcular su transformada de Fourier

img_example = color2bw(plt.imread("../images/InsInformatica.jpg"))  
S_img = fftpack.fft2(img_example)

fig, ax = plt.subplots(1, 2, figsize=(8, 3.5), tight_layout=True);
ax[0].imshow(img_example, cmap=plt.cm.Greys_r)
ax[0].set_title('Imagen de ejemplo')
ax[1].imshow(fftpack.fftshift(np.log(1+np.abs(S_img))), cmap=plt.cm.Spectral_r)
ax[1].set_title('Espectro de amplitud');
../../_images/06_filtrado_de_imágenes_3_0.png

Multipliquemos el espectro por una ventana rectangular que multiplique por cero las frecuencias altas

Eliminar las frecuencias altas suaviza la imagen. Esto puede usarse para eliminar ruido a costa de perder detalles

Luego tomamos la transformada de Fourier inversa del espectro multiplicado

Observe como cambia el resultado a medida que disminuimos progresivamente el tamaño de la ventana (sigma)

def create_mask(img_shape, sigma):
    cy, cx = img_shape[0]/2, img_shape[1]/2
    x = np.arange(0, img_shape[1]); 
    y = np.arange(0, img_shape[0]);
    X, Y = np.meshgrid(x, y)
    mask = np.zeros_like(S_img, dtype=np.float32)
    mask[int(cy-sigma):int(cy+sigma), 
         int(cx-sigma):int(cx+sigma)] = 1
    return mask

fig, ax = plt.subplots(3, 2, figsize=(8, 8), tight_layout=True, sharex=True, sharey=True);
for i, sigma in enumerate([100, 40, 10]):
    espectro_filtrado = fftpack.fftshift(S_img)*create_mask(img_example.shape, sigma)
    ax[i, 0].imshow(np.log(1+np.abs(espectro_filtrado)), cmap=plt.cm.Spectral_r)
    image_reconstruida = np.real(fftpack.ifft2(fftpack.ifftshift(espectro_filtrado)))
    ax[i, 1].imshow(image_reconstruida, cmap=plt.cm.Greys_r)
../../_images/06_filtrado_de_imágenes_5_0.png

Para evitar artefactos causados por los bordes abruptos de la señal rectangular podemos reemplazar la misma por una ventana Gaussiana

Esta operación se suele llamar “suavizado gaussiano”

def create_mask(img_shape, sigma):
    cy, cx = img_shape[0]/2, img_shape[1]/2
    x = np.arange(0, img_shape[1]); 
    y = np.arange(0, img_shape[0]);
    X, Y = np.meshgrid(x, y)    
    return 1e-8 + np.exp(-(((X-cx)/sigma)**2 + ((Y-cy)/sigma)**2))

fig, ax = plt.subplots(3, 2, figsize=(8, 8), tight_layout=True, sharex=True, sharey=True);
for i, sigma in enumerate([100, 40, 10]):
    espectro_filtrado = fftpack.fftshift(S_img)*create_mask(img_example.shape, sigma)
    ax[i, 0].imshow(np.log(1+np.abs(espectro_filtrado)), cmap=plt.cm.Spectral_r)
    image_reconstruida = np.real(fftpack.ifft2(fftpack.ifftshift(espectro_filtrado)))
    ax[i, 1].imshow(image_reconstruida, cmap=plt.cm.Greys_r)
../../_images/06_filtrado_de_imágenes_7_0.png

Ejemplo: Filtro pasa-altos

Si usamos el inverso de una ventana gaussiana entonces estaríamos multiplicando por cero las frecuencias más bajas

Esto borra los colores, dejándo sólo los detalles o bordes

Observe como cambia el resultado a medida que borramos más frecuencias bajas

def create_mask(img_shape, sigma):
    cy, cx = img_shape[0]/2, img_shape[1]/2
    x = np.arange(0, img_shape[1]); 
    y = np.arange(0, img_shape[0]);
    X, Y = np.meshgrid(x, y)    
    return 1.0  - np.exp(-(((X-cx)/sigma)**2 + ((Y-cy)/sigma)**2)) 

fig, ax = plt.subplots(3, 2, figsize=(8, 8), tight_layout=True, sharex=True, sharey=True);
for i, sigma in enumerate([10, 40, 100]):
    espectro_filtrado = fftpack.fftshift(S_img)*create_mask(img_example.shape, sigma)
    ax[i, 0].imshow(np.log(1+np.abs(espectro_filtrado)), cmap=plt.cm.Spectral_r)
    image_reconstruida = np.real(fftpack.ifft2(fftpack.ifftshift(espectro_filtrado)))
    ax[i, 1].imshow(image_reconstruida, cmap=plt.cm.Greys_r)
../../_images/06_filtrado_de_imágenes_9_0.png

6.2. Filtrado en el dominio de la imagen

También podemos filtrar una imagen en su espacio original usando la convolución bidimensional

El elemento que se convoluciona con la imagen se denomina filtro o kernel de convolución

La siguiente animación muestra el proceso de convolución en dos dimensioens

../../_images/filter2D_convolution.gif

¿Qué cree usted que hacen estos los siguientes kernels/filtros?

\[\begin{split} \begin{pmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ \end{pmatrix} \frac{1}{25} \qquad \begin{pmatrix} 0.018 & 0.082 & 0.1353 & 0.082 & 0.018 \\ 0.082 & 0.3678 & 0.6065 & 0.3678 & 0.082 \\ 0.1353 & 0.6065 & 1 & 0.6065 & 0.1353 \\ 0.082 & 0.3678 & 0.6065 & 0.3678 & 0.082 \\ 0.018 & 0.082 & 0.1353 & 0.082 & 0.018 \\ \end{pmatrix} \frac{1}{\sqrt{2\pi}} \end{split}\]

Respuesta: Ambos son filtros promediadores, es decir que reemplazan cada pixel por un promedio de sus vecinos. Estos filtros actuan como pasa-bajos ya que suavizan la imagen.

Scipy ofrece dos funciones para hacer convolución

  • convolve2d: Convolución tradicional, es más rápido cuando la imagen y el filtro son pequeños

  • fftconvolve: Convolución multiplicando en frecuencia, es más rápido cuando la imagen y el filtro son grandes

from scipy.signal import fftconvolve, convolve2d

fig, ax = plt.subplots(1, 2, figsize=(8, 3.5), tight_layout=True);

size = 20
kernel = np.ones(shape=(size, size))/size**2
img_filtered1 = fftconvolve(img_example, kernel, mode='same');
img_filtered2 = convolve2d(img_example, kernel, mode='same');
ax[0].set_title('fftconvolve')
ax[0].imshow(img_filtered1, cmap=plt.cm.Greys_r)
ax[1].set_title('convolve2d')
ax[1].imshow(img_filtered2, cmap=plt.cm.Greys_r);
../../_images/06_filtrado_de_imágenes_13_0.png

Ejemplo: Detección de borde con filtro Sobel

Los siguientes filtros se conocen como sobel horizontal y vertical

../../_images/filtro_gradient.gif

Se utilizan para resaltar los bordes de la imagen

fig = plt.figure(figsize=(7, 7), tight_layout=True)
sobelx = fftconvolve(img_example, [[-1, 0, 1], 
                                   [-2, 0, 2], 
                                   [-1, 0, 1]], mode='full')
ax =  plt.subplot2grid((2, 2), (0, 0))
ax.matshow(sobelx, cmap=plt.cm.Greys_r); 
ax.axis('off')
ax.set_title('Sobel vertical')
sobely = fftconvolve(img_example, [[-1, -2, -1], 
                                   [0, 0, 0], 
                                   [1, 2, 1]], mode='full')
ax = plt.subplot2grid((2, 2), (0, 1))
ax.matshow(sobely, cmap=plt.cm.Greys_r)
ax.axis('off')
ax.set_title('Sobel horisontal')
ax = plt.subplot2grid((2, 2), (1, 0), colspan=2)
ax.matshow(np.sqrt(sobely**2 + sobelx**2)[3:-3,3:-3], cmap=plt.cm.Greys_r)
ax.axis('off')
ax.set_title('Combinación');
../../_images/06_filtrado_de_imágenes_15_0.png

6.3. Ruido en imágenes

Existen distintos tipos de ruido que pueden afectar una imagen

  • Ruido térmico, ruido de lectura, ruido eléctronico: Se modelan tipicamente como ruido blanco Gaussiano

  • Ruido sal y pimienta o Ruido impulsivo: Se traduce en píxeles que se saturan en sus valores máximos/mínimos

  • Interferencia periódica: Puede modelarse como una sinusoide

A continuación mostraremos como se ven estos ruidos para aprender a reconocerlos

También veremos como disminuir sus efectos usando filtros

Considere la siguiente imagen de ejemplo:

from ipywidgets import interact, FloatSlider, IntSlider, FloatLogSlider

img_example = color2bw(plt.imread('../images/lobo.jpg')) 
fig, ax = plt.subplots(figsize=(6, 3.5), tight_layout=True)
ax.imshow(img_example, cmap=plt.cm.Greys_r)
ax.axis('off');
../../_images/06_filtrado_de_imágenes_17_0.png

Así se vería si la corrompemos añadiendo ruido blanco gaussiano con una desviación estándar de \(20\)

fig, ax = plt.subplots(figsize=(6, 3.5), tight_layout=True)

sigma = 20

noise = np.random.randn(img_example.shape[0], img_example.shape[1])
img_corrupted = img_example + sigma*noise
ax.matshow(img_corrupted, cmap=plt.cm.Greys_r)
ax.axis('off');
../../_images/06_filtrado_de_imágenes_19_0.png

El ruido blanco se presenta en todo el espectro

Por ende no es posible eliminarlo completamente pero podemos disminuir su efecto usando un filtro pasabajo

Para llegar a un buen resultado se debe calibrar el tamaño del filtro. En este caso particular un filtro rectangular de tamaño \(5\) entrega un resultado aceptable

fig, ax = plt.subplots(figsize=(6, 3.5), tight_layout=True)

size = 5
kernel = np.ones(shape=(size, size))/size**2
img_filtered = fftconvolve(img_corrupted, kernel, mode='same')
ax.matshow(img_filtered, cmap=plt.cm.Greys_r)
ax.axis('off');
../../_images/06_filtrado_de_imágenes_21_0.png

Ejemplo: Eliminar ruido impulsivo

El ruido impulso o ruido sal y pimienta aparece como pixeles saturados en la imagen

Un filtro tipicamente usando para disminuir este ruido es el filtro mediana. Si se aplica la mediana en una vecindad los valores extremos serán ignorados

from scipy.signal import medfilt

noise = np.random.rand(*img_example.shape)
img_corrupted  = np.where(noise < 0.1, 255, img_example)
img_filtered = medfilt(img_corrupted, 5)

fig, ax = plt.subplots(1, 2, figsize=(6, 3.5), tight_layout=True, sharex=True, sharey=True)
ax[0].matshow(img_corrupted[:, 100:600], cmap=plt.cm.Greys_r)     
ax[0].set_title('Imagen con ruido impulsivo')
ax[0].axis('off')
ax[1].matshow(img_filtered[:, 100:600], cmap=plt.cm.Greys_r)
ax[1].set_title('Limpieza con filtro mediana')
ax[1].axis('off');
../../_images/06_filtrado_de_imágenes_23_0.png

Ejemplo: Eliminar ruido periódico

Es un ruido que tiene frecuencia y dirección

En este ejemplo el ruido tiene una dirección vertical

fig, ax = plt.subplots(figsize=(6, 3.5), tight_layout=True)
X, Y = np.meshgrid(np.arange(0, img_example.shape[1]), np.arange(0, img_example.shape[0]))

img_corrupted = img_example + 200*np.cos(2.0*np.pi*40*Y/img_example.shape[0])
ax.matshow(img_corrupted, cmap=plt.cm.Greys_r) 
ax.axis('off');
../../_images/06_filtrado_de_imágenes_25_0.png

El espectro de la imagen anterior (enventanada) es:

win = np.dot(np.hanning(img_corrupted.shape[0]).reshape(-1, 1), 
             np.hanning(img_corrupted.shape[1]).reshape(1, -1))

fig, ax = plt.subplots(figsize=(6, 3.5), tight_layout=True)
S_img = fftpack.fftshift(fftpack.fft2(img_corrupted*win))   
ax.matshow(np.log(np.absolute(S_img)+1), cmap=plt.cm.Spectral_r) 
ax.axis('off');
../../_images/06_filtrado_de_imágenes_27_0.png

Note los dos puntos oscuros que aparecen en la vertical a una misma distancia del centro

Recordemos que estos impulsos en frecuencia corresponen a una sinusoide en la imagen

El ruido periodico se traduce como un punto en el espectro

Idea

Podríamos intentar “borrar” los componentes frecuenciales asociados al ruido usando una mascara multiplicativa correctamente posicionada

X, Y = np.meshgrid(np.arange(0, img_example.shape[1]), 
                   np.arange(0, img_example.shape[0]))
freq_x = fftpack.fftshift(fftpack.fftfreq(n=img_example.shape[1]))
freq_y = fftpack.fftshift(fftpack.fftfreq(n=img_example.shape[0]))

def create_mask(dims, frequency, size=10):
    freq_int = int(frequency*dims[0])
    mask = np.ones(shape=(dims[0], dims[1]))
    mask[dims[0]//2-size-freq_int:dims[0]//2+size-freq_int, 
         dims[1]//2-size:dims[1]//2+size] = 0 
    mask[dims[0]//2-size+freq_int:dims[0]//2+size+freq_int, 
          dims[1]//2-size:dims[1]//2+size] = 0
    return mask


S_img = fftpack.fftshift(fftpack.fft2(img_corrupted))
espectro_filtrado = S_img*create_mask(S_img.shape, 0.06)   
# Reconstrucción
img_reconstructed = np.real(fftpack.ifft2(fftpack.ifftshift(espectro_filtrado)))

fig, ax = plt.subplots(1, 3, figsize=(8, 6), tight_layout=True)
ax[0].matshow(img_corrupted[:, 200:500], cmap=plt.cm.Greys_r);
ax[1].imshow(np.log(1+np.abs(espectro_filtrado))[100:-100, 400:-400], cmap=plt.cm.Spectral_r,
             extent=(freq_x[400], freq_x[-400], freq_y[100], freq_y[-100]))
ax[2].matshow(img_reconstructed[:, 200:500], cmap=plt.cm.Greys_r);
../../_images/06_filtrado_de_imágenes_29_0.png

Algunas causas de degradación de calidad en imágenes:

  • Manipulación: Desenfoque

  • Ambiente: Reflejos y dispersión de luz

  • Dispositivo: Ruido del sensor y circuitos

  • Ruido de cuantización