Análisis de Señales
Contenido
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib import rcParams
rcParams['figure.dpi'] = 120
from IPython.display import HTML
3. Análisis de Señales¶
En esta lección veremos
Covarianza, correlación y convolución
Transformada de Fourier discreta
Transformada rápida de Fourier
3.1. Notación¶
Función de tiempo continuo: \(g(t)\)
Función de tiempo discreto: \(g[n] = g(t_n)\) con \(t_n = n T_s, n\in [0, N]\)
Tiempo o intervalo de muestreo: \(T_s\)
En la siguiente figura se muestra una señal de tiempo continuo (linea gris) muestreada a tiempo regular (flechas rojas)
3.2. Comparando señales en el tiempo¶
Covarianza y correlación cruzada
Podemos analizar la similitud entre dos señales \(g(\cdot)\) y \(f(\cdot)\) en función de un retardo \(\tau\) usando el la covarianza cruzada
y para procesos discretos con retardo \(m\):
donde la media de la señal se estima como \( \bar g = \frac{1}{N} \sum_{n=1}^N g[n]\).
Si las señales tienen distinta escala es conveniente usar la correlación cruzada
donde la desviación estándar de la señal se estima como \(\hat \sigma_g = \frac{1}{N} \sqrt{\sum_{n=1}^N (g[n] - \bar g)^2}\)
Nota
Podemos calcular la covarianza entre dos señales usando la función np.correlate
de NumPy
Ejemplo
Usando la correlación para encontrar el desfase o desplazamiento temporal relativo entre dos señales
x = np.linspace(0.0, 5.0, num=100)
y1 = np.exp(-4.0*(x-1.5)**2) + 0.1*np.random.randn(len(x))
y2 = np.exp(-4.0*(x-2.5)**2) + 0.1*np.random.randn(len(x))
tau = np.linspace(-x[-1], x[-1], 2*len(x)-1)
crosscorr = np.correlate(y1, y2, mode='full')/(len(x)*np.std(y1)*np.std(y2))
fig, ax = plt.subplots(1, 2, figsize=(8, 2.5), tight_layout=True)
ax[0].plot(x, y1, label='y1')
ax[0].plot(x, y2, label='y2')
ax[0].legend()
ax[1].plot(tau, crosscorr);
idx_max = np.argmax(crosscorr)
display(f"y1 está separado por {tau[idx_max]:0.4f} segundos de y2")
ax[1].axvline(tau[idx_max], c='r', ls='--');
'y1 está separado por -1.0101 segundos de y2'
Autocorrelación
También es de interés comparar una señal consigo misma usando la autocorrelación
Esto nos permite analizar en que momento \(m\) la señal se parece más a si misma: periodicidad
Ejemplo
El período de la señal corresponde al segundo máximo de la autocorrelación
x = np.linspace(0.0, 10.0, num=100)
y = np.cos(2.0*np.pi*1*x) + 0.5*np.cos(2.0*np.pi*2*x) + 0.5*np.random.randn(len(x))
tau = np.linspace(0, x[-1], len(x))
autocorr = np.correlate(y, y, mode='full')[len(x)-1:]/(len(x)*np.var(y))
fig, ax = plt.subplots(1, 2, figsize=(8, 2.5), tight_layout=True)
ax[0].plot(x, y)
ax[1].plot(tau, autocorr)
ax[1].axvline(tau[1+np.argmax(autocorr[1:])], c='r', ls='--')
display(f"El periodo (frecuencia fundamental de la señal es {tau[1+np.argmax(autocorr[1:])]:0.4f} segundos")
'El periodo (frecuencia fundamental de la señal es 1.0101 segundos'
Convolución
La operación de producto punto entre versiones desplazadas de funciones se llama convolución y se denota con \(*\)
La convolución discreta se define como
las funciónes de covarianza y correlación se pueden escribir como convoluciones
Nota
Podemos realizar la convolución usando la función np.convolve
de NumPy
Ejemplo
Convolución entre una señal cuadrada (naranja) y una Gaussiana (azul)
El resultado es una versión con bordes suavizados de la señal cuadrada (derecha)
fig, ax = plt.subplots(1, 2, figsize=(8, 2.5), tight_layout=True)
t = np.arange(-4, 4, step=1e-2)
def gaussian(t, a=0, s=0.2):
return np.exp(-np.absolute(t-a)**2/s**2)
def square(t, a=0, T=1):
s = np.zeros(shape=(len(t),))
s[np.absolute(t-a) < T] = 1
return s
conv_s = np.convolve(gaussian(t), square(t), mode='same')
ax[0].plot(t, gaussian(t))
ax[0].plot(t, square(t))
ax[1].plot(t, conv_s);
La siguiente animación muestra una representación gráfica de la convolución
El resultado de la convolución (derecha) es la integración del area que se marca en celeste en la figura de la izquierda
%%capture
fig, ax = plt.subplots(1, 2, figsize=(8, 2.5), tight_layout=True)
def update(a = 0):
ax[0].cla(); ax[1].cla()
p1, p2 = gaussian(t, 0.1*a - 4), square(t)
ax[0].plot(t, p2); ax[0].plot(t, p1);
ax[0].fill_between(t, 0, p1*p2, alpha=0.5)
ax[1].plot(t, conv_s[::-1]); ax[1].scatter(0.1*a -4, np.sum(p1*p2), s=100, c='k')
return ()
anim = animation.FuncAnimation(fig, update, frames=80, interval=100, blit=True)
HTML(anim.to_html5_video())
3.3. Análisis en frecuencia: Transformada de Fourier¶
Una herramienta de gran relevancia para el análisis de señales es la Transformada de Fourier
La transformada de Fourier de una señal \(g(t)\) se define como
donde
Usamos el operador \(\mathcal{F}\) para denotar la transformada
Tipicamente se usa \(G\) para la transformación de \(g\)
\(G\) es ahora una función de la frecuencia
Usando la transformada de Fourier podemos analizar las frecuencias de oscilación predominantes de una señal
def matrix_DFT(signal):
N = len(signal)
W_N = np.exp(-1j*2*np.pi/N)
n = np.arange(N)
Omega = W_N**(n*n.reshape(1,-1).T)
S = np.dot(Omega, signal)
return np.dot(Omega, signal)
Fs = 20
t = np.arange(0.0, 10.0, step=1/Fs)
g = np.cos(2.0*np.pi*1*t) + np.cos(2.0*np.pi*1.5*t) + 1.0*np.random.randn(len(t))
f = np.linspace(0, Fs/2, num=len(t)//2+1)
Gabs = np.abs(matrix_DFT(g))
fig, ax = plt.subplots(1, 2, figsize=(8, 2.5), tight_layout=True)
ax[0].plot(t, g)
ax[0].set_xlabel("Tiempo [s]")
ax[1].plot(f, Gabs[:len(f)])
ax[1].set_xlabel("Frecuencia [Hz]");
display(f"Las frecuencias predominantes son: {f[Gabs[:len(f)] > 50]} [Hz]")
'Las frecuencias predominantes son: [1. 1.5] [Hz]'
Representación en coordenadas polares
Es usual descomponer la transformada de Fourier en el espectro de amplitud y el espectro de fase
Donde
\(|G(f)|\) es el espectro de amplitud
\(\varphi(f)\) es el espectro de fase (ángulo)
Propiedad: La transformada de Fourier es invertible
Propiedad: La convolución en el tiempo se convierte en multiplicación en frecuencia y viceverza (Lathi & Ding, 2009, Sec 3.3.6)
Propiedad: La transformada de Fourier es un operador lineal
Teorema: (de) Parseval:
La energía de una señal se preserva. La transformada de Fourier no pierde información. (Lathi & Ding, 2009, Sec 3.7.1)
Teorema: (de) Wiener-Khinchin:
La transformada de Fourier de la autocorrelación es la densidad espectral de potencia y viceverza (Lathi & Ding, 2009, 3.7.5)
La densidad espectral es la potencia asignada a cada frecuencia de la señal
3.3.1. Transformada de Fourier discreta (DFT)¶
Para computar la transformada de Fourier sobre señales muestreadas (discretas) usamos la DFT
El resultado es una transformada de Fourier definida en un conjunto discreto de frecuencias
Sea una señal discreta \(g[n]\) con \(n \in [0, N-1]\) y tiempo de muestreo \(T_s\), es decir \(t[n] = n T_s\)
La DFT se define como
donde
\(f[k] = k f_0 ~~ \forall k \in [-\frac{(N-1)}{2}, \frac{(N-1)}{2}]\)
\(f_0 = \frac{F_s}{N}\)
\(F_s = \frac{1}{T_s}\)
\(t[n] = n T_s\)
La DFT es también invertible
Notar que N puntos en el tiempo se mapean con N frecuencias independientes
Para tener una notación más clara definiremos
Luego podemos escribrir la DFT como
Podemos escribir el sistema de ecuaciones
en forma matricial como
que corresponde a \(N\) tiempos definidos como
y a \(N\) frecuencias definidas como
En el ejemplo anterior implementamos la DFT como una multiplicación matricial usando Numpy
Multiplicación entre una matriz de NxN y un vector de Nx1
N sumas de N multiplicaciones, complejidad cuadrática
def matrix_DFT(signal):
N = len(signal)
W_N = np.exp(-1j*2*np.pi/N)
n = np.arange(N)
Omega = W_N**(n*n.reshape(1,-1).T)
S = np.dot(Omega, signal)
return S
3.3.2. Transformada rápida de Fourier o FFT¶
La computación de la DFT tiene complejidad \(\mathcal{O}(N^2)\)
Nota
Existen varios algoritmos exactos con complejidad \(\mathcal{O}(N\log N)\) para calcular la DFT: La Fast Fourier Transform (FFT)
El algoritmo de FFT más conocido es el de Cooley-Tukey, en donde se obtiene una FFT recursiva que explota las periodicidades en la matriz \(\Omega\)
Notar que se calculan dos “medias” DFT
Para continuar usaremos la siguiente propiedad de la exponencial compleja
Además por periodicidad/simetría de la DFT
Finalmente
En resumen
Para obtener \(G[k]\) y \(G[k + N/2]\):
Calculamos \(G_E[k]\) y \( \exp \left( -j2\pi \frac{k}{N} \right) G_O[k]\)
Sumamos o restamos los términos, respectivamente
Es decir que reducimos los cálculos necesarios a la mitad
Ejemplo
Sea una señal aleatoria con dias muestras
Calculemos y comparemos la DFT de los pares (puntos naranjos) e impares (cruces azules)
t = np.linspace(0, 10, num=10)
x = np.random.randn(len(t))
plt.figure(figsize=(6, 2), tight_layout=True)
plt.plot(t, x, c='k', alpha=0.5)
plt.scatter(t[::2], x[::2], marker='x', zorder=100)
plt.scatter(t[1::2], x[1::2], marker='o', zorder=100);
La función np.close
retorna True
si todos los elementos de los arreglos tienen una diferencia menor a 1e-8
np.set_printoptions(precision=3)
N = len(x)
W_N = np.exp(-1j*2*np.pi/N)
S = matrix_DFT(x)
Se = matrix_DFT(x[0::2]) # Transformada de Fourier de los pares
So = matrix_DFT(x[1::2]) # Transformada de Fourier de los impares
display(S[:N//2], Se + W_N**np.arange(N//2)*So)
display(np.allclose(S[:N//2], Se + W_N**np.arange(N//2)*So))
display(S[N//2:], Se - W_N**np.arange(N//2)*So)
display(np.allclose(S[N//2:], Se - W_N**np.arange(N//2)*So))
array([-0.964+0.j , -1.52 -4.137j, 0.653-1.628j, -2.13 -3.092j,
-0.658+0.476j])
array([-0.964+0.j , -1.52 -4.137j, 0.653-1.628j, -2.13 -3.092j,
-0.658+0.476j])
True
array([ 4.277+5.022e-16j, -0.658-4.760e-01j, -2.13 +3.092e+00j,
0.653+1.628e+00j, -1.52 +4.137e+00j])
array([ 4.277+0.j , -0.658-0.476j, -2.13 +3.092j, 0.653+1.628j,
-1.52 +4.137j])
True
El algoritmo FFT continua dividiendo en pares hasta llegar a \(N\log N\) productos, como muestra la siguiente figura
3.4. Implementaciones de la FFT¶
Python: numpy.fft y scipy.fft
C: The Fastest Fourier Transform in the WEst (FFTW)
OpenCV: Enfocado en señales bidimensionales (lo veremos más adelante)
En lo que sigue usaremos:
Funciones principales
Para obtener la transformada de Fourier usando el modulo fft
de scipy usamos
fft.fft
yfft.ifft
: Calcula la transformada y la transformada inversa (caso general)fft.rfft
yfft.irfft
: Calcula la transformada y la transformada inversa para señales con valores realesfft.fftfreq
yfft.rfftfreq
: Crea una arreglo de frecuencias consistente con las funciones anteriores
import scipy.fft
Ejemplo: Comparación entre implementación matricial usando NumPy y FFT de scipy
Primero creamos una serie de tiempo con frecuencia de muestreo \(F_s\)
Fs = 20
t = np.arange(0.0, 10.0, step=1/Fs)
g = np.cos(2.0*np.pi*1*t) + np.cos(2.0*np.pi*1.5*t) + 1.0*np.random.randn(len(t))
fig, ax = plt.subplots(figsize=(8, 2.5), tight_layout=True)
ax.plot(t, g);
Para calcular la FFT usamos
G = scipy.fft.rfft(g)
display(G[:10])
array([ 4.658 +0.j , 11.17 -6.519j, -16.456-26.884j, 6.762 -7.148j,
-9.299-12.139j, 11.051 -1.596j, -12.692 +2.41j , -4.455 -5.758j,
-8.252 -2.95j , 5.264 +6.864j])
La FFT es imaginaria, podemos recuperar
el espectro de magnitud usando la función
np.abs
el espectro de fase usando
np.angle
Para generar el vector de frecuencias asociado a la FFT usamos
freq = scipy.fft.rfftfreq(n=len(g), d=1/Fs)
display(freq[:10])
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
Finalmente dibujamos los espectros de magnitud
fig, ax = plt.subplots(figsize=(8, 2.5), tight_layout=True)
ax.plot(freq, np.abs(matrix_DFT(g))[:len(freq)], label='Numpy matmul', alpha=.75, lw=2)
ax.plot(freq,
np.abs(scipy.fft.rfft(g)), label='fftpack', alpha=.75, lw=2, ls='dashed')
ax.set_xlabel("Frecuencia [Hz]")
plt.legend(loc=1);
y comparamos los tiempos de cómputo
¿Cuántos órdenes de magnitud más rápido es la FFT?
%timeit -n 10 np.abs(matrix_DFT(g))
%timeit -n 10 np.abs(scipy.fft.fft(g))
36.7 ms ± 4.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
57.1 µs ± 24.7 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)