6.15. Filtraggio e spettrogrammi#

Il filtraggio, lo smoothing e gli spettri di magnitudine sono i compiti adiacenti a una FFT grezza: smoothing o filtraggio passa-banda di un flusso di campioni, calcolo di spettri di magnitudine in un loop in streaming senza allocare, e re-interpretazione di buffer grezzi delle periferiche come array di float. Gli strumenti disponibili:

  • sosfilt() – filtro digitale applicato tramite sezioni del secondo ordine in cascata.

  • spectrogram() – magnitudine abs(fft(...)) senza allocazioni intermedie.

  • from_int16_buffer() e le altre funzioni di supporto from_*_buffer di ulab.utils – estraggono un array di float da un buffer il cui dtype non è coperto dalla frombuffer() integrata.

6.15.1. Filtraggio con sosfilt#

sosfilt() applica un filtro digitale a risposta impulsiva infinita (IIR) come una cascata di sezioni del secondo ordine (SOS) – una forma numericamente robusta. sos è una sequenza di sezioni di lunghezza 6; x è l’input monodimensionale:

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)

Ogni riga di sos contiene sei coefficienti [b0, b1, b2, a0, a1, a2] per una sezione biquad. L’array sos viene di solito pre-calcolato su un PC con scipy.signal.iirfilter(..., output='sos') e copiato nello script della camera come letterale Python.

La parola chiave opzionale zi= trasporta lo stato del filtro tra i buffer. Passa uno stato iniziale di forma (n_sections, 2) e la funzione restituisce (y, zf) – l’output filtrato e lo stato finale – così lo stato finale di un buffer alimenta lo stato iniziale del successivo:

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

Questo è lo schema standard per un filtro in streaming su dati bufferizzati – input da microfono letto 1024 campioni alla volta, campioni ADC accumulati in blocchi pilotati da DMA, letture IMU raccolte su una finestra.

6.15.2. Spettrogrammi#

spectrogram() calcola la magnitudine della trasformata di Fourier. È concettualmente equivalente a np.sqrt(real * real + imag * imag) dopo una chiamata a fft(), ma riunisce il lavoro in un’unica chiamata – senza mai mantenere in RAM in alcun momento l’intermedio real * real, imag * imag, la somma, o l’array di magnitudine esplicito. Questo lo rende lo strumento giusto in qualsiasi loop in cui gli spettri vengono calcolati ripetutamente:

from ulab import numpy as np
from ulab import utils

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

La forma degli argomenti rispecchia fft(): un array reale, oppure una coppia (real, imag) quando l’input ha una parte immaginaria.

Tre argomenti per parola chiave aiutano con l’allocazione:

  • scratchpad=None – un array di float denso monodimensionale di lunghezza 2 * len(signal) che spectrogram() usa come spazio di lavoro.

  • out=None – un array di float monodimensionale in cui scrivere il risultato.

  • log=False – quando è True, calcola log() della magnitudine prima di restituirla, riunito nella stessa chiamata.

Lo schema in streaming consiste nell’allocare tutto una volta e non allocare mai più:

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 ...

Confronta con la versione ovvia ma dispendiosa:

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

Entrambe producono gli stessi numeri, ma la prima versione non alloca nulla all’interno del loop – la camera mantiene in uso la stessa memoria a ogni iterazione, e il loop gira più velocemente.

6.15.3. Buffer di periferiche più larghi di 16 bit#

frombuffer() gestisce solo i dtype che numpy stesso definisce (uint8 / int8, uint16 / int16, float). Quando una periferica produce campioni interi a 32 bit – un ADC a 24 o 32 bit, un microfono ad alta risoluzione – ulab.utils espone funzioni di conversione esplicite:

Ciascuna accetta un buffer di tipo bytes e restituisce un ndarray di float:

from ulab import utils

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

Le funzioni accettano le stesse manopole per il risparmio di allocazione di spectrogram():

  • count= e offset= per saltare un’intestazione o limitare la lettura.

  • out= per scrivere in un array di float pre-allocato.

  • byteswap=True quando la periferica è in disaccordo con l’MCU sull’ordine dei byte.

Lo schema combinato – una chiamata a from_int32_buffer() direttamente in una chiamata a spectrogram(), entrambe con buffer out= provenienti dall’esterno del loop – è il modello giusto per un analizzatore di spettro in streaming in esecuzione su un microfono ad alta risoluzione.