6.15. Filtrare și spectrograme#

Filtrarea, netezirea și spectrele de magnitudine sunt sarcinile adiacente unei FFT brute: netezirea sau filtrarea trece-bandă a unui flux de eșantioane, calcularea spectrelor de magnitudine într-o buclă de tip flux fără alocare și reinterpretarea tampoanelor (buffer) brute ale perifericelor ca tablouri de numere în virgulă mobilă. Instrumentele disponibile:

  • sosfilt() – filtru digital aplicat prin secțiuni de ordinul doi în cascadă.

  • spectrogram() – magnitudine abs(fft(...)) fără alocări intermediare.

  • from_int16_buffer() și celelalte funcții ajutătoare from_*_buffer din ulab.utils – extrag un tablou de numere în virgulă mobilă dintr-un tampon (buffer) al cărui dtype nu este acoperit de funcția încorporată frombuffer().

6.15.1. Filtrare cu sosfilt#

sosfilt() aplică un filtru digital cu răspuns infinit la impuls (IIR) sub forma unei cascade de secțiuni de ordinul doi (SOS) – o formă robustă numeric. sos este o secvență de secțiuni de lungime 6; x este intrarea 1-D:

from ulab import numpy as np
from ulab import scipy as sp

x   = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
sos = [[1, 2, 3, 1, 5, 6],
       [1, 2, 3, 1, 5, 6]]
y   = sp.signal.sosfilt(sos, x)

Fiecare rând din sos conține șase coeficienți [b0, b1, b2, a0, a1, a2] pentru o secțiune biquad. Tabloul sos este de obicei precalculat pe un PC cu scipy.signal.iirfilter(..., output='sos') și copiat în scriptul camerei ca un literal Python.

Cuvântul-cheie opțional zi= transportă starea filtrului între tampoane (buffer). Transmite o stare inițială de forma (n_sections, 2) și funcția returnează (y, zf) – ieșirea filtrată și starea finală – astfel încât starea finală a unui tampon (buffer) alimentează starea inițială a celui următor:

y0, zf0 = sp.signal.sosfilt(sos, buffer0, zi=zi)
y1, zf1 = sp.signal.sosfilt(sos, buffer1, zi=zf0)
# ...

Acesta este modelul standard pentru un filtru de tip flux aplicat datelor tamponate – intrare de microfon citită câte 1024 de eșantioane odată, eșantioane ADC acumulate în fragmente acționate prin DMA, citiri IMU colectate pe o fereastră.

6.15.2. Spectrograme#

spectrogram() calculează magnitudinea transformatei Fourier. Este conceptual echivalentă cu np.sqrt(real * real + imag * imag) după un apel la fft(), dar comprimă munca într-un singur apel – fără a păstra în RAM, în niciun moment, valorile intermediare real * real, imag * imag, suma sau tabloul de magnitudine explicit. Acest lucru o face instrumentul potrivit în orice buclă în care spectrele sunt calculate în mod repetat:

from ulab import numpy as np
from ulab import utils

x        = np.linspace(0, 10, num=1024)
spectrum = utils.spectrogram(x)

Forma argumentelor reflectă fft(): un singur tablou real sau o pereche (real, imag) atunci când intrarea are o parte imaginară.

Trei argumente de tip cuvânt-cheie ajută la alocare:

  • scratchpad=None – un tablou dens 1-D de numere în virgulă mobilă, de lungime 2 * len(signal), pe care spectrogram() îl folosește drept spațiu de lucru.

  • out=None – un tablou 1-D de numere în virgulă mobilă în care se scrie rezultatul.

  • log=False – când este True, se aplică log() magnitudinii înainte de returnare, comprimat în același apel.

Modelul de tip flux constă în a aloca totul o singură dată și a nu mai aloca niciodată:

from ulab import numpy as np
from ulab import utils

N = 1024
scratch = np.zeros(2 * N)
out     = np.zeros(N)

while True:
    signal = read_samples(N)
    utils.spectrogram(signal, out=out, scratchpad=scratch,
                      log=True)
    # out now holds the log-magnitude spectrum for this window ...

Comparați cu versiunea evidentă, dar risipitoare:

while True:
    signal   = read_samples(N)
    spectrum = np.log(utils.spectrogram(signal))   # two allocations

Ambele produc aceleași numere, dar prima versiune nu alocă nimic în interiorul buclei – camera păstrează aceeași memorie în uz la fiecare iterație, iar bucla rulează mai repede.

6.15.3. Tampoane de periferice mai late de 16 biți#

frombuffer() gestionează doar dtype-urile pe care numpy însuși le definește (uint8 / int8, uint16 / int16, float). Când un periferic produce eșantioane de numere întregi pe 32 de biți – un ADC pe 24 sau 32 de biți, un microfon de înaltă rezoluție – ulab.utils expune funcții ajutătoare explicite de conversie:

Fiecare primește un tampon (buffer) de tip bytes și returnează un ndarray de numere în virgulă mobilă:

from ulab import utils

buf = bytearray([1, 1, 0, 0, 0, 0, 0, 255])
utils.from_uint32_buffer(buf)
# array([257.0, 4278190080.0])

Funcțiile acceptă aceleași opțiuni de economisire a alocării ca spectrogram():

  • count= și offset= pentru a sări un antet sau a limita citirea.

  • out= pentru a scrie într-un tablou de numere în virgulă mobilă prealocat.

  • byteswap=True când perifericul nu este de acord cu MCU asupra ordinii octeților.

Modelul combinat – un apel from_int32_buffer() direct într-un apel spectrogram(), ambele cu tampoane (buffer) out= din afara buclei – este șablonul potrivit pentru un analizor de spectru de tip flux care rulează pe un microfon de înaltă rezoluție.