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.