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()– Betragabs(fft(...))ohne zwischenzeitliche Allokationen.from_int16_buffer()und die übrigenfrom_*_buffer-Hilfsfunktionen vonulab.utils– ziehen ein Float-Array aus einem Puffer, dessen dtype die eingebautefrombuffer()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änge2 * len(signal), dasspectrogram()als Arbeitsspeicher verwendet.out=None– ein 1-D-Float-Array, in das das Ergebnis geschrieben wird.log=False– wennTrue, wird vor der Rückgabe derlog()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=undoffset=, 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.