Envelope for Least Square Filtering and Smoothing

Signal Envelope

scipy.signal.hilbert
scipy.interpolate.interp1d

  • 복잡한 신호의 전반적인 형상을 파악하기 위해 envelope을 추출합니다.

  • python에서는 scipy.signal.hilbert를 통해 analytic signal을 추출하는 방법을 씁니다.

    1
    2
    x = np.arange(0, 20.1, 0.1)
    y = abs(np.sin(x)) * np.sin(x*20)


  • 이와 같은 신호에 hilbert를 다음과 같이 적용하면 다음과 같이 숨겨진 sine wave를 찾아냅니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from scipy.signal import hilbert, chirp

    duration = 1.0
    fs = 400.0
    samples = int(fs*duration)
    t = np.arange(samples) / fs

    analytic_signal = hilbert(y)
    amplitude_envelope = np.abs(analytic_signal)
    instantaneous_phase = np.unwrap(np.angle(analytic_signal))
    instantaneous_frequency = np.diff(instantaneous_phase) / (2.0*np.pi) * fs

    fig, ax = plt.subplots(nrows=2, sharex=True)
    ax[0].plot(x, y, c="b")
    ax[0].plot(x, amplitude_envelope)
    ax[1].plot(x[1:], instantaneous_frequency)
    ax[1].set_xlabel("time in seconds")


  • 그러나, 위 신호 그대로의 모습을 추출하려면 이론식을 찾는 hilbert는 답답한 면이 있습니다.

  • 그럴 때는 아래와 같이 그래프의 요철을 감지해서 interpolation 하는 방법이 직관적입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    def get_envelope_v1(x, y):
    x_list, y_list = list(x), list(y)
    assert len(x_list) == len(y_list)

    # First data
    ui, ux, uy = [0], [x_list[0]], [y_list[0]]
    li, lx, ly = [0], [x_list[0]], [y_list[0]]

    # Find upper peaks and lower peaks
    for i in range(1, len(x_list)-1):
    if y_list[i] >= y_list[i-1] and y_list[i] >= y_list[i+1]:
    ui.append(i)
    ux.append(x_list[i])
    uy.append(y_list[i])
    if y_list[i] <= y_list[i-1] and y_list[i] <= y_list[i+1]:
    li.append(i)
    lx.append(x_list[i])
    ly.append(y_list[i])

    # Last data
    ui.append(len(x_list)-1)
    ux.append(x_list[-1])
    uy.append(y_list[-1])
    li.append(len(y_list)-1)
    lx.append(x_list[-1])
    ly.append(y_list[-1])

    if len(ux) == 2 or len(lx) == 2:
    return [], []

    else:
    func_ub = interp1d(ux, uy, kind='cubic', bounds_error=False)
    func_lb = interp1d(lx, ly, kind='cubic', bounds_error=False)

    ub, lb = [], []
    for i in x_list:
    ub = func_ub(x_list)
    lb = func_lb(x_list)

    ub = np.array([y, ub]).max(axis=0)
    lb = np.array([y, lb]).min(axis=0)

    return ub, lb
    1
    2
    3
    4
    5
    6
    7
    8
    uy, ly = get_envelope_v1(x, y)

    fig, ax = plt.subplots()
    ax.plot(x, y, c="b")
    ax.plot(x, uy, c="m")
    ax.plot(x, ly, c="c")
    ax.set_xlim(0, 20)
    plt.show()


  • signal envelope은 보통 전체적인 형상을 추출하는데 사용되지만, smoothing 제어에 사용될 수도 있습니다.

Least Squares Smoothing

scipy.signal.savgol_filter
Least Squares Filtering and Smoothing, including Savitzky-Golay
Plateau Detection

  • 신호와 함께 존재하기 마련인 잡음을 제거하기 위해 smoothing을 적용합니다.

  • savinsky-golay filter와 같은 least squares filter는 mean이나 median에 비해 신호를 보존하고 잡음을 선택적으로 잘 제거한다는 장점이 있습니다.

  • 그러나 치명적인 단점이 있는데, signal이 크게 바뀔 경우 overshooting이 발생한다는 것입니다.


  • envelope을 이용해 overshooting의 범위를 제한해 보겠습니다.

  • 그러나 데이터가 이렇게 생기면 envelope도 깔끔하진 않습니다.

  • 같은 코드를 적용해도, 아래와 같이 envelope의 undershooting이 커서 문제가 됩니다.


  • envelop과 원래 데이터를 다시 비교해서 envelope의 범위를 제어합니다.

  • get_envelope()에 아래 부분을 추가합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # upper through check
    ux_chk, uy_chk = [x_list[0]], [y_list[0]]
    for ii in range(1, len(ui)):
    slope = (uy[ii]-uy[ii-1])/(ux[ii]-ux[ii-1])
    intercept = -slope*ux[ii] + uy[ii]

    for xi in range(ui[ii-1], ui[ii]):
    y_pred = slope*x_list[xi] + intercept
    if y_pred < y_list[xi] and (x_list[xi] not in ux_chk) and (y_list[xi] not in uy_chk):
    ux_chk.append(x_list[xi])
    uy_chk.append(y_list[xi])
    ux_chk.append(x_list[ui[ii]])
    uy_chk.append(y_list[ui[ii]])

    # lower through check
    lx_chk, ly_chk = [x_list[0]], [y_list[0]]
    for ii in range(1, len(li)):
    slope = (ly[ii]-ly[ii-1])/(lx[ii]-lx[ii-1])
    intercept = -slope*lx[ii] + ly[ii]

    for xi in range(li[ii-1], li[ii]):
    y_pred = slope*x_list[xi] + intercept
    if y_pred > y_list[xi] and (x_list[xi] not in lx_chk) and (y_list[xi] not in ly_chk):
    lx_chk.append(x_list[xi])
    ly_chk.append(y_list[xi])
    lx_chk.append(x_list[li[ii]])
    ly_chk.append(y_list[li[ii]])


  • envelope의 범위가 제어되었습니다.

  • 이제 envelope을 벗어난 signal 대신 envelope을 적용합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def savgol(x, y, box_size, recur=1):
    ub, lb = get_envelope(x, y)

    for _ in range(recur):
    y = sg(y, box_size, 2, mode='nearest')
    if len(ub) > 0:
    y = np.where(y > ub, ub, np.where(y < lb, lb, y))

    return y

    y_savgol = savgol(X, Y, 15)


  • Savgol filter의 overshooting 문제가 해결되었습니다.

  • 강제로 집어넣은 부분이 다른 부분에 비해 다소 날카롭습니다.

  • 그러나 overshooting에 비해서는 완화된 것을 확인할 수 있습니다.

  • 전체 코드는 여기에서 확인 가능합니다.


도움이 되셨나요? 카페인을 투입하시면 다음 포스팅으로 변환됩니다

Share