6.15. Filterung und Spektrogramme

Filterung, Glättung und Betragsspektren sind die benachbarten Aufgaben zu einer rohen FFT: das Glätten oder Bandpass-Filtern eines Stroms von Abtastwerten, das Berechnen von Betragsspektren in einer Streaming-Schleife ohne Allokation und das Neuinterpretieren roher Peripheriepuffer als Float-Arrays. Die verfügbaren Werkzeuge:

  • sosfilt() – digitaler Filter, angewendet über kaskadierte Sektionen zweiter Ordnung.

  • spectrogram() – Betrag abs(fft(...)) ohne zwischenzeitliche Allokationen.

  • from_int16_buffer() und die übrigen from_*_buffer-Hilfsfunktionen von ulab.utils – ziehen ein Float-Array aus einem Puffer, dessen dtype die eingebaute frombuffer() nicht abdeckt.

6.15.1. Filterung mit sosfilt

sosfilt() wendet einen digitalen Filter mit unendlicher Impulsantwort (IIR) als Kaskade von Sektionen zweiter Ordnung (SOS) an – eine numerisch robuste Form. sos ist eine Folge von Sektionen der Länge 6; x ist die 1-D-Eingabe:

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)

Jede Zeile von sos enthält sechs Koeffizienten [b0, b1, b2, a0, a1, a2] für eine Biquad-Sektion. Das sos-Array wird üblicherweise auf einem PC mit scipy.signal.iirfilter(..., output='sos') vorberechnet und als Python-Literal in das Kamera-Skript kopiert.

Das optionale Schlüsselwort zi= überträgt den Filterzustand über Puffer hinweg. Übergib einen Anfangszustand der Form (n_sections, 2), und die Funktion gibt (y, zf) zurück – die gefilterte Ausgabe und den Endzustand –, sodass der Endzustand eines Puffers den Anfangszustand des nächsten speist:

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

Dies ist das Standardmuster für einen Streaming-Filter auf gepufferten Daten – Mikrofoneingabe, die jeweils 1024 Abtastwerte gelesen wird, ADC-Abtastwerte, die in DMA-gesteuerten Blöcken angesammelt werden, IMU-Messungen, die über ein Fenster gesammelt werden.

6.15.2. Spektrogramme

spectrogram() berechnet den Betrag der Fourier-Transformation. Konzeptionell ist das äquivalent zu np.sqrt(real * real + imag * imag) nach einem Aufruf von fft(), fasst die Arbeit aber in einem einzigen Aufruf zusammen – ohne zu irgendeinem Zeitpunkt die Zwischengrößen real * real, imag * imag, die Summe oder das explizite Betrags-Array im RAM zu halten. Das macht sie zum richtigen Werkzeug in jeder Schleife, in der Spektren wiederholt berechnet werden:

from ulab import numpy as np
from ulab import utils

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

Die Argumentform spiegelt fft() wider: ein reelles Array oder ein (real, imag)-Paar, wenn die Eingabe einen Imaginärteil hat.

Drei Schlüsselwortargumente helfen bei der Allokation:

  • scratchpad=None – ein 1-D-dichtes Float-Array der Länge 2 * len(signal), das spectrogram() als Arbeitsspeicher verwendet.

  • out=None – ein 1-D-Float-Array, in das das Ergebnis geschrieben wird.

  • log=False – wenn True, wird vor der Rückgabe der log() des Betrags genommen, eingefaltet in denselben Aufruf.

Das Streaming-Muster besteht darin, alles einmal zu allokieren und nie wieder zu allokieren:

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

Vergleiche mit der offensichtlichen, aber verschwenderischen Version:

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

Beide erzeugen dieselben Zahlen, aber die erste Version allokiert nichts innerhalb der Schleife – die Kamera behält bei jeder Iteration denselben Speicher in Verwendung, und die Schleife läuft schneller.

6.15.3. Peripheriepuffer breiter als 16 Bit

frombuffer() verarbeitet nur die dtypes, die numpy selbst definiert (uint8 / int8, uint16 / int16, float). Wenn ein Peripheriegerät 32-Bit-Ganzzahl-Abtastwerte erzeugt – ein 24- oder 32-Bit-ADC, ein hochauflösendes Mikrofon –, stellt ulab.utils explizite Konvertierungshilfen bereit:

Jede nimmt einen bytes-ähnlichen Puffer und gibt ein Float-ndarray zurück:

from ulab import utils

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

Die Funktionen akzeptieren dieselben allokationssparenden Stellschrauben wie spectrogram():

  • count= und offset=, um einen Header zu überspringen oder den Lesevorgang zu begrenzen.

  • out=, um in ein vorab allokiertes Float-Array zu schreiben.

  • byteswap=True, wenn das Peripheriegerät mit dem MCU bei der Byte-Reihenfolge nicht übereinstimmt.

Das kombinierte Muster – ein from_int32_buffer()-Aufruf direkt in einen spectrogram()-Aufruf, beide mit out=-Puffern von außerhalb der Schleife – ist die richtige Vorlage für einen Streaming-Spektrumanalysator, der auf einem hochauflösenden Mikrofon läuft.