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

Фільтрація, згладжування та амплітудні спектри є суміжними задачами до сирого FFT: згладжування або смугова фільтрація потоку зразків, обчислення амплітудних спектрів у потоковому циклі без виділення пам’яті, та інтерпретація сирих буферів периферійних пристроїв як масивів з рухомою крапкою. Наявні інструменти:

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

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

  • from_int16_buffer() та інші допоміжні функції ulab.utils from_*_buffer – отримання масиву з рухомою крапкою з буфера, тип даних якого не підтримує вбудована функція 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= поза циклом – є правильним шаблоном для потокового аналізатора спектра на мікрофоні з високою роздільною здатністю.