6.15. 濾波與頻譜圖

濾波、平滑化與幅值頻譜是與原始 FFT 相鄰的工作:對一串樣本進行平滑化或帶通濾波、在串流迴圈中計算幅值頻譜而不進行配置,以及將原始周邊裝置緩衝區重新解讀為浮點陣列。可用的工具如下:

6.15.1. 使用 sosfilt 濾波

sosfilt()二階區段(second-order sections,SOS)串接的形式套用數位無限脈衝響應(infinite impulse response,IIR)濾波器,這是一種數值上穩健的形式。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= 緩衝區,正是在高解析度麥克風上執行串流頻譜分析儀的正確範本。