6.15. Filteren en spectrogrammen

Filteren, gladstrijken en magnitudespectra zijn de aangrenzende taken bij een ruwe FFT: het gladstrijken of bandpas-filteren van een stroom samples, het berekenen van magnitudespectra in een streaming-lus zonder allocatie, en het herinterpreteren van ruwe randapparaatbuffers als float-arrays. De beschikbare hulpmiddelen:

  • sosfilt() – digitaal filter dat wordt toegepast via gecascadeerde tweede-orde-secties.

  • spectrogram() – magnitude abs(fft(...)) zonder tussenliggende allocaties.

  • from_int16_buffer() en de overige ulab.utils from_*_buffer-hulpfuncties – haal een float-array uit een buffer waarvan het dtype niet door de ingebouwde frombuffer() wordt gedekt.

6.15.1. Filteren met sosfilt

sosfilt() past een digitaal infinite impulse response (IIR) filter toe als een cascade van second-order sections (SOS) – een numeriek robuuste vorm. sos is een reeks secties met lengte 6; x is de eendimensionale invoer:

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)

Elke rij van sos bevat zes coëfficiënten [b0, b1, b2, a0, a1, a2] voor één biquad-sectie. De sos-array wordt doorgaans vooraf op een pc berekend met scipy.signal.iirfilter(..., output='sos') en als Python-literal in het camerascript gekopieerd.

Het optionele zi=-trefwoord draagt filterstatus over tussen buffers. Geef een begintoestand met vorm (n_sections, 2) mee, en de functie geeft (y, zf) terug – de gefilterde uitvoer en de eindtoestand – zodat de eindtoestand van de ene buffer de begintoestand van de volgende voedt:

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

Dit is het standaardpatroon voor een streaming-filter op gebufferde data – microfooninvoer die per 1024 samples wordt ingelezen, ADC-samples die in DMA-aangestuurde brokken worden verzameld, IMU-metingen die over een venster worden verzameld.

6.15.2. Spectrogrammen

spectrogram() berekent de magnitude van de Fouriertransformatie. Het is conceptueel equivalent aan np.sqrt(real * real + imag * imag) na een aanroep van fft(), maar bundelt het werk in één aanroep – zonder op enig moment de tussenliggende real * real, imag * imag, de som of de expliciete magnitude-array in het RAM aan te houden. Daarmee is het het juiste hulpmiddel in elke lus waarin spectra herhaaldelijk worden berekend:

from ulab import numpy as np
from ulab import utils

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

De argumentvorm weerspiegelt fft(): één reële array, of een (real, imag)-paar wanneer de invoer een imaginair deel heeft.

Drie trefwoordargumenten helpen bij allocatie:

  • scratchpad=None – een eendimensionale dichte float-array met lengte 2 * len(signal) die spectrogram() als werkruimte gebruikt.

  • out=None – een eendimensionale float-array om het resultaat in te schrijven.

  • log=False – wanneer True, wordt vóór het teruggeven de log() van de magnitude genomen, gebundeld in dezelfde aanroep.

Het streaming-patroon is om alles één keer te alloceren en daarna nooit meer:

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

Vergelijk met de voor de hand liggende maar verspillende versie:

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

Beide leveren dezelfde getallen op, maar de eerste versie alloceert niets binnen de lus – de camera houdt elke iteratie hetzelfde geheugen in gebruik, en de lus draait sneller.

6.15.3. Randapparaatbuffers breder dan 16 bit

frombuffer() verwerkt alleen de dtypes die numpy zelf definieert (uint8 / int8, uint16 / int16, float). Wanneer een randapparaat 32-bits integer-samples produceert – een 24- of 32-bits ADC, een hogeresolutiemicrofoon – biedt ulab.utils expliciete conversiehulpfuncties:

Elk neemt een bytes-achtige buffer en geeft een float ndarray terug:

from ulab import utils

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

De functies accepteren dezelfde allocatiebesparende knoppen als spectrogram():

  • count= en offset= om een header over te slaan of het inlezen te beperken.

  • out= om in een vooraf gealloceerde float-array te schrijven.

  • byteswap=True wanneer het randapparaat het oneens is met de MCU over de byte-volgorde.

Het gecombineerde patroon – één from_int32_buffer()-aanroep rechtstreeks in één spectrogram()-aanroep, beide met out=-buffers van buiten de lus – is het juiste sjabloon voor een streaming-spectrumanalysator die op een hogeresolutiemicrofoon draait.