6.15. Фильтрация и спектрограммы

Фильтрация, сглаживание и амплитудные спектры – это задачи, смежные с сырым FFT: сглаживание или полосовая фильтрация потока отсчётов, вычисление амплитудных спектров в потоковом цикле без выделения памяти и переинтерпретация сырых буферов периферии как массивов чисел с плавающей точкой. Доступные инструменты:

  • sosfilt() – цифровой фильтр, применяемый через каскад секций второго порядка.

  • spectrogram() – амплитуда abs(fft(...)) без промежуточных выделений памяти.

  • from_int16_buffer() и другие вспомогательные функции from_*_buffer из ulab.utils – извлекают массив чисел с плавающей точкой из буфера, тип данных которого не охватывает встроенная frombuffer().

6.15.1. Фильтрация с помощью sosfilt

sosfilt() применяет цифровой фильтр с бесконечной импульсной характеристикой (IIR) в виде каскада секций второго порядка (SOS) – численно устойчивой формы. sos – это последовательность секций длиной 6; x – одномерный вход:

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)

Каждая строка sos содержит шесть коэффициентов [b0, b1, b2, a0, a1, a2] для одной биквадратной секции. Массив sos обычно предварительно вычисляется на ПК с помощью scipy.signal.iirfilter(..., output='sos') и копируется в скрипт камеры в виде литерала Python.

Необязательный именованный аргумент zi= переносит состояние фильтра между буферами. Передайте начальное состояние формы (n_sections, 2), и функция вернёт (y, zf) – отфильтрованный выход и финальное состояние, – так что финальное состояние одного буфера передаётся в качестве начального состояния следующего:

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

Это стандартная схема для потокового фильтра по буферизованным данным – вход с микрофона, считываемый по 1024 отсчёта за раз, отсчёты ADC, накапливаемые блоками через DMA, показания IMU, собираемые за окно.

6.15.2. Спектрограммы

spectrogram() вычисляет амплитуду преобразования Фурье. Концептуально это эквивалентно np.sqrt(real * real + imag * imag) после вызова fft(), но сворачивает всю работу в один вызов – без хранения в RAM в какой-либо момент промежуточных real * real, imag * imag, их суммы или явного массива амплитуд. Это делает её правильным инструментом в любом цикле, где спектры вычисляются многократно:

from ulab import numpy as np
from ulab import utils

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

Форма аргументов повторяет fft(): один действительный массив или пара (real, imag), когда вход имеет мнимую часть.

Три именованных аргумента помогают с выделением памяти:

  • scratchpad=None – одномерный плотный массив чисел с плавающей точкой длиной 2 * len(signal), который spectrogram() использует как рабочее пространство.

  • out=None – одномерный массив чисел с плавающей точкой, в который записывается результат.

  • log=False – при значении True перед возвратом берётся log() от амплитуды, свёрнутый в тот же вызов.

Потоковая схема состоит в том, чтобы выделить всё один раз и больше никогда не выделять:

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

Сравните с очевидной, но расточительной версией:

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

Обе версии дают одни и те же числа, но первая ничего не выделяет внутри цикла – камера использует одну и ту же память на каждой итерации, и цикл работает быстрее.

6.15.3. Буферы периферии шире 16 бит

frombuffer() обрабатывает только те типы данных, которые определяет сам numpy (uint8 / int8, uint16 / int16, float). Когда периферийное устройство выдаёт 32-битные целочисленные отсчёты – 24- или 32-битный ADC, микрофон высокого разрешения – ulab.utils предоставляет явные вспомогательные функции преобразования:

Каждая принимает байтоподобный буфер и возвращает 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])

Функции принимают те же параметры экономии памяти, что и spectrogram():

  • count= и offset= для пропуска заголовка или ограничения чтения.

  • out= для записи в заранее выделенный массив чисел с плавающей точкой.

  • byteswap=True, когда периферийное устройство расходится с MCU в порядке байтов.

Объединённая схема – один вызов from_int32_buffer() прямо в один вызов spectrogram(), оба с буферами out=, заданными вне цикла, – это правильный шаблон для потокового спектроанализатора, работающего на микрофоне высокого разрешения.