6.15. フィルタリングとスペクトログラム

フィルタリング、スムージング、振幅スペクトルは、生のFFTに隣接する作業です。サンプルのストリームをスムージングまたはバンドパスフィルタリングし、割り当てなしでストリーミングループ内で振幅スペクトルを計算し、生のペリフェラルバッファをfloat配列として再解釈します。利用できるツールは次のとおりです:

  • sosfilt() -- 縦続接続した2次セクションを介して適用するデジタルフィルタ。

  • spectrogram() -- 中間的な割り当てなしの振幅 abs(fft(...))

  • from_int16_buffer() とその他の ulab.utilsfrom_*_buffer ヘルパー -- 組み込みの frombuffer() が対応していないdtypeのバッファからfloat配列を取り出します。

6.15.1. sosfiltによるフィルタリング

sosfilt() は、デジタル無限インパルス応答(IIR)フィルタを2次セクション(SOS)の縦続接続として適用します。これは数値的に堅牢な形式です。sos は長さ6のセクションのシーケンス、x は1次元の入力です:

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 の各行は、1つのバイカッドセクションに対する6個の係数 [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サンプル読み込む場合、ADCサンプルをDMA駆動のチャンクに蓄積する場合、IMUの読み取り値をウィンドウにわたって収集する場合などです。

6.15.2. スペクトログラム

spectrogram() はフーリエ変換の振幅を計算します。概念的には fft() を呼び出した後の np.sqrt(real * real + imag * imag) と同等ですが、その処理を1回の呼び出しにまとめます。その際、中間の real * realimag * imag、その和、または明示的な振幅配列のいずれもRAMに保持しません。これにより、スペクトルを繰り返し計算するあらゆるループで適切なツールとなります:

from ulab import numpy as np
from ulab import utils

x        = np.linspace(0, 10, num=1024)
spectrum = utils.spectrogram(x)

引数の形式は fft() と同じです。1つの実数配列、または入力に虚部がある場合は (real, imag) のペアを渡します。

3つのキーワード引数が割り当てに役立ちます:

  • scratchpad=None -- spectrogram() が作業領域として使用する、長さ 2 * len(signal) の1次元密float配列。

  • out=None -- 結果を書き込む先の1次元float配列。

  • 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 は明示的な変換ヘルパーを公開します:

それぞれbytesライクなバッファを受け取り、float 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= -- 事前に割り当てられたfloat配列に書き込みます。

  • byteswap=True -- ペリフェラルがMCUとバイト順で一致しない場合に使用します。

組み合わせたパターン -- 1回の from_int32_buffer() 呼び出しをそのまま1回の spectrogram() 呼び出しに渡し、どちらもループの外から取得した out= バッファを使う -- は、高分解能マイク上で動作するストリーミングスペクトラムアナライザに適したテンプレートです。