Filtros Digitales con Matrices: Algoritmos y Ejemplos en Python
Introducción
Los filtros digitales son la columna vertebral del procesamiento de señales (DSP). En su forma más elemental, un filtro FIR (Finite Impulse Response) se implementa mediante una convolución discreta entre la señal de entrada y un conjunto de coeficientes (el kernel del filtro). Cuando trabajamos con matrices podemos expresar tanto la señal como el kernel como vectores y aprovechar operaciones lineales optimizadas (BLAS, SIMD, etc.) que ofrecen librerías como NumPy o SciPy.
Conceptos básicos
- Señal discreta: vector
x[n]de longitudN. - Kernel del filtro (coeficientes): vector
h[k]de longitudM. - Convolución:
y[n] = \sum_{k=0}^{M-1} h[k] \cdot x[n-k] - Representación matricial: la operación anterior puede escribirse como
y = X @ h, dondeXes una matriz Toeplitz construida a partir dex.
Algoritmo de convolución mediante matrices
Construir la matriz Toeplitz permite aplicar la convolución como un producto matricial, lo que facilita el uso de batch processing y la paralelización en GPUs.
import numpy as np
def toeplitz_matrix(x, M):
"""Construye una matriz Toeplitz de x con M columnas.
Cada fila corresponde a un segmento desplazado de la señal.
"""
N = len(x)
# Padding al final para manejar los bordes
x_padded = np.concatenate([x, np.zeros(M-1)])
return np.lib.stride_tricks.as_strided(
x_padded,
shape=(N, M),
strides=(x_padded.strides[0], x_padded.strides[0])
)
# Ejemplo rápido
x = np.arange(1, 9) # Señal de 8 muestras
h = np.array([0.2, 0.5, 0.3]) # Kernel de 3 coeficientes
X = toeplitz_matrix(x, len(h))
y = X @ h
print(y)
El resultado y es idéntico al obtenido con np.convolve(x, h, mode='full')[:len(x)], pero la representación matricial abre la puerta a optimizaciones avanzadas.
Implementación práctica en Python
Ejemplo 1: Filtro pasa‑bajo FIR con NumPy
import numpy as np
import matplotlib.pyplot as plt
# Señal de prueba: suma de dos senoidales
fs = 1000 # Hz
t = np.arange(0, 1, 1/fs)
signal = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*200*t)
# Coeficientes FIR (windowed‑sinc)
cutoff = 100 / (fs/2) # Normalizado (Nyquist)
numtaps = 31
h = np.sinc(2*cutoff*(np.arange(numtaps)- (numtaps-1)/2))
window = np.hamming(numtaps)
h = h * window
h /= np.sum(h) # Normalizar ganancia DC
# Convolución mediante matriz Toeplitz
X = toeplitz_matrix(signal, len(h))
filtered = X @ h
plt.figure(figsize=(10,4))
plt.plot(t, signal, label='Original')
plt.plot(t, filtered, label='Filtrado (FIR)', alpha=0.8)
plt.legend(); plt.title('Filtro pasa‑bajo FIR con matrices'); plt.xlabel('Tiempo (s)'); plt.show()
Ejemplo 2: Comparativa con scipy.signal.lfilter (IIR)
from scipy import signal as sp
# Diseño de un filtro Butterworth IIR de orden 4
b, a = sp.butter(N=4, Wn=cutoff, btype='low', analog=False)
filtered_iir = sp.lfilter(b, a, signal)
plt.figure(figsize=(10,4))
plt.plot(t, signal, label='Original')
plt.plot(t, filtered_iir, label='IIR (Butterworth)', alpha=0.8)
plt.legend(); plt.title('Filtro IIR Butterworth con SciPy'); plt.xlabel('Tiempo (s)'); plt.show()
En la tabla siguiente se comparan métricas clave entre la implementación matricial FIR y la solución IIR de SciPy.
| Métrica | FIR (NumPy) | IIR (SciPy) |
|---|---|---|
| Orden del filtro | 30 (coeficientes) | 4 (polo‑cero) |
| Retardo de grupo | ≈15 samples | ≈0 samples (casi sin retardo) |
| Estabilidad numérica | Garantizada (coeficientes finitos) | Depende de la colocación de polos |
| Rendimiento (CPU, 1 M muestras) | ≈12 ms (NumPy BLAS) | ≈6 ms (C‑optimizado) |
| Facilidad de paralelismo GPU | Alta (matriz‑vector) | Media (kernel interno) |
Optimización y rendimiento
- Uso de
numpy.dotvsnumpy.matmul:dotes ligeramente más rápido para vectores 1‑D, mientras quematmulaprovecha@y se adapta mejor a GPU concupy. - Batch processing: Si se procesan múltiples canales simultáneamente, construya una tensor 3‑D y use
einsumpara evitar bucles explícitos. - Precisión: Para filtros de orden elevado, prefiera
float64ofloat128para evitar errores de redondeo, especialmente en IIR. - Memoria: La matriz Toeplitz ocupa
N×Melementos; para señales muy largas (≥10⁶) utilice bloques deslizantes (overlap‑add) o la FFT (np.fft.fft) para reducir complejidad aO(N log N).
Seguridad y buenas prácticas
- Valide siempre la longitud del kernel:
M <= Npara evitar index out of bounds al crear la matriz Toeplitz. - Sanitice los datos de entrada (escala a [-1, 1]) para evitar saturación en filtros de tipo IIR.
- Utilice
np.seterr(divide='raise')durante el desarrollo para detectar divisiones por cero en filtros IIR. - Documente la respuesta en frecuencia (
sp.freqz) y compárela contra especificaciones de diseño.
Depuración (troubleshooting)
Algunos síntomas comunes y sus soluciones:
- Respuesta distorsionada: Verifique que el kernel esté correctamente normalizado y que no haya aliasing (frecuencia de Nyquist).
- Retardo inesperado: Recuerde que un FIR introduce un retardo de
(M‑1)/2muestras; compense con un desplazamiento temporal. - Desbordamiento de memoria: Para
N×Mgrande, cambie a overlap‑add o FFT‑based convolution (scipy.signal.fftconvolve).
Conclusiones
Los filtros digitales basados en matrices ofrecen una visión algebraica clara del proceso de convolución y facilitan la paralelización en CPUs y GPUs. Si bien la implementación directa con np.convolve es suficiente para prototipos, la construcción de la matriz Toeplitz abre puertas a:
- Batch processing multi‑canal.
- Integración con frameworks de aprendizaje profundo (
torch.nn.Conv1d). - Optimización mediante BLAS/LAPACK o kernels CUDA.
Combine la claridad de los FIR matriciales con la eficiencia de los IIR cuando el retardo sea crítico, y siempre valide la respuesta en frecuencia para cumplir los requisitos de su aplicación.
Filtros Digitales con Matrices: Algoritmos y Ejemplos en Python