モモノキ&ナノネと一緒にPythonでFFTの使い方を覚えよう(5)
極大値と極小値の取得方法を覚えてピークの自動検出に挑戦しよう
高速フーリエ変換(FFT)練習の続きを始めるよ。今回はFFT分析結果からピークの周波数を自動で検出してみよう。
グラフを見れば、周波数はだいだいわかるんじゃない?
でも、FFT分析結果のグラフにピークがマーキングできたりしたらチョットうれしくない?
そうかなぁ?
簡易的な自動ピーク検出だけど、先に結果を出してみるね。たとえばこんな感じでできるよ。
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 512 # サンプル数
dt = 0.01 # サンプリング間隔
fq1, fq2 = 3, 8 # 周波数
amp1, amp2 = 1.5, 1 # 振幅
t = np.arange(0, N*dt, dt) # 時間軸
# 時間信号作成
f = amp1*np.sin(2*np.pi*fq1*t)
f += amp2*np.sin(2*np.pi*fq2*t)
f += np.random.randn(N)*0.5 # ノイズ
freq = np.linspace(0, 1.0/dt, N) # 周波数軸
# 高速フーリエ変換
F = np.fft.fft(f)
F_abs = np.abs(F) # 複素数 ->絶対値に変換
# 振幅を元の信号のスケールに揃える
F_abs = F_abs / (N/2) # 交流成分
F_abs[0] = F_abs[0] / 2 # 直流成分
# グラフ表示(時間軸)
plt.figure(figsize=(8,6))
plt.subplot(211)
plt.plot(t, f)
plt.xlabel('Time(s)')
plt.ylabel('Signal')
# FFTデータからピークを自動検出
maximal_idx = signal.argrelmax(F_abs, order=1)[0] # ピーク(極大値)のインデックス取得
# ピーク検出感度調整もどき、後半側(ナイキスト超)と閾値より小さい振幅ピークを除外
peak_cut = 0.3 # ピーク閾値
maximal_idx = maximal_idx[(F_abs[maximal_idx] > peak_cut) & (maximal_idx <= N/2)]
# グラフ表示(周波数軸)
plt.subplot(212)
plt.xlabel('Frequency(Hz)')
plt.ylabel('Amplitude')
plt.axis([0,1.0/dt/2,0,max(F_abs)*1.5])
plt.plot(freq, F_abs)
plt.plot(freq[maximal_idx], F_abs[maximal_idx],'ro')
# グラフにピークの周波数をテキストで表示
for i in range(len(maximal_idx)):
plt.annotate('{0:.0f}(Hz)'.format(np.round(freq[maximal_idx[i]])),
xy=(freq[maximal_idx[i]], F_abs[maximal_idx[i]]),
xytext=(10, 20),
textcoords='offset points',
arrowprops=dict(arrowstyle="->",connectionstyle="arc3,rad=.2")
)
plt.subplots_adjust(hspace=0.4)
plt.show()
print('peak', freq[maximal_idx])
同じコードを使って信号をもう少し複雑すると、こんな感じ。
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 512 # サンプル数
dt = 0.01 # サンプリング間隔
fq1, fq2 , fq3 = 12, 24, 33 # 周波数
amp1, amp2, amp3 = 1.5, 1, 2 # 振幅
t = np.arange(0, N*dt, dt) # 時間軸
# 時間信号作成
f = amp1*np.sin(2*np.pi*fq1*t)
f += amp2*np.sin(2*np.pi*fq2*t)
f += amp3*np.sin(2*np.pi*fq3*t)
f += np.random.randn(N)*0.3 # ノイズ
freq = np.linspace(0, 1.0/dt, N) # 周波数軸
# 高速フーリエ変換
F = np.fft.fft(f)
F_abs = np.abs(F) # 複素数 ->絶対値に変換
# 振幅を元の信号のスケールに揃える
F_abs = F_abs / (N/2) # 交流成分
F_abs[0] = F_abs[0] / 2 # 直流成分
# グラフ表示(時間軸)
plt.figure(figsize=(8,6))
plt.subplot(211)
plt.plot(t, f)
plt.xlabel('Time(s)')
plt.ylabel('Signal')
# FFTデータからピークを自動検出
maximal_idx = signal.argrelmax(F_abs, order=1)[0] # ピーク(極大値)のインデックス取得
# ピーク検出感度調整もどき、後半側(ナイキスト超)と閾値より小さい振幅ピークを除外
peak_cut = 0.3 # ピーク閾値
maximal_idx = maximal_idx[(F_abs[maximal_idx] > peak_cut) & (maximal_idx <= N/2)]
# グラフ表示(周波数軸)
plt.subplot(212)
plt.xlabel('Frequency(Hz)')
plt.ylabel('Amplitude')
plt.axis([0,1.0/dt/2,0,max(F_abs)*1.5])
plt.plot(freq, F_abs)
plt.plot(freq[maximal_idx], F_abs[maximal_idx],'ro')
# グラフにピークの周波数をテキストで表示
for i in range(len(maximal_idx)):
plt.annotate('{0:.0f}(Hz)'.format(np.round(freq[maximal_idx[i]])),
xy=(freq[maximal_idx[i]], F_abs[maximal_idx[i]]),
xytext=(10, 20),
textcoords='offset points',
arrowprops=dict(arrowstyle="->",connectionstyle="arc3,rad=.2")
)
plt.subplots_adjust(hspace=0.4)
plt.show()
print('peak', freq[maximal_idx])
ちょっとイイかも。
どうすればピークを検出できるの?
FFT解析データからピークを検出したい場合、グラフの極大値が分かれば目的を達成できそうだよね。
モモノキ、極大値って何箇所もあるけど簡単に求まるの?
scipyを使うと極大値や極小値を自動で求めてくれるよ。
自動で簡単ならやってみる。
FFT結果からピークを検出する例は、上のコード例を参考に自分で試してみて。今回は極大値と極小値を取得するキホン練習だけやってみよう。
OK、FFTのピーク検出はあとで試してみる。極大値と極小値の取得を練習する。
まずはデータ準備。ナノネ、乱数でノイズを入れた正弦波を一つ作ってグラフ表示してみて。
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 64 # データ数
x = np.linspace(0, 2*np.pi, N) # x軸
y = np.sin(x) # 正弦波
y_add_noise = y + np.random.randn(N)*0.3 # 正弦波+ノイズ(ピーク検出テスト用)
plt.plot(x, y,'r',label='sin(x)', c='gray', alpha=0.5) # 正弦波
plt.plot(x, y_add_noise,label='sin(x)+noise', c='g') # 正弦波+ノイズ
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
ノイズ入り正弦波を作った(ノイズなし正弦波も、おまけでプロット)。
続いて、scipyのsignalをインポートしてね。極大値はsignal.argrelmaxで、極小値はsignal.argrelminを使って、それぞれ求めることができるよ。
from scipy import signal
signalのインポートが完了した。使い方は?
最初は極大値の取得方法。signal.argrelmax(numpy配列データ, order=1)とすれば、極大値のインデックスが配列で返ってくるよ。
ナノネ、さっき作ったノイズ入り正弦波から極大値を取得を試してみて。インデックスの値を利用して極大値にところにマーカーのプロットもできる?
# ピーク検出(極大値、極小値)
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 64 # データ数
x = np.linspace(0, 2*np.pi, N) # x軸
y = np.sin(x) # 正弦波
y_add_noise = y + np.random.randn(N)*0.3 # 正弦波+ノイズ(ピーク検出テスト用)
# ピーク検出
maximal_idx = signal.argrelmax(y_add_noise, order=1) # 極大値インデックス取得
plt.plot(x, y,'r',label='sin(x)', c='gray', alpha=0.5) # 正弦波
plt.plot(x, y_add_noise,label='sin(x)+noise', c='g') # 正弦波+ノイズ
plt.plot(x[maximal_idx],y_add_noise[maximal_idx],'ro',label='peak_maximal') # 極大点プロット
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
signal.argrelmaxで配列データを渡すだけなら簡単だね。グラフの描き方は少し慣れてきたから大丈夫。
orderというパラメータがあったけど、1から値を変えるとどうなるの?
それならorderの値を少し増やして、結果がどうなるか実際試してみて(orderは1以上の整数を指定してね)。
# ピーク検出(極大値、オーダー値変更)
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 128 # データ数
x = np.linspace(0, 2*np.pi, N) # x軸
y = np.sin(x) # 正弦波
y_add_noise = y + np.random.randn(N)*0.3 # 正弦波+ノイズ(ピーク検出テスト用)
order_list = [1, 2, 4, 8]
axes_count = 4 # グラフの個数
fig = plt.figure(figsize=(12,8))
for i in range(axes_count):
# ピーク検出
maximal_idx = signal.argrelmax(y_add_noise, order=order_list[i]) # 極大値インデックス取得
fig.add_subplot(2, 2, i+1)
plt.title('order={}'.format(order_list[i]), fontsize=18)
plt.plot(x, y,'r',label='sin(x)', c='gray', alpha=0.5) # 正弦波
plt.plot(x, y_add_noise,label='sin(x)+noise', c='gray') # 正弦波+ノイズ
plt.plot(x[maximal_idx],y_add_noise[maximal_idx],'ro',label='peak_maximal') # 極大点プロット
plt.tight_layout()
orderの値を増やすと検出ピークの数が減ってる。
order値は、増減チェックを行うデータ数の幅(グラフ上のx軸)に相当するよ。1だと前後各1点、2だと前後各2点を対象にするみたいなイメージ。
次は極小値の取得方法。signal.argrelmin(numpy配列データ, order=1)とするればOK。使い方はargrelmaxと同じだよ。
# ピーク検出(極小値)
from scipy import signal
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 64 # データ数
x = np.linspace(0, 2*np.pi, N) # x軸
y = np.sin(x) # 正弦波
y_add_noise = y + np.random.randn(N)*0.3 # 正弦波+ノイズ(ピーク検出テスト用)
# ピーク検出
minimal_idx = signal.argrelmin(y_add_noise, order=1) # 極小値インデックス取得
plt.plot(x, y,'r',label='sin(x)', c='gray', alpha=0.5) # 正弦波
plt.plot(x, y_add_noise,label='sin(x)+noise', c='g') # 正弦波+ノイズ
plt.plot(x[minimal_idx],y_add_noise[minimal_idx],'bo',label='peak_minimal') # 極小点プロット
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
極小値も簡単に取得できた。
極大値と極小値をいっぺんにプロットしてみる。
# ピーク検出(極大値、極小値)
from scipy import signal
import numpy as np
from scipy import signal
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0) # 乱数seed固定
N = 64 # データ数
x = np.linspace(0, 2*np.pi, N) # x軸
y = np.sin(x) # 正弦波
y_add_noise = y + np.random.randn(N)*0.3 # 正弦波+ノイズ(ピーク検出テスト用)
#ピーク検出
maximal_idx = signal.argrelmax(y_add_noise, order=1) # 極大値インデックス取得
minimal_idx = signal.argrelmin(y_add_noise, order=1) # 極小値インデックス取得
plt.plot(x, y,'r',label='sin(x)', c='gray', alpha=0.5) # 正弦波
plt.plot(x, y_add_noise,label='sin(x)+noise', c='g') # 正弦波+ノイズ
plt.plot(x[maximal_idx],y_add_noise[maximal_idx],'ro',label='peak_maximal') # 極大点プロット
plt.plot(x[minimal_idx],y_add_noise[minimal_idx],'bo',label='peak_minimal') # 極小点プロット
plt.xlabel('x')
plt.ylabel('f(x)')
plt.legend()
イイね。データ内容は下の例のように確認できるよ。
# 極大値情報の表示例
print('idx_length:', len(maximal_idx)) # peakの検出数
print('idx_value:', maximal_idx) # peakのindex
print('x_value:', x[maximal_idx]) # peakのx値
print('y_value:', y_add_noise[maximal_idx]) #peakのy値
(最初の方に載せた、)FFT解析結果のピーク検出例は、極大値を一旦全部取得。そのあとで一定の振幅値を超えた極大値をピークとして採用しているよ。
あとは、グラフ上のピーク付近に周波数の値を表示すれば、簡易の自動ピーク検出は完成。引き出し線とテキスト挿入はMatplotlibのannotate()を利用しているよ。
検出条件も変更して練習してみる。
FFT簡易ピーク検出の練習はこれでおしまい。
お疲れ様でした。
お疲れ様でした。
またね!
勉強になりました!ありがとうございます。
返信削除