6.15. Filtrering och spektrogram

Filtrering, utjämning och magnitudspektra är de närliggande uppgifterna till en rå FFT: att jämna ut eller bandpassfiltrera en ström av sampel, att beräkna magnitudspektra i en strömmande loop utan att allokera, och att omtolka råa buffertar från kringutrustning som flyttalsarrayer. De tillgängliga verktygen:

  • sosfilt() – digitalt filter applicerat via kaskadkopplade andra ordningens sektioner.

  • spectrogram() – magnitud abs(fft(...)) utan mellanliggande allokeringar.

  • from_int16_buffer() och de övriga ulab.utils-hjälparna from_*_buffer – hämtar ut en flyttalsarray ur en buffert vars dtype den inbyggda frombuffer() inte täcker.

6.15.1. Filtrering med sosfilt

sosfilt() applicerar ett digitalt oändligt impulssvars-filter (IIR) som en kaskad av andra ordningens sektioner (SOS) – en numeriskt robust form. sos är en sekvens av sektioner med längd 6; x är den 1-dimensionella indatan:

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)

Varje rad i sos innehåller sex koefficienter [b0, b1, b2, a0, a1, a2] för en biquad-sektion. sos-arrayen förberäknas vanligtvis på en PC med scipy.signal.iirfilter(..., output='sos') och kopieras in i kameraskriptet som en Python-literal.

Det valfria nyckelordet zi= bär filtertillståndet över buffertgränserna. Skicka in ett initialt tillstånd med formen (n_sections, 2) så returnerar funktionen (y, zf) – den filtrerade utdatan och det slutliga tillståndet – så att en bufferts sluttillstånd matar nästa bufferts initialtillstånd:

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

Detta är standardmönstret för ett strömmande filter på buffrad data – mikrofoningång som läses 1024 sampel i taget, ADC-sampel som ackumuleras i DMA-drivna delar, IMU-avläsningar som samlas in över ett fönster.

6.15.2. Spektrogram

spectrogram() beräknar magnituden av Fouriertransformen. Den är begreppsmässigt likvärdig med np.sqrt(real * real + imag * imag) efter ett anrop till fft(), men väver in arbetet i ett enda anrop – utan att vid någon tidpunkt hålla det mellanliggande real * real, imag * imag, summan eller den explicita magnitudarrayen i RAM. Det gör den till rätt verktyg i varje loop där spektra beräknas upprepade gånger:

from ulab import numpy as np
from ulab import utils

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

Argumentformen speglar fft(): en reell array, eller ett par (real, imag) när indatan har en imaginärdel.

Tre nyckelordsargument hjälper till med allokering:

  • scratchpad=None – en 1-dimensionell tät flyttalsarray med längden 2 * len(signal) som spectrogram() använder som arbetsutrymme.

  • out=None – en 1-dimensionell flyttalsarray att skriva resultatet i.

  • log=False – när den är True tas log() av magnituden innan retur, invävt i samma anrop.

Det strömmande mönstret är att allokera allt en gång och aldrig allokera igen:

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

Jämför med den uppenbara men slösaktiga versionen:

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

Båda producerar samma tal, men den första versionen allokerar ingenting inuti loopen – kameran håller samma minne i bruk vid varje iteration, och loopen körs snabbare.

6.15.3. Buffertar från kringutrustning bredare än 16 bitar

frombuffer() hanterar endast de dtype:r som numpy självt definierar (uint8 / int8, uint16 / int16, float). När en kringutrustning producerar 32-bitars heltalssampel – en 24- eller 32-bitars ADC, en högupplöst mikrofon – exponerar ulab.utils explicita konverteringshjälpare:

Var och en tar en bytesliknande buffert och returnerar en flyttals-ndarray

from ulab import utils

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

Funktionerna accepterar samma allokeringsbesparande reglage som spectrogram():

  • count= och offset= för att hoppa över en header eller begränsa läsningen.

  • out= för att skriva in i en förallokerad flyttalsarray.

  • byteswap=True när kringutrustningen inte är överens med MCU:n om byteordning.

Det kombinerade mönstret – ett anrop till from_int32_buffer() direkt in i ett anrop till spectrogram(), båda med out=-buffertar från utanför loopen – är den rätta mallen för en strömmande spektrumanalysator som körs på en högupplöst mikrofon.