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.