6.15. Filtrování a spektrogramy

Filtrování, vyhlazování a spektra velikostí jsou úlohy příbuzné surové FFT: vyhlazování nebo pásmová propust proudu vzorků, výpočet spekter velikostí ve streamovací smyčce bez alokace a nová interpretace surových bufferů periferií jako polí typu float. Dostupné nástroje:

  • sosfilt() – digitální filtr aplikovaný prostřednictvím kaskádovaných sekcí druhého řádu.

  • spectrogram() – velikost abs(fft(...)) bez mezilehlých alokací.

  • from_int16_buffer() a ostatní pomocné funkce ulab.utils typu from_*_buffer – získají pole typu float z bufferu, jehož dtype vestavěná funkce frombuffer() nepokrývá.

6.15.1. Filtrování pomocí sosfilt

sosfilt() aplikuje digitální filtr s nekonečnou impulzní odezvou (IIR) jako kaskádu sekcí druhého řádu (SOS) – numericky robustní formu. sos je sekvence sekcí o délce 6; x je jednorozměrný vstup:

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ždý řádek pole sos obsahuje šest koeficientů [b0, b1, b2, a0, a1, a2] pro jednu biquad sekci. Pole sos se obvykle předem vypočítá na PC pomocí scipy.signal.iirfilter(..., output='sos') a zkopíruje do skriptu kamery jako Python literál.

Volitelný klíčový argument zi= přenáší stav filtru mezi buffery. Předejte počáteční stav tvaru (n_sections, 2) a funkce vrátí (y, zf) – filtrovaný výstup a koncový stav – takže koncový stav jednoho bufferu poslouží jako počáteční stav dalšího:

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

Toto je standardní vzor pro streamovací filtr nad bufferovanými daty – vstup z mikrofonu čtený po 1024 vzorcích, vzorky z ADC akumulované v blocích řízených DMA, hodnoty z IMU sbírané přes okno.

6.15.2. Spektrogramy

spectrogram() vypočítá velikost (magnitudu) Fourierovy transformace. Koncepčně je ekvivalentní výrazu np.sqrt(real * real + imag * imag) po volání fft(), ale skládá tuto práci do jediného volání – aniž by v kterémkoli okamžiku držel v RAM mezivýsledky real * real, imag * imag, součet nebo explicitní pole velikostí. To z ní činí správný nástroj v každé smyčce, kde se spektra počítají opakovaně:

from ulab import numpy as np
from ulab import utils

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

Forma argumentů odpovídá fft(): jedno reálné pole, nebo dvojice (real, imag), když má vstup imaginární část.

Tři klíčové argumenty pomáhají s alokací:

  • scratchpad=None – jednorozměrné husté pole typu float o délce 2 * len(signal), které spectrogram() používá jako pracovní prostor.

  • out=None – jednorozměrné pole typu float, do kterého se zapíše výsledek.

  • log=False – když je True, před vrácením se vezme log() velikosti, vložená do téhož volání.

Streamovací vzor spočívá v tom, že se vše alokuje jednou a už nikdy znovu:

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

Porovnejte s evidentní, ale plýtvavou verzí:

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

Obě produkují stejná čísla, ale první verze uvnitř smyčky nealokuje nic – kamera používá v každé iteraci stejnou paměť a smyčka běží rychleji.

6.15.3. Buffery periferií širší než 16 bitů

frombuffer() zvládá pouze dtypy, které definuje samotný numpy (uint8 / int8, uint16 / int16, float). Když periferie produkuje 32bitové celočíselné vzorky – 24- nebo 32bitový ADC, mikrofon s vysokým rozlišením – ulab.utils poskytuje explicitní pomocné funkce pro převod:

Každá přijímá buffer podobný bytes a vrací ndarray typu 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])

Tyto funkce přijímají stejné parametry pro úsporu alokací jako spectrogram():

  • count= a offset= pro přeskočení hlavičky nebo omezení čtení.

  • out= pro zápis do předem alokovaného pole typu float.

  • byteswap=True, když se periferie s MCU neshodují v pořadí bytů.

Kombinovaný vzor – jedno volání from_int32_buffer() přímo do jednoho volání spectrogram(), obě s buffery out= vytvořenými mimo smyčku – je správnou šablonou pro streamovací spektrální analyzátor běžící na mikrofonu s vysokým rozlišením.