Les quatre transformées de Fourier¶
Ce notebook illustre le tableau fondamental qui relie les quatre formes de la transformée de Fourier, selon la nature du signal (continu/discret, périodique/apériodique).
| Signal temporel | Transformée | Spectre fréquentiel |
|---|---|---|
| Continu, apériodique | TF — Transformée de Fourier | Continu, apériodique |
| Continu, périodique (période $T$) | SF — Série de Fourier | Discret, apériodique (raies à $k/T$) |
| Discret, apériodique (échantillonné à $F_e$) | TFTD — TF à Temps Discret | Continu, périodique (période $F_e$) |
| Discret, périodique ($N$ points, période $T$) | TFD/DFT — TF Discrète | Discret, périodique ($N$ raies) |
Règle fondamentale :
🔁 Discret dans un domaine → Périodique dans l'autre
➡️ Continu dans un domaine → Apériodique dans l'autre
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.gridspec import GridSpec
plt.rcParams.update({
'figure.dpi': 120,
'font.size': 10,
'axes.grid': True,
'grid.alpha': 0.3,
'axes.spines.top': False,
'axes.spines.right': False,
})
# Palette commune
C_TIME = '#2980b9' # bleu = domaine temporel
C_FREQ = '#c0392b' # rouge = domaine fréquentiel
C_GHOST = '#bdc3c7' # gris = répétitions "fantômes" (périodicité)
print("OK")
Vue d'ensemble — le tableau en images¶
Avant d'entrer dans les détails, voici les quatre cas côte à côte :
fig, axes = plt.subplots(4, 2, figsize=(14, 14))
titres = [
('TF — Transformée de Fourier', 'Continu, apériodique', 'Continu, apériodique'),
('SF — Série de Fourier', 'Continu, périodique', 'Discret, apériodique'),
('TFTD — TF à Temps Discret', 'Discret, apériodique', 'Continu, périodique'),
('TFD — Transformée de Fourier Discrète', 'Discret, périodique', 'Discret, périodique'),
]
# ── TF ──────────────────────────────────────────────────────────────
t = np.linspace(-4, 4, 2000)
x = np.exp(-t**2) # gaussienne
f = np.linspace(-3, 3, 2000)
Xf = np.sqrt(np.pi) * np.exp(-np.pi**2 * f**2) # TF analytique
axes[0,0].plot(t, x, color=C_TIME, lw=1.8)
axes[0,1].plot(f, Xf, color=C_FREQ, lw=1.8)
# ── SF ──────────────────────────────────────────────────────────────
T = 2.0
t2 = np.linspace(-3*T, 3*T, 3000)
x2 = np.sign(np.sin(2*np.pi*t2/T)) # créneau périodique
ks = np.arange(-9, 10)
ck = np.where(ks % 2 != 0, 2/(np.pi*ks), 0.0) # coeff de Fourier
ck[ks==0] = 0
axes[1,0].plot(t2, x2, color=C_TIME, lw=1.5)
axes[1,1].vlines(ks/T, 0, ck, color=C_FREQ, lw=2)
axes[1,1].scatter(ks/T, ck, color=C_FREQ, s=30, zorder=3)
# ── TFTD ────────────────────────────────────────────────────────────
Fe = 10.0
ns = np.arange(-20, 21)
x3 = np.exp(-(ns/Fe)**2 * 5) # gaussienne échantillonnée
f3 = np.linspace(-Fe, Fe, 2000) # 2 périodes pour montrer la périodicité
TFTD = np.array([np.sum(x3 * np.exp(-2j*np.pi*f_k*ns/Fe)) for f_k in f3]).real
axes[2,0].vlines(ns/Fe, 0, x3, color=C_TIME, lw=1.5)
axes[2,0].scatter(ns/Fe, x3, color=C_TIME, s=20, zorder=3)
# répétitions périodiques en gris
axes[2,1].plot(f3, TFTD, color=C_FREQ, lw=1.8)
axes[2,1].axvline(-Fe/2, color=C_GHOST, ls='--', lw=1, label='±Fe/2 (frontière)')
axes[2,1].axvline( Fe/2, color=C_GHOST, ls='--', lw=1)
axes[2,1].legend(fontsize=8)
# ── TFD ─────────────────────────────────────────────────────────────
N = 16
ns4 = np.arange(N)
x4 = np.cos(2*np.pi*3*ns4/N) + 0.5*np.cos(2*np.pi*5*ns4/N)
X4 = np.fft.fft(x4)
# on affiche 2 périodes pour montrer la périodicité
ns4_ext = np.arange(2*N)
x4_ext = np.tile(x4, 2)
ks4_ext = np.arange(2*N)
X4_ext = np.tile(np.abs(X4), 2)
axes[3,0].vlines(ns4_ext, 0, x4_ext, color=C_TIME, lw=1.5)
axes[3,0].scatter(ns4_ext, x4_ext, color=C_TIME, s=25, zorder=3)
axes[3,0].axvline(N-0.5, color=C_GHOST, ls='--', lw=1, label='1 période (N pts)')
axes[3,0].legend(fontsize=8)
axes[3,1].vlines(ks4_ext, 0, X4_ext, color=C_FREQ, lw=1.5)
axes[3,1].scatter(ks4_ext, X4_ext, color=C_FREQ, s=25, zorder=3)
axes[3,1].axvline(N-0.5, color=C_GHOST, ls='--', lw=1, label='1 période (N raies)')
axes[3,1].legend(fontsize=8)
# ── Mise en forme ────────────────────────────────────────────────────
labels_t = ['Temps', 'Temps', 'Échantillon n', 'Échantillon n']
labels_f = ['Fréquence (Hz)', 'Fréquence (Hz)', 'Fréquence (Hz)', 'Raie k']
for i, (titre, nature_t, nature_f) in enumerate(titres):
axes[i,0].set_title(f'{titre}\n▸ Temps : {nature_t}', fontsize=9)
axes[i,1].set_title(f'{titre}\n▸ Spectre : {nature_f}', fontsize=9)
axes[i,0].set_xlabel(labels_t[i])
axes[i,1].set_xlabel(labels_f[i])
plt.suptitle('Les quatre transformées de Fourier — vue d\'ensemble', fontsize=13, fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()
Cas 1 — Transformée de Fourier (TF)¶
Signal : continu, apériodique → Spectre : continu, apériodique
$$X(f) = \int_{-\infty}^{+\infty} x(t)\, e^{-j2\pi ft}\, dt$$
C'est le cas le plus général, mais aussi le plus théorique : en pratique, un signal réellement continu et infini n'existe pas. La TF sert de fondement mathématique aux trois autres.
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Trois signaux apériodiques classiques et leur TF analytique
t = np.linspace(-5, 5, 5000)
f = np.linspace(-4, 4, 5000)
signaux = [
('Gaussienne\n$x(t)=e^{-t^2}$',
np.exp(-t**2),
np.sqrt(np.pi)*np.exp(-np.pi**2*f**2),
'TF : gaussienne (même forme)'),
('Porte\n$x(t)=\\Pi(t/2)$',
(np.abs(t) <= 1).astype(float),
2*np.sinc(2*f),
'TF : sinus cardinal'),
('Exponentielle\n$x(t)=e^{-|t|}$',
np.exp(-np.abs(t)),
2/(1+(2*np.pi*f)**2),
'TF : lorentzienne'),
]
for ax, (nom, xt, Xf, tf_nom) in zip(axes, signaux):
ax2 = ax.twinx()
ax.plot(t, xt, color=C_TIME, lw=1.8, label=nom)
ax2.plot(f, np.abs(Xf), color=C_FREQ, lw=1.8, ls='--', label=tf_nom)
ax.set_xlabel('t (bleu) / f (rouge)')
ax.set_title(f'{nom}\n→ {tf_nom}', fontsize=9)
ax.set_xlim(-4, 4)
ax.tick_params(axis='y', labelcolor=C_TIME)
ax2.tick_params(axis='y', labelcolor=C_FREQ)
plt.suptitle('TF — signal continu apériodique → spectre continu apériodique', fontweight='bold')
plt.tight_layout()
plt.show()
print("Remarque : les deux domaines sont continus et les fonctions s'étendent à l'infini.")
Cas 2 — Série de Fourier (SF)¶
Signal : continu, périodique (période $T$) → Spectre : discret, apériodique (raies à $k/T$)
$$c_k = \frac{1}{T}\int_0^T x(t)\, e^{-j2\pi kt/T}\, dt \qquad x(t) = \sum_{k=-\infty}^{+\infty} c_k\, e^{j2\pi kt/T}$$
La périodicité dans le temps provoque la discrétisation du spectre : il n'y a plus qu'un ensemble de raies aux fréquences $k/T$, espacées de $1/T$.
Pourquoi ? La contrainte de périodicité impose que seules les fréquences harmoniques de $1/T$ peuvent s'additionner et reconstruire le signal sans discontinuité.
T = 1.0 # période
t = np.linspace(-2*T, 2*T, 4000)
# Créneau : c_k = 0 si k pair, 2/(jπk) si k impair
def creneau_sf(t, T, N_harm):
"""Reconstruction par série de Fourier tronquée à N_harm harmoniques."""
x = np.zeros_like(t)
for k in range(1, N_harm+1, 2): # k impairs seulement
x += (4/(np.pi*k)) * np.sin(2*np.pi*k*t/T)
return x
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
# Reconstruction progressive
for ax, N_h in zip(axes.flat, [1, 3, 7, 21]):
x_reel = np.sign(np.sin(2*np.pi*t/T))
x_sf = creneau_sf(t, T, N_h)
ax.plot(t, x_reel, color=C_GHOST, lw=1, label='Créneau réel')
ax.plot(t, x_sf, color=C_TIME, lw=1.8, label=f'{N_h} harmonique(s)')
ax.set_title(f'{N_h} harmonique(s) — raies à k = ±1, ±3, ... ±{N_h}')
ax.set_xlabel('Temps (s)')
ax.set_ylim(-1.6, 1.6)
ax.legend(fontsize=8)
plt.suptitle('SF — convergence de la série de Fourier vers le créneau\n(phénomène de Gibbs visible aux discontinuités)',
fontweight='bold')
plt.tight_layout()
plt.show()
# Spectre discret : raies c_k
k_max = 15
ks = np.arange(-k_max, k_max+1)
ck_amp = np.where(ks % 2 != 0, np.abs(2/(np.pi*ks)), 0.0)
ck_amp[ks==0] = 0
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
# Signal temporel
t2 = np.linspace(-2.5*T, 2.5*T, 4000)
x2 = np.sign(np.sin(2*np.pi*t2/T))
axes[0].plot(t2, x2, color=C_TIME, lw=1.8)
axes[0].set_title('Signal temporel — continu, périodique (période T=1 s)')
axes[0].set_xlabel('Temps (s)')
# Spectre
axes[1].vlines(ks/T, 0, ck_amp, color=C_FREQ, lw=2.5)
axes[1].scatter(ks/T, ck_amp, color=C_FREQ, s=40, zorder=4)
axes[1].set_title('Spectre — discret (raies à k/T), apériodique\n(amplitudes décroissantes : c_k → 0 quand k → ∞)')
axes[1].set_xlabel('Fréquence (Hz)')
axes[1].set_ylabel('|c_k|')
plt.suptitle('SF — signal périodique → spectre à raies discrètes', fontweight='bold')
plt.tight_layout()
plt.show()
Cas 3 — Transformée de Fourier à Temps Discret (TFTD)¶
Signal : discret, apériodique (échantillonné à $F_e$) → Spectre : continu, périodique (période $F_e$)
$$X(f) = \sum_{n=-\infty}^{+\infty} x[n]\, e^{-j2\pi fn/F_e}$$
C'est la situation de l'échantillonnage : on discrétise le temps en prélevant des échantillons à la cadence $F_e$.
Pourquoi le spectre devient-il périodique ?
Un signal discret peut s'écrire comme un signal continu multiplié par un peigne de Dirac (train d'impulsions espacées de $1/F_e$). La multiplication dans le temps correspond à une convolution dans le domaine fréquentiel avec le même peigne — ce qui répète le spectre toutes les $F_e$ Hz.
Fe = 10.0
ns = np.arange(-30, 31)
sigma = 4.0
x_disc = np.exp(-0.5*(ns/sigma)**2) # gaussienne apériodique
# TFTD calculée sur [-1.5*Fe, 1.5*Fe] pour montrer 3 périodes
f_tftd = np.linspace(-1.5*Fe, 1.5*Fe, 3000)
TFTD = np.array([np.sum(x_disc * np.exp(-2j*np.pi*f_k*ns/Fe)).real for f_k in f_tftd])
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
# Signal discret
axes[0].vlines(ns/Fe, 0, x_disc, color=C_TIME, lw=1.5)
axes[0].scatter(ns/Fe, x_disc, color=C_TIME, s=25, zorder=3)
axes[0].set_title(f'Signal discret, apériodique\n(échantillonné à Fe={Fe} Hz — {len(ns)} échantillons)')
axes[0].set_xlabel('Temps (s)')
# Spectre continu et périodique
axes[1].plot(f_tftd, TFTD, color=C_FREQ, lw=1.8)
for k in [-1, 0, 1]:
axes[1].axvspan(k*Fe - Fe/2, k*Fe + Fe/2,
alpha=0.06, color=['orange','blue','green'][k+1])
axes[1].axvline(k*Fe, color=C_GHOST, ls=':', lw=1)
axes[1].axvline(-Fe/2, color='red', ls='--', lw=1.5, label='-Fe/2 (repliement)')
axes[1].axvline( Fe/2, color='red', ls='--', lw=1.5, label='+Fe/2 (repliement)')
axes[1].set_title('Spectre TFTD — continu, périodique (période Fe)\n'
'Chaque bande colorée est une copie identique')
axes[1].set_xlabel('Fréquence (Hz)')
axes[1].legend(fontsize=8)
plt.suptitle('TFTD — signal discret apériodique → spectre continu périodique', fontweight='bold')
plt.tight_layout()
plt.show()
print("→ On ne travaille qu'avec [-Fe/2, +Fe/2] en pratique : c'est la bande de Nyquist.")
print(f"→ Fréquence de Nyquist = Fe/2 = {Fe/2} Hz")
# Visualisation de l'aliasing : conséquence de la périodicité du spectre
Fe = 10.0
t_cont = np.linspace(0, 3, 3000)
ns_disc = np.arange(0, int(3*Fe)+1)
t_disc = ns_disc / Fe
f_sig = 2.0 # fréquence réelle
f_alias = Fe - f_sig # fréquence image (repliée)
x_cont1 = np.sin(2*np.pi*f_sig*t_cont)
x_cont2 = np.sin(2*np.pi*f_alias*t_cont)
x_disc1 = np.sin(2*np.pi*f_sig*t_disc)
x_disc2 = np.sin(2*np.pi*f_alias*t_disc)
fig, ax = plt.subplots(figsize=(13, 4))
ax.plot(t_cont, x_cont1, color=C_TIME, lw=1.5, label=f'f = {f_sig} Hz (signal réel)', alpha=0.8)
ax.plot(t_cont, x_cont2, color=C_FREQ, lw=1.5, label=f'f = {f_alias} Hz (alias = Fe−f)', alpha=0.8)
ax.vlines(t_disc, -1.05, 1.05, color=C_GHOST, lw=0.5, alpha=0.4)
ax.scatter(t_disc, x_disc1, color=C_TIME, s=50, zorder=5, label='Échantillons (identiques pour les deux !)')
ax.scatter(t_disc, x_disc2, color=C_FREQ, s=20, zorder=6, marker='x')
ax.set_title(f'Aliasing : f={f_sig} Hz et f={f_alias} Hz donnent exactement les mêmes échantillons à Fe={Fe} Hz\n'
'→ Conséquence directe de la périodicité du spectre TFTD')
ax.set_xlabel('Temps (s)')
ax.legend()
plt.tight_layout()
plt.show()
Cas 4 — Transformée de Fourier Discrète (TFD / DFT)¶
Signal : discret, périodique ($N$ points) → Spectre : discret, périodique ($N$ raies)
$$X[k] = \sum_{n=0}^{N-1} x[n]\, e^{-j2\pi kn/N} \qquad x[n] = \frac{1}{N}\sum_{k=0}^{N-1} X[k]\, e^{j2\pi kn/N}$$
C'est le seul cas calculable en pratique : discret dans les deux domaines. C'est ce que fait la FFT.
Attention : la TFD suppose implicitement que le signal est périodique (elle voit le bloc de $N$ points comme une période). Si le signal ne l'est pas réellement → fuite spectrale (leakage).
N = 32
ns = np.arange(N)
f0 = 3 # fréquence du signal (en raies)
x = np.cos(2*np.pi*f0*ns/N) + 0.6*np.cos(2*np.pi*5*ns/N)
X = np.fft.fft(x)
ks = np.arange(N)
# On affiche 2 périodes pour montrer la périodicité
x2 = np.tile(x, 2)
X2 = np.tile(np.abs(X), 2)
n2 = np.arange(2*N)
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
axes[0].vlines(n2, 0, x2, color=C_TIME, lw=1.5, alpha=0.7)
axes[0].scatter(n2, x2, color=C_TIME, s=30, zorder=3)
axes[0].axvspan(N-0.5, 2*N-0.5, alpha=0.07, color='orange', label='2e période (répétition)')
axes[0].axvline(N-0.5, color=C_GHOST, ls='--', lw=1.5)
axes[0].set_title(f'Signal discret, périodique (N={N} pts)\n→ la TFD voit ce bloc comme une période')
axes[0].set_xlabel('Échantillon n')
axes[0].legend(fontsize=8)
axes[1].vlines(n2, 0, X2, color=C_FREQ, lw=2, alpha=0.7)
axes[1].scatter(n2, X2, color=C_FREQ, s=30, zorder=3)
axes[1].axvspan(N-0.5, 2*N-0.5, alpha=0.07, color='orange', label='2e période (N raies)')
axes[1].axvline(N-0.5, color=C_GHOST, ls='--', lw=1.5)
axes[1].set_title(f'Spectre TFD — discret, périodique (N={N} raies)\nPics aux raies k=3 et k=5 (et symétriques k=N-3, k=N-5)')
axes[1].set_xlabel('Raie k')
axes[1].legend(fontsize=8)
plt.suptitle('TFD — signal discret périodique → spectre discret périodique', fontweight='bold')
plt.tight_layout()
plt.show()
# Fuite spectrale : conséquence de l'hypothèse implicite de périodicité
N = 64
ns = np.arange(N)
f_entier = 8.0 # fréquence = nombre entier de périodes dans le bloc → OK
f_fracti = 8.7 # fréquence non entière → fuite
x_ok = np.cos(2*np.pi*f_entier*ns/N)
x_fuite = np.cos(2*np.pi*f_fracti*ns/N)
X_ok = np.fft.fft(x_ok)
X_fuite = np.fft.fft(x_fuite)
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
ks = np.arange(N)
axes[0].vlines(ks, 0, np.abs(X_ok), color=C_FREQ, lw=1.5)
axes[0].set_title(f'Pas de fuite : f={f_entier} Hz → N/f = {N/f_entier:.0f} pts = entier\n'
'→ La sinusoïde se raccorde parfaitement en boucle')
axes[0].set_xlabel('Raie k')
axes[1].vlines(ks, 0, np.abs(X_fuite), color='darkorange', lw=1.5)
axes[1].set_title(f'Fuite spectrale : f={f_fracti} Hz → N/f = {N/f_fracti:.2f} pts ≠ entier\n'
'→ Discontinuité au raccord → énergie dispersée sur toutes les raies')
axes[1].set_xlabel('Raie k')
plt.suptitle('Fuite spectrale (leakage) — conséquence de l\'hypothèse de périodicité implicite de la TFD',
fontweight='bold')
plt.tight_layout()
plt.show()
print("→ Solution : fenêtrage (Hann, Hamming...) comme dans la méthode de Welch.")
fig = plt.figure(figsize=(13, 7))
ax = fig.add_axes([0.05, 0.05, 0.9, 0.9])
ax.set_xlim(0, 10)
ax.set_ylim(0, 6)
ax.axis('off')
# Titre
ax.text(5, 5.7, 'La règle Discret ↔ Périodique', ha='center', va='center',
fontsize=14, fontweight='bold')
# En-têtes colonnes
for x_pos, label, color in [(2.5, 'TEMPS', C_TIME), (7.5, 'FRÉQUENCE', C_FREQ)]:
ax.text(x_pos, 5.2, label, ha='center', va='center',
fontsize=12, fontweight='bold', color=color)
# Séparateur vertical
ax.axvline(5, color='gray', lw=1.5, ls='--', ymin=0.05, ymax=0.9)
# Les 4 cas
cas = [
('TF', 'Continu', 'Apériodique', 'Continu', 'Apériodique'),
('SF', 'Continu', 'Périodique', 'Discret', 'Apériodique'),
('TFTD', 'Discret', 'Apériodique', 'Continu', 'Périodique'),
('TFD', 'Discret', 'Périodique', 'Discret', 'Périodique'),
]
y_positions = [4.2, 3.1, 2.0, 0.9]
for (nom, t_cont, t_per, f_cont, f_per), y in zip(cas, y_positions):
# Fond de ligne
bg = '#f8f9fa' if cas.index((nom,t_cont,t_per,f_cont,f_per)) % 2 == 0 else '#ffffff'
rect = mpatches.FancyBboxPatch((0.2, y-0.35), 9.6, 0.7,
boxstyle='round,pad=0.05', fc=bg, ec='lightgray', lw=1)
ax.add_patch(rect)
# Nom de la transformée
ax.text(0.7, y, nom, ha='left', va='center', fontsize=11, fontweight='bold', color='#2c3e50')
# Domaine temporel
c_disc_t = '#e74c3c' if t_cont == 'Discret' else C_TIME
c_per_t = '#e67e22' if t_per == 'Périodique' else C_TIME
ax.text(2.5, y+0.12, t_cont, ha='center', va='center', fontsize=10, color=c_disc_t, fontweight='bold' if t_cont=='Discret' else 'normal')
ax.text(2.5, y-0.15, t_per, ha='center', va='center', fontsize=10, color=c_per_t, fontweight='bold' if t_per=='Périodique' else 'normal')
# Flèche
ax.annotate('', xy=(6.5, y), xytext=(3.5, y),
arrowprops=dict(arrowstyle='->', color='gray', lw=1.5))
ax.text(5, y, nom, ha='center', va='center', fontsize=8, color='gray',
bbox=dict(fc='white', ec='none', pad=1))
# Domaine fréquentiel
c_disc_f = '#e74c3c' if f_cont == 'Discret' else C_FREQ
c_per_f = '#e67e22' if f_per == 'Périodique' else C_FREQ
ax.text(7.5, y+0.12, f_cont, ha='center', va='center', fontsize=10, color=c_disc_f, fontweight='bold' if f_cont=='Discret' else 'normal')
ax.text(7.5, y-0.15, f_per, ha='center', va='center', fontsize=10, color=c_per_f, fontweight='bold' if f_per=='Périodique' else 'normal')
# Légende
ax.text(5, 0.2, '🔴 Discret ↔ 🟠 Périodique (bleu = temps, rouge = fréquence)',
ha='center', va='center', fontsize=10, color='#555',
bbox=dict(fc='#fef9e7', ec='#f39c12', boxstyle='round,pad=0.4'))
plt.show()
La règle expliquée mathématiquement¶
La dualité Discret ↔ Périodique n'est pas un hasard : elle découle directement des propriétés de la transformée.
Échantillonner dans le temps (rendre discret) revient à multiplier par un peigne de Dirac $\sum_n \delta(t - n/F_e)$.
La TF d'un peigne de Dirac est un autre peigne de Dirac dans le domaine fréquentiel.
Multiplier par un peigne ↔ convoluer par un peigne → le spectre se répète (devient périodique).
$$x_s(t) = x(t) \cdot \underbrace{\sum_n \delta(t-n/F_e)}_{\text{peigne}} \xrightarrow{\mathcal{F}} X_s(f) = F_e \underbrace{\sum_k X(f - kF_e)}_{\text{spectre périodique}}$$
Le même raisonnement s'applique en sens inverse : rendre le spectre discret (SF) revient à rendre le signal périodique dans le temps.