Sea $f(x,y)$ una imagen $M\times N$, con $x=0,1,2,\ldots,M-1$ (columnas) y $y=0,1,2,\ldots,N-1$ (filas).
La Transformada Discreta de Fourier (DFT en inglés) de $f$, que llamaremos $F(m,n)$, viene dada por
$$ F(m,n)= \frac{1}{MN}\sum_{x=0}^{M-1}\sum_{y=0}^{N-1} f(x,y) \exp(-2\pi i(\frac{x}{M}m+\frac{y}{N}n)),$$
con $m=0,1,2,\ldots,M-1$ y $n=0,1,2,\ldots,N-1$.
La región rectangular $M\times N$ definida para $ (m,n)$ se llama dominio de frecuencias, y los valores de $F(m,n)$ se llaman coeficientes de Fourier.
La Transformada Inversa Discreta de Fourier viene dada por
$$ f(x,y)=MN\sum_{m=0}^{M-1}\sum_{n=0}^{N-1} F(m,n) \exp(2\pi i(\frac{m}{M}x+\frac{n}{N}y)),$$
con $x=0,1,2,\ldots,M-1$ y $y=0,1,2,\ldots,N-1$.
Nota. Hay otras formas de normalizar la DFT y su inversa por medio de los factores $1/MN$ y $MN$. El que hemos escogido es el que se usa habitualmente en Python.
Incluso si $f(x,y)$ es real, su transformada $F(m,n)$ es, en general, una función compleja. Para visualizarla utilizaremos el espectro de potencias,
$$P(m,n)= F(m,n) \bar F(m,n),$$donde $\bar F$ es el conjugado del complejo $F$.
Propiedad: Si la imagen es periódica, es decir, si toma los mismos valores en los bordes superiores en inferiores y en los bordes derecho e izquierdo, entonce, la DFT es periódica también y el espectro de potencia es simétrico respecto del origen: $$ P(m,n)=P(-m,-n). $$
Si la imagen no es periódica, entonces $F$ puede tener discontinuidades. Esto se conoce como el fenómeno de Gibbs.
Gracias a la propiedad mencionada, podemos representar la DFT en un cuadrado centrado en $ (0,0) $, lo cual es más conveniente para su visualización.
La DFT y su inversa se obtienen en la práctica usando el algoritmo de la Transformada Rápida de Fourier (Fast Fourier Transform, FFT). Las funciones correspondientes de Numpy para la transformada directa y la inversa son fft2
y ifft2
.
Para calcular el espectro de potencia usamos la función Numpy abs
que nos da el módulo del complejo. Para mover el origen de la transformada al centro del rectángulo de frecuencias usamos fftshift
. Finalmente, si queremos ver mejor el resultado, usaremos una escala logarÃtmica.
Ejemplo
from __future__ import division # hace que se utilice la división en punto flotante
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
from numpy.fft import fft2, fftshift, ifft2
Muestra los gráficos insertados.
%matplotlib inline
Empezamos creando una imagen periódica de tamaño $(601,1201)$. El periodo, $10.5$, aparece en la dirección horizontal. En vertical, la imagen es constante:
hW, hH = 600, 300
hFrec = 10.5
# Crea una malla en el cuadrado de dimensiones [0,1)x[0,1)
x = np.linspace( 0, 2*hW/(2*hW +1), 2*hW+1) # columnas (Anchura)
y = np.linspace( 0, 2*hH/(2*hH +1), 2*hH+1) # filas (Altura)
[X,Y] = np.meshgrid(x,y)
A = np.sin(hFrec*2*np.pi*X)
plt.imshow(A, extent=[0,1,0,1], cmap ='gray');
H,W = np.shape(A) # Dimensiones de la imagen A
Si entendemos que estamos representando la función $A = f(X,Y)$ como una superficie, un corte por un plano paralelo al plano $OXZ$ de dicha superficie serÃa
xx = np.linspace(0,1,1200)
plt.plot(xx,np.sin(hFrec*2*np.pi*xx))
plt.show()
que es una función periódica de frecuencia $f = 10.5$ Hz y periodo $T=\dfrac{1}{f}$
print "T = ", 1./hFrec
El paso siguiente es calcular la DFT centrada en el origen y mostrar la figura del espectro de potencia (su raÃz cuadrada).
F = fft2(A)/(W*H)
F = fftshift(F)
P = np.abs(F)
plt.imshow(P, extent = [-hW,hW,-hH,hH]);
Hacemos zoom (nos acercamos) para ver mejor los valores altos de $P$ a las frecuenccias $\pm hFreq$
plt.imshow(P[hH-25:hH+25,hW-25:hW+25], extent=[-25,25,-25,25]);
Si cortamos la superficie representada por un plano vertical que contenga el eje $OX$ tenemos
plt.plot(range(-25,25),P[hH,hW-25:hW+25])
plt.show()
Otros ejemplos: Con la malla anterior, creamos otras imágenes.
hFrec = 10.5 # Frecuencia horizontal
vFrec = 20.5 # Frecuencia vertical
A1 = np.sin(hFrec*2*np.pi*X) + np.sin(vFrec*2*np.pi*Y)
plt.figure()
plt.imshow(A1, cmap = 'gray', extent=[0,1,0,1]);
Si cortamos esta superficie por el plano $y=0.5$ tenemos la curva
xx = np.linspace(0,1,1200)
yy = 0.5*np.ones(1200)
plt.plot(xx,np.sin(hFrec*2*np.pi*xx) + np.sin(vFrec*2*np.pi*yy))
plt.show()
y si cortamos por el plano $x =0.5$
yy = np.linspace(0,1,1200)
xx = 0.5*np.ones(1200)
plt.plot(yy,np.sin(hFrec*2*np.pi*xx) + np.sin(vFrec*2*np.pi*yy))
plt.show()
F1 = fft2(A1)/(W*H)
F1 = fftshift(F1)
P1 = np.abs(F1)
plt.figure()
plt.imshow(P1[hH-25:hH+25,hW-25:hW+25], extent=[-25,25,-25,25]);
Si cortamos la superficie con un plano perpendicular al plano $OXY$ que contiene al eje $OX$
plt.plot(range(-25,25),P1[hH,hW-25:hW+25])
plt.show()
Y con un plano que contiene a $OY$
plt.plot(range(-25,25),P1[hH-25:hH+25,hW])
plt.show()
hFrec = 10.5 # Frecuencia horizontal
vFrec = 20.5 # Frecuencia vertical
A2 = np.sin(hFrec*2*np.pi*X + vFrec*2*np.pi*Y)
plt.figure()
plt.imshow(A2, cmap = 'gray',extent=[0,1,0,1]);
F2 = fft2(A2)/(W*H)
F2 = fftshift(F2)
P2 = np.abs(F2)
r = 25
plt.figure()
plt.imshow(P2[hH-r:hH+r,hW-r:hW+r], extent=[-r,r,-r,r]);
Finalmente vamos a usar una imagen real, con una frecuencia alta a lo largo del eje horizontal, lo que se puede ver porque el máximo relativo se alÃnea con el eje horizontal:
I = Image.open("canes.jpg")
I = I.convert('L') # 'L' para convertir a escala de grises
A3 = np.asarray(I, dtype = np.float32) # Convertimos la imagen I en
# un array Numpy A3 con elementos tipo float32
H,W = np.shape(A3)
plt.imshow(A3, cmap = 'gray');
F3 = fft2(A3)/float(W*H)
F3 = fftshift(F3)
P3 = np.abs(F3)
r = 100
mW = int(np.fix(0.5*W)) # Entero que vale, aproximadamente, la mitad de W
mH = int(np.fix(0.5*H)) # Entero que vale, aproximadamente, la mitad de H
plt.figure()
plt.imshow(np.log(1+P3[mH-r:mH+r,mW-r:mW+r]), extent=[-r,r,-r,r]);
Una imagen se recupera a partir de los coeficientes $F$ usando la Transformada Inversa de Fourier. Como hemos visto, en general, no todos los coeficientes tienen el mismo valor. Para usar sólo los más importantes, podemos seleccionar aquellos cuyo módulo sea mayor que uno dado llamado umbral $T$. Es decir, hacemos cero todos los coeficientes no contenidos en el conjunto siguiente:
$$ S_T=\left\{0\leq m\leq M-1, 0\leq n \leq N-1 : \left| F(m,n)\right| \geq T\right\} .$$
Llamemos $g(T)$ al número de elementos del array $(m,n)$, contenidos en $S_T$. Por una parte, para $T=0$ tenemos $g(0)= MN$, porque $\left| F(m,n)\right| $ son no negativos. Por otra parte, para $T> \max \left| F(m,n)\right| $ tenemos $g(T)=0$. Observar que $g$ es una función de $T$ que es decreciente. Construiremos un plot de $g$ usando la función count_nonzero
, que da el número de elementos no nulos de una matriz.
puntos = 100
Trango = np.linspace(0.05,1,puntos)
g = np.zeros(puntos, dtype = 'float32')
n = 0
for Tcont in Trango:
g[n] = np.count_nonzero(P3 >= Tcont)
n += 1
plt.plot(Trango,g);
plt.xlabel('T (valor del umbral de corte)',fontsize=14)
plt.title('Elementos no nulos en la matriz P3',fontsize=14);
Ahora aplicamos el método del valor umbral, es decir, mandamos a cero los coeficientes menores que un $T$ dado. ¿Cómo escogemos $T$?
Tomemos, por ejemplo, $T=0.1$ y veamos el resultado:
T = 0.1
c = F3 * (P3 >= T)
fM = ifft2(c)*W*H
plt.imshow(np.abs(fM), cmap = 'gray');
Finalmente, calcularemos el ratio entre el número de coeficientes de la Transformada de Fourier distintos de cero y aquellos que superan el umbral contenidos en la matriz $c$. Asà obtenemos una relación de compresión:
out1 = np.count_nonzero(F3)
out2 = np.count_nonzero(c)
print 'Número inicial de elementos distintos de cero = ',out1
print 'Número final de elementos distintos de cero = ',out2
print 'Relación de compresión = ',out1/out2
Ejercicio
Tenemos una imagen de tamaño $W\times H$, y queremos comprimirla de acuerdo con un ratio dado $r$ manteniendo los coeficientes de la transformada de Fourier más significativos, es decir, queremos encontrar el umbral, $T$, tal que el ratio de compresión sea $r$.
Dicho de otra manera, queremos calcular $T$ de forma que se cumpla que $ \displaystyle\frac{g(T)}{WH}=r $.
Para conseguirlo, tenemos que calcular la raÃz de la función $$h(T)=g(T) -rWH.$$
Para escribir el programa compresion.py
:
np.count_nonzero
. Para hacerse una idea de cómo es $h$, dibujar esta función para el intervalo $[minF,0.6]$. fM
como hicimos en este apartado. Dibujar las imágenes original y comprimida.out1/out2
).%run compresion.py
En esta aplicación, tenemos un vehÃculo que queremos orientar de forma que siga lÃneas verticales.
Primero, tomamos la imagen original $A$ y la rotamos:
A4 = (A - A.min())/(A.max() - A.min()) # normalizamos a [0,1]
I = Image.fromarray(np.uint8(A4*255)) # convertimos a imagen PIL
rot_angulo = -65
B = I.rotate(rot_angulo, expand = True) # rotamos sin recortar
B4 = np.asarray(B,dtype=np.float32)
sY,sX = np.shape(B4)
plt.figure()
plt.imshow(B,cmap = 'gray');
F4 = fft2(B4)/(sX*sY)
F4 = fftshift(F4)
P4 = np.abs(F4)
cX = int(np.fix(sX/2)) # punto medio de la imagen
cY = int(np.fix(sY/2))
plt.figure()
plt.imshow(P4[cY-25:cY+25,cX-25:cX+25], extent=[-25,25,-25,25]);
Como hay una región grande de valor constante (negro), el valor del espectro de potencia en el origen es alto, haciendo que no resalten los valores de las frecuencias importantes. Eliminamos este efecto restando el valor medio de la imagen.
F4 = fft2(B4-B4.mean())/(sX*sY)
F4 = fftshift(F4)
P4 = np.abs(F4)
plt.figure()
plt.imshow(P4[cY-25:cY+25,cX-25:cX+25], extent=[-25,25,-25,25]);
Calculamos las coordenadas de los coeficientes de más alta frecuencia y calculamos el ángulo
indices = np.where(P4 == np.max(P4))
maxY = (indices[0][0]-cY)/sY
maxX = (indices[1][0]-cX)/sX
alpha=np.arctan(maxY/maxX)*180/np.pi
print "alpha = ", alpha
Ahora rotamos y recuperamos la imagen original
C=B.rotate(alpha)
plt.figure()
plt.imshow(C,cmap = 'gray');
Ejercicio
Crear una función que tenga como:
Aplicarlo a las siguientes imágenes:
%run angulo.py
I5 = Image.open("surcos1.jpg")
alpha5, C5, P5 = angulo(I5)
print alpha5
plt.figure()
plt.imshow(I5);
plt.figure()
plt.imshow(C5);
plt.figure()
plt.imshow(P5);
En este ejemplo, el algoritmo funcionó bien. Fijarse en que los valores altos de P5
están lejos del origen.
Sin embargo, en el ejemplo siguiente, el algoritmo no funciona. ¿Por qué? ¿Cómo podrÃa arreglarse?
I6 = Image.open("surcos2.jpg")
alpha6, C6, P6 = angulo(I6)
print alpha6
plt.figure()
plt.imshow(I6);
plt.figure()
plt.imshow(C6);
plt.figure()
plt.imshow(P6);