L'analyse spectrale d'un signal s(t) se fait sur une durée T, correspondant par exemple à la durée de l'enregistrement du signal. On considère par convention que la fonction s(t) est définie sur . L'analyse spectrale repose sur la transformée de Fourier, qui par définition s'applique à une fonction définie sur $-]\infty,+\infty[$. L'analyse spectrale de s(t) consiste à calculer la transformée de Fourier de :
où est une fonction de fenêtrage (ou plus simplement fenêtre) définie sur $]-\infty,+\infty[$ mais non nulle seulement sur l'intervalle . La fonction u(t) est bien définie sur $]-\infty,+\infty[$ mais elle est nulle en dehors de l'intervalle .
D'après le théorème de convolution, la transformée de Fourier (TF) de u(t) est le produit de convolution de la TF de s(t) et de la TF de w(t). On est donc amené à s'intéresser à la TF de la fenêtre w(t). La résolution fréquentielle de l'analyse spectrale est essentiellement déterminée par la durée T mais la forme de la fenêtre a aussi une influence sur le spectre et notamment sur la possibilité de résoudre deux raies très voisines.
L'analyse spectrale d'un signal échantillonné ([1]) repose sur la transformée de Fourier à temps discret (TFTD) et sur la transformée de Fourier discrète (TFD).
Ce document montre les propriétés de différentes fenêtres. On verra en particulier comment la TFD permet d'obtenir une approximation de la TF d'une fenêtre. La méthode exposée est de première importance car elle s'applique également pour obtenir la TF du signal s(t).
Rappelons tout d'abord la définition de la transformée de Fourier (TF). Par convention, on note par une majuscule la TF d'un signal dont la fonction est notée par une minuscule. La transformée de Fourier est une fonction de la fréquence ou bien de la pulsation . Dans ce document, on exprime la TF comme fonction de la fréquence. La TF de u(t) est définie par :
Si la fonction u(t) est à valeurs réelles (hypothèse que l'on fera pour la suite), on a :
où désigne le complexe conjugé de .
Un signal échantillonné avec la période d'échantillonnage Te est défini par :
où q est un nombre entier (relatif). Le signal uq est qualifié de signal numérique ou bien de signal à temps discret. L'approximation de l'intégrale par la formule des rectangles s'écrit :
La transformée de Fourier à temps discret (TFTD) est définie par :
La TFTD constitue une approximation de la TF, d'autant meilleure que la période d'échantillonnage est petite :
Bien évidemment, le calcul numérique d'une TFTD est impossible car le nombre de terme de la somme est infini, à moins que la fonction u(t) soit à support borné, ce qui est précisément le cas de la fonction u(t) définie par . Sur l'intervalle de largeur T, la fonction u(t) comporte N échantillons, que l'on suppose pair. Posons alors :
avec .
Les échantillons sont donc indexés par k et on bien sûr . L'approximation s'écrit alors :
Sachant que , on obtient :
Posons :
qui constitue l'approximation de la TF pour un signal comportant N échantillons. Dans un premier temps, on limite le calcul de l'approximation de la TF aux fréquences définies par :
où n est un nombre entier. On a pour ces fréquences :
La transformée de Fourier discrète est définie par :
Il est important de faire la disctinction entre la TFTD et la TFD :
Autrement dit, dans la TFTD, seul le temps est discret, alors que dans la TFD, le temps et la fréquence sont discrets.
D'après ce qui précède, on a :
Si nous faisons en plus les deux hypothèses suivantes :
alors la TFDT est exactement égale à la TF sur l'intervalle de fréquences [-fe/2,fe/2] :
La TFD possède deux propriétés importantes. La première est la périodicité :
Il résulte de la périodicité que le calcul de la TFD se limite à :
La TFD peut être vue comme une application qui à N nombres réels uk associe les N nombres complexes pour . Le calcul numérique de la TFD de N nombres consiste donc à calculer N sommes, chacun comportant N termes. La complexité temporelle de ce calcul est donc en principe . Si N est une puissance de 2, l'algorithme de Transformée de Fourier rapide permet de faire le calcul avec une complexité (cet algorithme devrait s'appeler transformée de Fourier discrète rapide).
Dans la mesure où les valeurs de u(t) sont réelles, la TFD possède une autre propriété importante :
Remarque : dans la définition de la TFD (relation ) le facteur est parfois omis. C'est le cas pour la fonction numpy.fft.fft.
La TFD permet, grace à la relation , d'obtenir une approximation de la TF de u(t), mais seulement pour les fréquences . Il est possible d'obtenir la TFTD, ou du moins une approximation de la TFTD, qui est plus proche de la TF car elle est définie pour toute fréquence. La TFTD correspond à la limite de la TFD lorsque . Pour s'approcher de la TFTD, il suffit donc d'augmenter la durée T au delà de la largeur de la fenêtre , voire même beaucoup plus grande que cette largeur. Il suffit pour cela de compléter le signal échantillonné et fenêtré par des zéros. C'est la méthode de remplissage par des zéros (zero padding). Si N désigne la longueur du signal échantillonné, on complète par des zéros de manière à obtenir un signal à N' échantillons auquel on applique la TFD. N' doit être une puissance de 2 de manière à pouvoir faire le calcul avec l'algorithme de transformée de Fourier rapide. La résolution fréquentielle obtenue est :
Un rapport de 5 à 10 permet d'obtenir en général une très bonne approximation de la TFTD, c'est-à-dire un spectre pour une fréquence continue (non discrète).
Pour résumer, cette méthode du remplissage par des zéros a un double intérêt :
Bien évidemment, l'augmentation de T ne change rien au fait que la période d'échantillonnage Te est finie (non nulle). Pour cette raison, le spectre du signal échantillonné est périodique, de période (la fréquence d'échantillonnage).
Si N' est assez grand, une simple interpolation linéaire entre les points (ce que fait la fonction matplotlib.pyplot.plot) permet d'obtenir une bonne représentation graphique de la TFDT, donc de la TF.
Remarque : il arrive fréquemment que le nombre d'échantillons N enregistrés dans une expérience ne soit pas une puissance de 2. Ce serait une erreur de chercher à réduire N pour obtenir une puissance de 2. En effet, cette méthode consisterait soit à ne garder qu'une partie des échantillons (en nombre égal à une puissance de 2), soit à procéder à une réduction de la fréquence d'échantillonnage afin de garder la durée T inchangée. Ces deux méthodes sont mauvaises, la première parce qu'elle réduit la durée analysée, la seconde parce qu'elle réduit la fréquence d'échantillonnage (qui normalement est choisie de manière judicieuse par l'expérimentateur). La bonne méthode consiste à compléter le signal échantillonné par des zéros afin d'obtenir un nombre N' d'échantillons puissance de 2. Cette méthode ne change ni la fréquence d'échantillonnage, ni la durée analysée. On aura même intérêt à ajouter un très grand nombre de zéros afin à obtenir une bonne représentation de la TFTD.
La fenêtre la plus simple est la fenêtre rectangulaire, définie par :
C'est le fenêtrage qui se fait par défaut lorsque les échantillons sont obtenus par une expérience de durée T.
Considérons sa transformée de Fourier :
Cette intégrale se calcule aisément, on obtient :
où sinc désigne la fonction sinus cardinal.
Les valeurs de cette TF sont réelles mais certaines sont négatives. Pour obtenir une représentation graphique indépendante de T, nous pouvons tracer en fonction de :
import numpy as np from matplotlib.pyplot import * figure(figsize=(16,6)) x = np.linspace(-100,100,10000) Wr = np.sinc(x) plot(x,Wr) xlim(-20,20) grid() xlabel("fT",fontsize=16) ylabel(r"$W_r(f)/T$",fontsize=16)fig1.pdf
Le plus souvent, on représente le module de la TF :
import numpy as np from matplotlib.pyplot import * figure(figsize=(16,6)) plot(x,np.absolute(Wr)) xlim(-20,20) grid() xlabel("fT",fontsize=16) ylabel(r"$|W_r(f)|/T$",fontsize=16)fig2.pdf
Comme déjà mentionné, il s'agit d'une fonction paire car le signal (ici la fonction de fenêtrage) est à valeurs réelles.
Le lobe principal de la TF a une largeur :
Ce lobe principal a donc une largeur inversement proportionnelle à la durée de la fenêtre. Pour apprécier la hauteur des lobes secondaires, il est préférable de tracer le spectre en décibel :
import numpy as np from matplotlib.pyplot import * figure(figsize=(16,6)) plot(x,20*np.log10(np.absolute(Wr))) grid() xlabel("fT",fontsize=16) ylabel(r"$20\,ln(W_r(f)/T)$",fontsize=16) xlim(-20,20) ylim(-50,0)fig3.pdf
La fenêtre rectangulaire est caractérisée par des lobes secondaires relativement hauts. Il faut aller jusqu'au troisième lobe pour descendre en dessous de -20 dB par rapport au maximum.
Voyons à présent comment la TFD permet d'obtenir une approximation de la TF de la fenêtre rectangulaire. On doit calculer la TFD d'un signal défini par N échantillons tous égaux à 1 :
from numpy.fft import fft N = 1000 w = np.ones(N) tfd = 1/N*fft(w)
Il fait remarquer que la fonction numpy.fft.fft fonctionne pour une taille de tableau quelconque. L'algorithme implémenté par cette fonction consiste à appliquer la FFT tant que la taille du tableau est divisible par 2 puis à terminer par un calcul direct de la TFD. Par exemple, si le tableau contient 100 éléments, on se ramène au calcul de deux TFD à 50 éléments, puis à 4 TFD à 25 éléments. Puisque 25 est impair, le calcul d'une TFD à 25 éléments doit se faire de manière directe, c'est-à-dire par calcul direct des 25 sommes définissant la TFD (chacune comportant 25 termes). Pour que le calcul soit optimisé, il est donc important que N soit une puissance de 2.
Pour représenter graphiquement le spectre, il faut définir les fréquences associées à la TFD : . Comme ci-dessus, nous allons en fait utiliser la fréquence réduite définie par .
freq_red = np.arange(N)
Pour obtenir l'approximation de , on applique la relation :
tf_approx = np.exp(1j*np.pi*freq_red)*tfd
Il faut remarquer que le facteur ne change rien au module.
Voici le tracé de l'approximation de la TF obtenue par la TFD (sous forme de points) superposé au tracé de la TF :
import numpy as np from matplotlib.pyplot import * figure(figsize=(16,6)) plot(x,np.absolute(Wr)) plot(freq_red,np.absolute(tf_approx),'r.') grid() xlabel("fT",fontsize=16) ylabel(r"$W_r(f)/T$",fontsize=16) xlim(0,20)fig4.pdf
Comme on le voit, la TFD appliquée aux échantillons limités à la fenêtre elle-même donne une approximation de la TF très grossière car elle ne permet pas de reconstituer la courbe avec ses lobes. Cela n'a rien d'étonnant, puisque le signal échantillonné considéré n'est qu'une partie de la fonction de fenêtrage. Nous avons volontairement commencé par cet exemple car c'est précisément de qu'il se passe lorsqu'on applique la TFD à un signal échantillonné limité à une durée T sans le compléter par des zéros. Pour améliorer le résultat, il faut compléter par des zéros, ce qui revient simplement à définir le signal échantillonné sur une durée beaucoup plus grande que la largeur de la fenêtre (T). Le signal complété par des zéros comporte N' échantillons (de préférence une puissance de 2). Les fréquences de la TFD sont espacées de soit un espacement de exprimé en fréquence réduite (fréquence mutlipliée par T). Nous ajoutons des zéros à gauche et à droite, de manière garder une fonction paire. Si on s'intéresse seulement au module de la TF, on peut ajouter les zéros à droite seulement.
Np = 2**12 w_p = np.zeros(Np) i = (N-Np)//2 w_p[i:i+N] = w tfd_p = 1/N*fft(w_p) freq_red_p = np.arange(Np)*N/Np figure(figsize=(16,6)) plot(freq_red_p,np.absolute(tfd_p),'r.') plot(x,np.absolute(Wr)) xlim(0,20) xlabel("fT",fontsize=16) ylabel(r"$W_r(f)/T$",fontsize=16) grid()fig5.pdf
Voici la totalité de la TFD :
figure(figsize=(20,6)) plot(freq_red_p,np.absolute(tfd_p),'r-') xlim(0,N) xlabel("fT",fontsize=16) ylabel(r"$W_r(f)/T$",fontsize=16) grid()fig6.pdf
Le spectre de la TFD s'étend de la fréquence nulle à la fréquence d'échantillonnage, qui s'écrit en fréquence réduite : . La première moitié de ce spectre, pour des fréquences de 0 à N/2, correspond à la TF. La seconde moitié, pour des fréquence de N/2 à N correspond, d'après la propriété de périodicité de la TFD, aux fréquence négatives de la TF. Pour un signal à valeurs réelles, la proriété de la TF (et la propriété équivalente de la TFD) implique que le spectre est une fonction paire et qu'il n'est donc pas nécessaire de le représenter pour les fréquences négatives.
L'ajout de zéro permet, comme expliqué plus haut, d'obtenir une approximation satisfaisante de la TFTD à partir de la TF, ce qui finalement nous permet d'obtenir une bonne approximation de la TF. Voyons le résultat avec un N' 100 fois plus grand :
Np = 2**14 w_p = np.zeros(Np) i = (N-Np)//2 w_p[i:i+N] = w tfd_p = 1/N*fft(w_p) freq_red_p = np.arange(Np)*N/Np figure(figsize=(16,6)) plot(freq_red_p,np.absolute(tfd_p),'r.') plot(x,np.absolute(Wr)) xlim(0,20) xlabel("fT",fontsize=16) ylabel(r"$W_r(f)/T$",fontsize=16) grid()fig7.pdf
Comme prévu, plus on ajoute de zéros, c'est-à-dire plus la durée est grande, plus on s'approche de la TFTD et plus on s'approche de la TF (pour les fréquences inférieures à ).
La fenêtre de Hamming généralisée est définie par :
où est la fenêtre rectangulaire de largeur T étudiée ci-dessus.
En particulier la fenêtre de Hamming est obtenue pour α=0,54 et la fenêtre de Hann pour α=0,5. La fonction scipy.signal.windows.general_hamming renvoie un échantillonnage à N points de cette fenêtre. Voici par exemple la fenêtre de Hamming :
from scipy.signal.windows import general_hamming N = 1000 alpha = 0.54 w = general_hamming(N,alpha) t = np.arange(N)/N-0.5 figure(figsize=(8,6)) title('Hamming') plot(t,w) grid() xlabel(r"$\frac{t}{T}$",fontsize=16) ylabel(r"$w_h$",fontsize=16) ylim(0,1)fig8.pdf
Pour calculer la transformée de Fourier (TF) de la fenêtre de Hamming généralisée, considérons tout d'abord la TF du cosinus :
La TF de la fenêtre de Hamming généralisée s'en déduit :
On trace en fonction de :
figure(figsize=(16,6)) title('Hamming') alpha = 0.54 x = np.linspace(-100,100,10000) Wh = alpha*np.sinc(x)+(1-alpha)/2*(np.sinc(1-x)+np.sinc(1+x)) plot(x,np.absolute(Wh)) xlim(-20,20) grid() xlabel("fT",fontsize=16) ylabel(r"$W_h(f)/T$",fontsize=16)fig9.pdf
figure(figsize=(16,6)) title('Hamming') Wh_max = Wh.max() plot(x,20*np.log10(np.absolute(Wh/Wh_max))) xlim(-20,20) ylim(-100,0) grid() xlabel("fT",fontsize=16) ylabel(r"$W_h(f)/T$",fontsize=16) ylim(-100,0)fig10.pdf
Le lobe principal de la TF de la fenêtre de Hamming est beaucoup plus large que celui de la fenêtre rectangulaire mais les autres lobes sont beaucoup plus faibles puisqu'ils sont à moins de -40 dB par rapport au maximum.
Pour calculer une approximation de la TF par TFD, on procède comme pour la fenêtre rectangulaire, en ajoutant des zéros :
Np = 2**15 w_p = np.zeros(Np) i = (N-Np)//2 w_p[i:i+N] = w tfd_p = 1/N*fft(w_p) freq_red_p = np.arange(Np)*N/Np figure(figsize=(16,6)) title('Hamming') plot(freq_red_p,20*np.log10(np.absolute(tfd_p)),'r.') plot(x,20*np.log10(np.absolute(Wh))) xlim(0,20) ylim(-100,0) xlabel("fT",fontsize=16) ylabel(r"$W_h(f)/T$",fontsize=16) grid()fig11.pdf
Voici la fenêtre de Hann (α=0,5) :
from scipy.signal.windows import general_hamming N = 1000 alpha = 0.5 w = general_hamming(N,alpha) t = np.arange(N)/N-0.5 figure(figsize=(8,6)) title('Hann') plot(t,w) grid() xlabel(r"$\frac{t}{T}$",fontsize=16) ylabel(r"$w_h$",fontsize=16) ylim(0,1)fig12.pdf
et sa transformée de Fourier :
figure(figsize=(16,6)) title('Hann') x = np.linspace(-100,100,10000) Whh = alpha*np.sinc(x)+(1-alpha)/2*(np.sinc(1-x)+np.sinc(1+x)) plot(x,np.absolute(Whh)) xlim(-20,20) grid() xlabel("fT",fontsize=16) ylabel(r"$W_h(f)/T$",fontsize=16)fig13.pdf
figure(figsize=(16,6)) title('Hann') Whh_max = Whh.max() plot(x,20*np.log10(np.absolute(Whh/Whh_max))) xlim(-20,20) ylim(-100,0) grid() xlabel("fT",fontsize=16) ylabel(r"$W_{hh}(f)/T$",fontsize=16) ylim(-100,0)fig14.pdf
Il est intéressant de comparer les fenêtres rectangulaire, Hamming et Hann :
figure(figsize=(16,6)) plot(x,20*np.log10(np.absolute(Wr)),label='Rectangulaire') plot(x,20*np.log10(np.absolute(Wh/Wh_max)),label='Hamming') plot(x,20*np.log10(np.absolute(Whh/Whh_max)),label='Hann') xlim(-20,20) ylim(-100,0) grid() xlabel("fT",fontsize=16) ylabel(r"$W_{hh}(f)/T$",fontsize=16) ylim(-100,0) legend(loc='upper right')fig15.pdf
La fenêtre rectangulaire a le lobe principal le moins large mais ses lobes secondaires sont très intenses. La fenêtre rectangulaire est très bien pour l'analyse spectrale des signaux périodiques, pour lesquels les fréquences des harmoniques sont très distantes. En revanche, elle peut poser problème quand le spectre contient deux raies très voisines, en particulier une raie faible au voisinage d'une raie intense. La fenêtre de Hamming a un lobe principal deux fois plus grand mais ses lobes secondaires sont beaucoup plus faibles car ils sont tous en dessous de -40 dB. La fenêtre de Hann présente la plus forte décroissance de la hauteur des lobes secondaires mais son premier lobe secondaire est plus haut que celui de la fenêtre de Hamming.
Par ailleurs, la hauteur du maximum (lobe principal) est différente pour les trois fenêtres. Cette hauteur doit être prise en compte pour l'estimation de la hauteur des raies dans l'analyse spectrale.
print(Wr.max()) --> 0.9998354818101798
print(Wh.max()) --> 0.5399571664110703
print(Whh.max()) --> 0.499967747680713
Les hauteurs exactes sont respectivement pour les fenêtres rectangulaire, Hamming et Hann : 1, 0,54 et 0,50.
La fenêtre de Blackman est définie par :
avec :
La fenêtre peut aussi s'écrire :
Sa transformée de Fourier est :
from scipy.signal.windows import blackman N = 1000 w = blackman(N) t = np.arange(N)/N-0.5 figure(figsize=(8,6)) title('Blackman') plot(t,w) grid() xlabel(r"$\frac{t}{T}$",fontsize=16) ylabel(r"$w_h$",fontsize=16) ylim(0,1)fig16.pdf
figure(figsize=(16,6)) title('Blackman') alpha1 = 0.42 alpha2 = -0.5 alpha3 = 0.08 x = np.linspace(-100,100,10000) Wb = alpha1*np.sinc(x)-alpha2/2*(np.sinc(1-x)+np.sinc(1+x))+alpha3/2*(np.sinc((2-x))+np.sinc((2+x))) plot(x,np.absolute(Wb)) xlim(-20,20) grid() xlabel("fT",fontsize=16) ylabel(r"$W_h(f)/T$",fontsize=16)fig17.pdf
figure(figsize=(16,6)) title('Blackman') Wb_max = Wb.max() plot(x,20*np.log10(np.absolute(Wb/Wb_max))) xlim(-20,20) ylim(-100,0) grid() xlabel("fT",fontsize=16) ylabel(r"$W_{hh}(f)/T$",fontsize=16) ylim(-100,0)fig18.pdf
Voici le calcul de sa TF par TFD :
Np = 2**15 w_p = np.zeros(Np) i = (N-Np)//2 w_p[i:i+N] = w tfd_p = 1/N*fft(w_p) freq_red_p = np.arange(Np)*N/Np figure(figsize=(16,6)) title('Blackman') plot(freq_red_p,20*np.log10(np.absolute(tfd_p)),'r.') plot(x,20*np.log10(np.absolute(Wb))) xlim(0,20) ylim(-120,0) xlabel("fT",fontsize=16) ylabel(r"$W_b(f)/T$",fontsize=16) grid()fig19.pdf
Voici une comparaison des quatre fenêtres :
figure(figsize=(16,6)) plot(x,20*np.log10(np.absolute(Wr)),label='Rectangulaire') plot(x,20*np.log10(np.absolute(Wh/Wh_max)),label='Hamming') plot(x,20*np.log10(np.absolute(Whh/Whh_max)),label='Hann') plot(x,20*np.log10(np.absolute(Wb/Wb_max)),label='Blackman') xlim(-20,20) ylim(-100,0) grid() xlabel("fT",fontsize=16) ylabel(r"$W_{hh}(f)/T$",fontsize=16) ylim(-100,0) legend(loc='upper right')fig20.pdf
et le maximum de la TF de la fenêtre de Blackman :
print(Wb.max()) --> 0.41997890901492774
La fenêtre de Blackman a des lobes secondaires très faibles mais son lobe principal est très large. Le maximum est le coefficient : sa valeur exacte est donc 0,42.