6.15. 濾波與頻譜圖¶
濾波、平滑化與幅值頻譜是與原始 FFT 相鄰的工作:對一串樣本進行平滑化或帶通濾波、在串流迴圈中計算幅值頻譜而不進行配置,以及將原始周邊裝置緩衝區重新解讀為浮點陣列。可用的工具如下:
sosfilt(),透過串接的二階區段套用的數位濾波器。spectrogram(),計算幅值abs(fft(...))且不進行中間配置。from_int16_buffer()與其他ulab.utils的from_*_buffer輔助函式,能從內建的frombuffer()未涵蓋之 dtype 的緩衝區中取出浮點陣列。
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 * 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= 緩衝區,正是在高解析度麥克風上執行串流頻譜分析儀的正確範本。