web-dev-qa-db-ja.com

numpy / scipyを使用してデジタル信号の勾配の変化を識別しますか?

私はPython=で一般化された方法を考え出して、一連の計画された宇宙船の操縦中に発生するピッチ回転を特定しようとしています。それを shiftの特定のケースと考えることができます検出 問題。

一連の測定値のsolar_elevation_angle変数を検討して、宇宙船の計器から測定された太陽の仰角を特定します。データをいじりたい人のために、solar_elevation_angle.txtファイル here を保存しました。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import gridspec
from scipy.signal import argrelmax
from scipy.ndimage.filters import gaussian_filter1d

solar_elevation_angle = np.loadtxt("solar_elevation_angle.txt", dtype=np.float32)

fig, ax = plt.subplots()    
ax.set_title('Solar elevation angle')
ax.set_xlabel('Scanline')
ax.set_ylabel('Solar elevation angle [deg]')
ax.plot(solar_elevation_angle)
plt.show()

Solar elevation angle plot

スキャンラインは私の時間の次元です。勾配が変化する4つのポイントは、宇宙船のピッチ回転を示します。

ご覧のとおり、宇宙船の操縦領域外での太陽仰角の変化は、時間の関数としてかなり線形であり、これはこの特定の宇宙船の場合は常に当てはまります(主要な障害を除く)。

各宇宙船の操作中、傾斜の変化は明らかに連続的ですが、私の角度値のセットでは離散化されています。つまり、操作ごとに、操作が行われた単一のスキャンラインを見つけようとしても意味がありません。私の目標は、操作ごとに、操作が発生した時間間隔を定義するスキャンラインの範囲内の「代表的な」スキャンラインを特定することです(たとえば、中央の値、または左の境界)。

すべての操作が行われた「代表的な」スキャンラインインデックスのセットを取得したら、それらのインデックスを使用して操作時間の大まかな見積もりを行ったり、プロットにラベルを自動的に配置したりできます。

これまでの私の解決策は:

  1. np.gradientを使用して、太陽仰角の2次導関数を計算します。
  2. 結果の曲線の絶対値とクリッピングを計算します。線形セグメントの離散化ノイズであると私が想定しているため、クリッピングが必要です。これは、ポイント4の「実際の」極大値の識別に深刻な影響を及ぼします。
  3. 結果のカーブにスムージングを適用して、複数のピークを取り除きます。私はそのために試行錯誤のシグマ値を持つscipyの1dガウスフィルターを使用しています。
  4. 極大値を特定します。

これが私のコードです:

fig = plt.figure(figsize=(8,12))
gs = gridspec.GridSpec(5, 1) 

ax0 = plt.subplot(gs[0])
ax0.set_title('Solar elevation angle')
ax0.plot(solar_elevation_angle)

solar_elevation_angle_1stdev = np.gradient(solar_elevation_angle)
ax1 = plt.subplot(gs[1])
ax1.set_title('1st derivative')
ax1.plot(solar_elevation_angle_1stdev)

solar_elevation_angle_2nddev = np.gradient(solar_elevation_angle_1stdev)
ax2 = plt.subplot(gs[2])
ax2.set_title('2nd derivative')
ax2.plot(solar_elevation_angle_2nddev)

solar_elevation_angle_2nddev_clipped = np.clip(np.abs(np.gradient(solar_elevation_angle_2nddev)), 0.0001, 2)
ax3 = plt.subplot(gs[3])
ax3.set_title('absolute value + clipping')
ax3.plot(solar_elevation_angle_2nddev_clipped)

smoothed_signal = gaussian_filter1d(solar_elevation_angle_2nddev_clipped, 20)
ax4 = plt.subplot(gs[4])
ax4.set_title('Smoothing applied')
ax4.plot(smoothed_signal)

plt.tight_layout()
plt.show()

enter image description here

その後、scipyのargrelmax関数を使用して、極大値を簡単に識別できます。

max_idx = argrelmax(smoothed_signal)[0]
print(max_idx)
# [ 689 1019 2356 2685]

私が探していたスキャンラインインデックスを正しく識別する:

fig, ax = plt.subplots()
ax.set_title('Solar elevation angle')
ax.set_xlabel('Scanline')
ax.set_ylabel('Solar elevation angle [deg]')
ax.plot(solar_elevation_angle)
ax.scatter(max_idx, solar_elevation_angle[max_idx], marker='x', color='red')
plt.show()

enter image description here

私の質問は:この問題に取り組むより良い方法はありますか?
ノイズを取り除くためにクリッピングしきい値を手動で指定する必要があるため、ガウシアンフィルターのシグマがこのアプローチを大幅に弱め、他の同様のケースに適用できないことがわかりました。

11
stm4tt

最初の改善は、 Savitzky-Golayフィルター を使用して、ノイズの少ない方法で導関数を見つけることです。たとえば、特定のサイズの各データスライスに放物線(最小二乗の意味で)を適合させ、その放物線の2次導関数をとることができます。結果は、gradientで2階の差を取るだけの場合よりもはるかに優れています。ここでは、ウィンドウサイズ101を使用しています。

savgol_filter(solar_elevation_angle, window_length=window, polyorder=2, deriv=2) 

filtered_2d

次に、argrelmaxを使用して最大値のポイントを探す代わりに、2次導関数が大きい場所を探すことをお勧めします。たとえば、最大サイズの少なくとも半分。これはもちろん多くのインデックスを返しますが、それらのインデックス間のギャップを調べて、各ピークの始まりと終わりを特定できます。ピークの中点が簡単に見つかります。

これが完全なコードです。唯一のパラメーターはウィンドウサイズで、101に設定されています。このアプローチは堅牢です。サイズ21または201は、基本的に同じ結果になります(奇数でなければなりません)。

from scipy.signal import savgol_filter
window = 101
der2 = savgol_filter(solar_elevation_angle, window_length=window, polyorder=2, deriv=2)
max_der2 = np.max(np.abs(der2))
large = np.where(np.abs(der2) > max_der2/2)[0]
gaps = np.diff(large) > window
begins = np.insert(large[1:][gaps], 0, large[0])
ends = np.append(large[:-1][gaps], large[-1])
changes = ((begins+ends)/2).astype(np.int)
plt.plot(solar_elevation_angle)
plt.plot(changes, solar_elevation_angle[changes], 'ro')
plt.show()

changes

挿入と追加の煩わしさは、大きな導関数を持つ最初のインデックスが「ピークの始まり」と見なされ、最後のインデックスが「ピークの終わり」と見なされるためです。無限です)。

区分的線形フィット

これは代替法(必ずしも優れているとは限りません)であり、導関数を使用しません。フィットは 次数1の平滑化スプライン (つまり、区分的線形曲線)であり、ノットの位置に注意してください。

まず、データを正規化して(solar_elevation_angleではなくyと呼びます)、標準偏差を1にします。

y /= np.std(y)

最初のステップは、データから最大で指定されたしきい値だけ逸脱する区分線形曲線を作成することです。任意に0.1に設定します(yは正規化されているため、ここでは単位はありません)。これは、UnivariateSplineを繰り返し呼び出し、大きなスムージングパラメータから開始して、曲線がフィットするまで徐々に減らして行います。 (残念ながら、希望する均一なエラー範囲を単純に渡すことはできません)。

from scipy.interpolate import UnivariateSpline
threshold = 0.1

m = y.size
x = np.arange(m)
s = m
max_error = 1
while max_error > threshold: 
  spl = UnivariateSpline(x, y, k=1, s=s)
  interp_y = spl(x)
  max_error = np.max(np.abs(interp_y - y))
  s /= 2
knots = spl.get_knots()
values = spl(knots)

これまでのところ、ノットを見つけ、それらのノットでのスプラインの値に注目しました。しかし、これらすべての結び目が本当に重要なわけではありません。各ノットの重要性をテストするために、それを削除し、それなしで補間します。新しい補間関数が古い補間関数と大幅に異なる場合(エラーを2倍にする)、ノットは重要であると見なされ、見つかった勾配変化のリストに追加されます。

ts = knots.size
idx = np.arange(ts)
changes = []
for j in range(1, ts-1):
  spl = UnivariateSpline(knots[idx != j], values[idx != j], k=1, s=0)
  if np.max(np.abs(spl(x) - interp_y)) > 2*threshold:
    changes.append(knots[j])
plt.plot(y)
plt.plot(changes, y[np.array(changes, dtype=int)], 'ro')
plt.show()

found

理想的には、与えられたデータに区分線形関数を当てはめ、ノットの数を増やして、もう1つ追加しても「実質的な」改善がもたらされないようにします。上記はSciPyツールでの大雑把な近似ですが、可能な限り最善ではありません。 Pythonの市販の区分線形モデル選択ツールについては知りません。

7
user6655984