6.15. Filtrowanie i spektrogramy

Filtrowanie, wygładzanie i widma modułu to zadania sąsiadujące z surowym FFT: wygładzanie lub filtrowanie pasmowoprzepustowe strumienia próbek, obliczanie widm modułu w pętli strumieniowej bez alokacji oraz reinterpretowanie surowych buforów urządzeń peryferyjnych jako tablic typu float. Dostępne narzędzia:

  • sosfilt() – filtr cyfrowy stosowany za pomocą kaskadowych sekcji drugiego rzędu.

  • spectrogram() – moduł abs(fft(...)) bez pośrednich alokacji.

  • from_int16_buffer() oraz pozostałe funkcje pomocnicze from_*_buffer z ulab.utils – wydobywają tablicę float z bufora, którego typu danych nie obsługuje wbudowana frombuffer().

6.15.1. Filtrowanie za pomocą sosfilt

sosfilt() stosuje cyfrowy filtr o nieskończonej odpowiedzi impulsowej (IIR) w postaci kaskady sekcji drugiego rzędu (SOS) – formy odpornej numerycznie. sos jest sekwencją sekcji o długości 6; x jest jednowymiarowym wejściem:

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)

Każdy wiersz sos zawiera sześć współczynników [b0, b1, b2, a0, a1, a2] dla jednej sekcji biquad. Tablica sos jest zazwyczaj obliczana wstępnie na komputerze za pomocą scipy.signal.iirfilter(..., output='sos') i kopiowana do skryptu kamery jako literał Pythona.

Opcjonalny argument kluczowy zi= przenosi stan filtra między buforami. Przekaż stan początkowy o kształcie (n_sections, 2), a funkcja zwróci (y, zf) – przefiltrowane wyjście oraz stan końcowy – dzięki czemu stan końcowy jednego bufora staje się stanem początkowym następnego:

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

Jest to standardowy wzorzec dla filtra strumieniowego działającego na buforowanych danych – wejście z mikrofonu odczytywane po 1024 próbki naraz, próbki ADC gromadzone w porcjach sterowanych przez DMA, odczyty IMU zbierane w obrębie okna.

6.15.2. Spektrogramy

spectrogram() oblicza moduł transformaty Fouriera. Jest koncepcyjnie równoważny np.sqrt(real * real + imag * imag) po wywołaniu fft(), ale łączy całą pracę w jedno wywołanie – bez przechowywania w pamięci RAM pośrednich wartości real * real, imag * imag, sumy ani jawnej tablicy modułu w żadnym momencie. To czyni go właściwym narzędziem w każdej pętli, gdzie widma obliczane są wielokrotnie:

from ulab import numpy as np
from ulab import utils

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

Forma argumentu odzwierciedla fft(): jedna tablica rzeczywista lub para (real, imag), gdy wejście ma część urojoną.

Trzy argumenty kluczowe pomagają w zarządzaniu alokacją:

  • scratchpad=None – jednowymiarowa gęsta tablica float o długości 2 * len(signal), której spectrogram() używa jako przestrzeni roboczej.

  • out=None – jednowymiarowa tablica float, do której zapisywany jest wynik.

  • log=False – gdy ustawione na True, oblicza log() modułu przed zwróceniem, w obrębie tego samego wywołania.

Wzorzec strumieniowy polega na jednorazowym przydzieleniu wszystkiego i niealokowaniu nigdy więcej:

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

Dla porównania oczywista, lecz rozrzutna wersja:

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

Obie dają te same liczby, ale pierwsza wersja niczego nie alokuje wewnątrz pętli – kamera utrzymuje tę samą pamięć w użyciu w każdej iteracji, a pętla działa szybciej.

6.15.3. Bufory urządzeń peryferyjnych szersze niż 16 bitów

frombuffer() obsługuje wyłącznie typy danych zdefiniowane przez sam numpy (uint8 / int8, uint16 / int16, float). Gdy urządzenie peryferyjne produkuje 32-bitowe próbki całkowite – 24- lub 32-bitowy ADC, mikrofon wysokiej rozdzielczości – ulab.utils udostępnia jawne funkcje pomocnicze do konwersji:

Każda przyjmuje bufor podobny do bytes i zwraca tablicę float 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])

Funkcje akceptują te same pokrętła oszczędzające alokację co spectrogram():

  • count= i offset= umożliwiają pominięcie nagłówka lub ograniczenie odczytu.

  • out= zapisuje wynik do wstępnie przydzielonej tablicy float.

  • byteswap=True gdy urządzenie peryferyjne nie zgadza się z MCU co do kolejności bajtów.

Połączony wzorzec – jedno wywołanie from_int32_buffer() prowadzące prosto do jednego wywołania spectrogram(), oba z buforami out= pochodzącymi spoza pętli – to właściwy szablon dla strumieniowego analizatora widma działającego na mikrofonie wysokiej rozdzielczości.