%matplotlib inline
import numpy as np
from matplotlib import rcParams
rcParams['figure.dpi'] = 120
import matplotlib.pyplot as plt
from scipy import fftpack
from IPython.display import YouTubeVideo
from functools import partial
YTVideo_formato = partial(YouTubeVideo, width=640, height=400, rel=0, modestbranding=1)

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

5. Análisis de imágenes en frecuencia

En esta lección veremos:

  • Transformada de Fourier bidimensional

  • Espectro de imágenes sintéticas y naturales

5.1. Transformada de Fourier bidimensional

La DFT se puede aplicar a funciones multi-dimensionales

En el caso discreto de una señal bidimensional \(g[n_1, n_2]\) con índices \(n_1 \in [0, N_1-1]\) y \(n_2 \in [0, N_2-1]\) tenemos

\[ G[k_1, k_2] = \sum_{n_1=0}^{N_1-1} \sum_{n_2=0}^{N_2-1} g[n_1, n_2] \exp \left ( -j2\pi \left[\frac{n_1 k_1}{N_1} + \frac{n_2 k_2}{N_2} \right] \right) \]

y su inversa

\[ g[n_1, n_2] = \frac{1}{N_1 N_2}\sum_{k_1=0}^{N_1-1} \sum_{k_2=0}^{N_2-1} G[k_1, k_2] \exp \left ( j2\pi \left[\frac{n_1 k_1}{N_1} + \frac{n_2 k_2}{N_2} \right] \right) \]

Notemos que

\[\begin{split} \begin{align} G[k_1, k_2] &= \sum_{n_1=0}^{N_1-1} \left(\sum_{n_2=0}^{N_2-1} g[n_1, n_2] \exp \left (-j2\pi \frac{n_2 k_2}{N_2}\right) \right) \exp \left (-j2\pi \frac{n_1 k_1}{N_1}\right) \\ &= \sum_{n_1=0}^{N_1-1} \hat g_{n_2}[n_1] \exp \left (-j2\pi \frac{n_1 k_1}{N_1}\right), \end{align} \end{split}\]

Es decir que la DFT 2D se puede calcular usando repetidas veces la DFT de una dimensión.

Ejemplo: Visualización de la base de Fourier bidimensional

A continuación se muestra la base de Fourier (parte real), son cosenos en dos dimensiones con distintas orientaciones

  • La frecuencia cero se ubica en la esquina superior izquierda

  • La frecuencia crece de izquierda a derecha y de arriba para abajo

Cuando aplicamos la transformada de Fourier 2D a una imagen estamos separando la imagen en base a estas “texturas” sinusoidales

x = np.arange(0, 32, step=1)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots(9, 9, figsize=(6, 6), tight_layout=True)

for n in range(9):
    for m in range(9):
        ax[n, m].matshow(np.cos(2.0*np.pi*X*m/len(x) + 2.0*np.pi*Y*n/len(x)), 
                         cmap=plt.cm.RdBu_r, vmin=-1, vmax=1)
        ax[n, m].axis('off')
    
../../_images/05_análisis_de_imágenes_4_0.png

5.2. Espectro de una imagen

Podemos usar la transformada de Fourier 2D para obtener el espectro de amplitud de una imagen

Los ejes de la DFT son las frecuencias espaciales

img_color = plt.imread('../images/valdivia.jpg')
img_bw = color2bw(img_color)[:, 300:]

fig, ax = plt.subplots(1, 2, figsize=(6, 3), tight_layout=True)
ax[0].imshow(img_bw, cmap=plt.cm.Greys_r) 
ax[0].set_title("Imagen")
S = fftpack.fft2(img_bw)
ax[1].imshow(fftpack.fftshift(np.abs(S)), cmap=plt.cm.Spectral_r,
             extent=[-img_bw.shape[1]//2, img_bw.shape[1]//2, 
                     -img_bw.shape[0]//2, img_bw.shape[0]//2])
ax[1].set_title("Espectro de amplitud");
../../_images/05_análisis_de_imágenes_6_0.png

Nota

La energía está muy concentrada en el componente central. Esto es común en imágenes naturales

Para visualizar mejor el espectro de una imagen natural se recomienda usar

\[ \log(|\text{fft2}(I)|+1) \]

de esta forma el componente central no es tan dominante

fig, ax = plt.subplots(1, 2, figsize=(6, 3), tight_layout=True)
ax[0].imshow(img_bw, cmap=plt.cm.Greys_r) 
ax[0].set_title("Imagen")
S = fftpack.fft2(img_bw)
ax[1].imshow(fftpack.fftshift(np.log(1+np.abs(S))), cmap=plt.cm.Spectral_r,
             extent=[-img_bw.shape[1]//2, img_bw.shape[1]//2, 
                     -img_bw.shape[0]//2, img_bw.shape[0]//2])
ax[1].set_title("Espectro de amplitud");
../../_images/05_análisis_de_imágenes_8_0.png

Ahora podemos apreciar otras estructuras en el espectro, pero

¿A qué corresponden?

Para entrenarnos en la interpretación del espectro conviene estudiar espectros de imágenes sintéticas

5.3. Espectro de una imagen sintética

Ejemplo: Impulsos en frecuencia

Dos impulsos en el espectro corresponde a una sinusoide en el espacio original y viceversa

La posición de los impulsos está asociada a la frecuencia de la sinusoide

YTVideo_formato('JkYpoMWGbN4')

Ejemplo: Espectro de una Linea

Una línea en la imagen es una línea en el espectro (con otra orientación)

YTVideo_formato('vwey-8_EADg')

Pregunta: ¿Cómo se explica esto?

Consideremos primero el caso donde la rotación es 40 pixeles (linea vertical)

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

def draw_line_spectrum(ax, angle):
    img = np.zeros(shape=(80, 80));     
    for i in range(80):
        img[i, int(i - 2*angle*i/80 + angle)] = 1 

    ax[1].set_title("Espectro de magnitud");  
    ax[0].set_title("Imagen")
    S_img = np.abs(fftpack.fft2(img))
    im = ax[1].imshow(fftpack.fftshift(S_img), cmap=plt.cm.Greys_r, 
                      extent=[-40, 40, 40, -40])
    im = ax[0].imshow(img, cmap=plt.cm.Greys_r);
    
draw_line_spectrum(ax, 40)
../../_images/05_análisis_de_imágenes_15_0.png

Notemos que en este caso la componente vertical y horizontal pueden independizarse como muestra el sencillo ejemplo a continuación

A = np.array([[0., 0., 1., 0., 0.]])
B = np.array([[1., 1., 1., 1., 1.]])
A*B.T
array([[0., 0., 1., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 1., 0., 0.]])

Esto se conoce como señal separable

Propiedad:

La DFT 2D de una señal separable equivale a la multiplicación de las dos DFT 1D

Demostración: Una señal es sepable si

\[ g[n_1, n_2] = g_1[n_1] g_2[n_2], \]

aplicando esto en la DFT tenemos

\[\begin{split} \begin{align} G[k_1, k_2] &= \sum_{n_1=0}^{N_1-1} \sum_{n_2=0}^{N_2-1} g_1[n_1] g_2[n_2] \exp \left ( -j2\pi \left[\frac{n_1 k_1}{N_1} + \frac{n_2 k_2}{N_2} \right] \right) \nonumber \\ & = \sum_{n_1=0}^{N_1-1} g_1[n_1] \exp \left ( -j2\pi \frac{n_1 k_1}{N_1} \right) \sum_{n_2=0}^{N_2-1} g_2[n_2] \exp \left ( -j2\pi\frac{n_2 k_2}{N_2} \right) \nonumber \\ \end{align} \end{split}\]

es decir que se obtiene la multiplicación de cada DFT.

Usemos esta propiedad en el ejemplo anterior.

La primera señal es un impulso en el origen

Su transformada de Fourier es una señal constante

SA = fftpack.fftshift(np.abs(fftpack.fft(A)))
display(A, SA)
array([[0., 0., 1., 0., 0.]])
array([[1., 1., 1., 1., 1.]])

La segunda señal es una constante

Su transformada de Fourier es un impulso en el origen

(la constante y el impulso en el origen son un par de Fourier)

SB = fftpack.fftshift(np.abs(fftpack.fft(B)))
display(B, SB)
array([[1., 1., 1., 1., 1.]])
array([[0., 0., 5., 0., 0.]])

Resultado: La multiplicación de ambas es una linea rotada en 90 grados con respecto a la original

SA*SB.T
array([[0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.],
       [5., 5., 5., 5., 5.],
       [0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0.]])

Pregunta:

¿Por qué pareciera repetirse la linea en el espectro cuando la rotación es distinta de 0 pixeles (0 grados), 40 pixeles (90 grados) o 80 pixeles (180 grados)?

fig, ax = plt.subplots(1, 2, figsize=(8, 4), tight_layout=True);    
draw_line_spectrum(ax, 50)
../../_images/05_análisis_de_imágenes_26_0.png

Este efecto o artefacto es obedece a la siguiente propiedad de la DFT

Propiedad:

La DFT es periodica

En este caso el artefacto que observamos se debe a que en ciertos ángulos los bordes no calzan, tal como muestra la siguiente figura

fig, ax = plt.subplots(figsize=(8, 4), tight_layout=True); 

img = np.zeros(shape=(80, 80));     
for i in range(80):
    img[i, int(i - 2*50*i/80 + 50)] = 1 

S_img = fftpack.fftshift(np.abs(fftpack.fft2(img)))
ax.imshow(np.tile(S_img, (3,3)), plt.cm.Greys_r)
ax.axhline(80, c='r', ls='--')
ax.axhline(160, c='r', ls='--')
ax.axvline(80, c='r', ls='--')
ax.axvline(160, c='r', ls='--')
<matplotlib.lines.Line2D at 0x7efc0e44d040>
../../_images/05_análisis_de_imágenes_28_1.png

Más adelante veremos como disminuir la influencia de este artefacto usando enventanado

Ejemplo: Espectro de un rectangulo

Un rectangulo en la imagen es un sinc en el espectro (y viceverza)

Notemos como mientras más ancho es el rectangulo más angosto es el sinc (y viceversa)

Demostración para una dimensión: https://www.thefouriertransform.com/pairs/box.php

YTVideo_formato('4ctVzvU89lw')

Ejemplo: Espectro de una Gaussiana

Una Gaussiana en el espectro es una Gaussiana en el espacio de la imagen

Sin embargo el ancho de la gaussiana en el espectro es inversamente proporcional a la del espacio de la imagen

Demostración para una dimensión: https://www.thefouriertransform.com/pairs/gaussian.php

YTVideo_formato('2Sukn_-4Wss')

Ejemplo: Espectro de ruido blanco

El espectro de una imagen de ruido blanco es también ruido blanco

El adjetivo “blanco” se refiere a que es un ruido sin color, es decir que aparece de igual forma en todas las frecuencias de la señal

El video a continuación muestre imágenes de ruido blanco gaussiano generado con distitnas semillas y sus correspondientes espectros de magnitud

YTVideo_formato("mUTdCJzxLkc")

5.4. Espectro de una imagen natural

Ahora que hemos aprendido a interpretar el los espectros, analizemos una imagen natural

img_doge = color2bw(plt.imread('../images/doge.jpg'))
fig, ax = plt.subplots(1, 2, figsize=(8, 4), tight_layout=True)

im = ax[0].imshow(img_doge, cmap=plt.cm.Greys_r)
S_img = fftpack.fft2(img_doge)
im = ax[1].imshow(fftpack.fftshift(np.log(np.abs(S_img)+1)), cmap=plt.cm.Spectral_r)
../../_images/05_análisis_de_imágenes_37_0.png

Nota

Las fuerte componentes horizontal y vertical son en realidad un artefacto de borde

Puedes disminuir este efecto usando enventanado

Esto consiste en suavizar los bordes de la imagen mediante la multiplicación por una función de ventana

En este ejemplo se usa una ventana de Hanning

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

fig, ax = plt.subplots(1, 2, figsize=(8, 4), tight_layout=True)
im = ax[0].imshow(img_doge*win, cmap=plt.cm.Greys_r)
S_img = fftpack.fft2(img_doge*win)
im = ax[1].imshow(fftpack.fftshift(np.log(np.abs(S_img)+1)), cmap=plt.cm.Spectral_r)
../../_images/05_análisis_de_imágenes_40_0.png

5.5. Espectro de magnitud y de fase

La DFT en dos dimensiones \(G[k_1, k_2]\) es un número complejo y como tal puede escribirse en coordenadas polares como

\[ G[k_1, k_2] = M[k_1, k_2] e^{j \Phi[k_1, k_2]} \]

donde \(j\) es el número imaginario.

Hasta ahora solo hemos estudiando el valor absoluto \(M = | G |\), esto es lo que se conoce como espectro de magnitud

La magnitud espectral guarda información de la amplitud de los componentes

Ahora estudiaremos la influencia que tiene \(\Phi[k_1, k_2]\), es decir el espectro de fase o ángulo

La fase espectral guarda información del desfase (dirección) de los componentes

Consideremos nuevamente nuestra imagen de ejemplo

plt.figure(figsize=(6, 4), tight_layout=True)
plt.imshow(img_doge, cmap=plt.cm.Greys_r);
../../_images/05_análisis_de_imágenes_42_0.png

¿Cómo se ven los espectros de magnitud y fase de esta imagen?

fig, ax = plt.subplots(1, 2, figsize=(8, 4), tight_layout=True)
S_img = fftpack.fft2(img_doge*win)
im = ax[0].imshow(fftpack.fftshift(np.log(1.+np.abs(S_img))), cmap=plt.cm.Spectral_r)
ax[0].set_title('Espectro de magnitud')
fig.colorbar(im, ax=ax[0], orientation='horizontal')
ax[1].set_title('Espectro de Fase')
im = ax[1].imshow(fftpack.fftshift(np.angle(S_img)), cmap=plt.cm.Spectral_r)  
fig.colorbar(im, ax=ax[1], orientation='horizontal');
../../_images/05_análisis_de_imágenes_44_0.png

¿Podemos reconstruir la imagen usando sólo el espectro de magnitud? ¿O usando sólo el de fase?

Con este sencillo experimento podemos resaltar que es lo que guarda cada espectro

def hist_eq(img, nbins=256):
    image_hist, bins = np.histogram(img.flatten(), nbins, density=True)
    cdf = image_hist.cumsum() 
    cdf = 255 * cdf / cdf[-1] 
    image_eq = np.interp(img.flatten(), bins[:-1], cdf)
    return image_eq.reshape(img.shape).astype('int')
    #return img

reconstruct = lambda S: np.real(fftpack.ifft2(S))

fig, ax = plt.subplots(2, 2, figsize=(8, 6), tight_layout=True)
for ax_ in ax.ravel():
    ax_.axis('off')

ax[0, 0].imshow(img_doge, cmap=plt.cm.Greys_r);
S_dog = fftpack.fft2(img_doge)
ax[0, 1].hist(img_doge.ravel(), alpha=0.5, bins=100); 
ax[0, 1].hist(hist_eq(img_doge.ravel()), alpha=0.5, bins=100);ax[0, 1].axis('on')
ax[1, 0].set_title('Usando solo magnitud')
ax[1, 0].imshow(hist_eq(reconstruct(np.abs(S_dog))), cmap=plt.cm.Greys_r);
ax[1, 1].set_title('Usando solo fase')
ax[1, 1].imshow(hist_eq(reconstruct(np.exp(1j*np.angle(S_dog, deg=False)))), cmap=plt.cm.Greys_r);
../../_images/05_análisis_de_imágenes_46_0.png

La base de Fourier representan las texturas presentes en la imagen

  • El espectro de amplitud nos dice la importancia de cada textura

  • El espectro de fase nos dice la dirección de cada textura

Si solo usamos la importancia sin la dirección el resultado es ininteligible

Si solo usamos sólo la dirección podemos entender los contornos pero se pierden la información de intensidad de color (importancia)

Pregunta:

¿Qué pasa si intercambiamos la fase y magnitud de dos imágenes de igual tamaño?

img_inst = color2bw(plt.imread("../images/InsInformatica.jpg"))  
fig, ax = plt.subplots(2, 2, figsize=(6, 5), tight_layout=True)
for ax_ in ax.ravel():
    ax_.axis('off')

ax[0, 0].imshow(img_doge, cmap=plt.cm.Greys_r);
ax[0, 1].imshow(img_inst, cmap=plt.cm.Greys_r); 
S_inf = fftpack.fft2(img_inst)
rec_doge = fftpack.ifft2(np.abs(S_dog)*np.exp(1j*np.angle(S_inf, deg=False)))
rec_inst = fftpack.ifft2(np.abs(S_inf)*np.exp(1j*np.angle(S_dog, deg=False)))
ax[1, 0].set_title('Amplitud doge\nFase instituto')
ax[1, 0].imshow(np.real(rec_doge), cmap=plt.cm.Greys_r); 
ax[1, 1].set_title('Amplitud instituto\nFase doge')
ax[1, 1].imshow(np.real(rec_inst), cmap=plt.cm.Greys_r); 
../../_images/05_análisis_de_imágenes_49_0.png