6.15. 滤波与频谱图¶
滤波、平滑和幅度谱是与原始 FFT 相邻的工作:对样本流进行平滑或带通滤波、在流式循环中不进行分配地计算幅度谱,以及将原始外设缓冲区重新解释为浮点数组。可用的工具有:
sosfilt()—— 通过级联二阶节实现的数字滤波器。spectrogram()—— 计算幅度abs(fft(...)),无中间分配。from_int16_buffer()以及其他ulab.utils中的from_*_buffer辅助函数 —— 从内置frombuffer()不支持其 dtype 的缓冲区中提取浮点数组。
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 的每一行包含一个双二阶节(biquad)的六个系数 [b0, b1, b2, a0, a1, a2]。sos 数组通常在 PC 上用 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 个样本的麦克风输入、在 DMA 驱动的数据块中累积的 ADC 样本、在一个窗口内收集的 IMU 读数。
6.15.2. 频谱图¶
spectrogram() 计算傅里叶变换的幅度。它在概念上等价于在调用 fft() 之后执行 np.sqrt(real * real + imag * imag),但将这些工作折叠为一次调用——在任何时刻都不会在 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 自身定义的 dtype(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= 缓冲区——是在高分辨率麦克风上运行流式频谱分析仪的合适模板。