6.15. 滤波与频谱图

滤波、平滑和幅度谱是与原始 FFT 相邻的工作:对样本流进行平滑或带通滤波、在流式循环中不进行分配地计算幅度谱,以及将原始外设缓冲区重新解释为浮点数组。可用的工具有:

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 * realimag * 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 / int8uint16 / int16float)。当外设产生 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= 缓冲区——是在高分辨率麦克风上运行流式频谱分析仪的合适模板。