So implementieren Sie einen Bandpass-Butterworth-Filter mit Scipy.signal.butter

84

AKTUALISIEREN:

Ich habe ein Scipy-Rezept gefunden, das auf dieser Frage basiert! Wenn Sie interessiert sind, gehen Sie direkt zu: Inhalt »Signalverarbeitung» Butterworth Bandpass


Es fällt mir schwer, eine anfangs einfache Aufgabe der Implementierung eines Butterworth-Bandpassfilters für ein 1-D-Numpy-Array (Zeitreihen) zu erreichen.

Die Parameter, die ich einschließen muss, sind die Abtastrate, die Grenzfrequenzen IN HERTZ und möglicherweise die Reihenfolge (andere Parameter wie Dämpfung, Eigenfrequenz usw. sind für mich dunkler, daher würde jeder "Standard" -Wert ausreichen).

Was ich jetzt habe, ist das, was als Hochpassfilter zu funktionieren scheint, aber ich bin mir nicht sicher, ob ich es richtig mache:

def butter_highpass(interval, sampling_rate, cutoff, order=5):
    nyq = sampling_rate * 0.5

    stopfreq = float(cutoff)
    cornerfreq = 0.4 * stopfreq  # (?)

    ws = cornerfreq/nyq
    wp = stopfreq/nyq

    # for bandpass:
    # wp = [0.2, 0.5], ws = [0.1, 0.6]

    N, wn = scipy.signal.buttord(wp, ws, 3, 16)   # (?)

    # for hardcoded order:
    # N = order

    b, a = scipy.signal.butter(N, wn, btype='high')   # should 'high' be here for bandpass?
    sf = scipy.signal.lfilter(b, a, interval)
    return sf

Geben Sie hier die Bildbeschreibung ein

Die Dokumente und Beispiele sind verwirrend und undurchsichtig, aber ich möchte das Formular implementieren, das in der Empfehlung "für Bandpass" angegeben ist. Die Fragezeichen in den Kommentaren zeigen, wo ich gerade ein Beispiel kopiert habe, ohne zu verstehen, was passiert.

Ich bin kein Elektrotechniker oder Wissenschaftler, sondern nur ein Entwickler medizinischer Geräte, der eine recht einfache Bandpassfilterung für EMG-Signale durchführen muss.

Heltonbiker
quelle
Ich habe bei dsp.stackexchange etwas ausprobiert, aber sie konzentrieren sich zu sehr (mehr als ich handhaben kann) auf konzeptionelle Fragen des Engineerings und nicht so sehr auf die Verwendung der Scipy-Funktionen.
Heltonbiker

Antworten:

119

Sie können die Verwendung von Buttord überspringen und stattdessen einfach eine Reihenfolge für den Filter auswählen und prüfen, ob er Ihrem Filterkriterium entspricht. Um die Filterkoeffizienten für ein Bandpassfilter zu generieren, geben Sie butter () die Filterreihenfolge, die Grenzfrequenzen Wn=[low, high](ausgedrückt als Bruchteil der Nyquist-Frequenz, die die Hälfte der Abtastfrequenz beträgt) und den Bandtyp an btype="band".

Hier ist ein Skript, das einige praktische Funktionen für die Arbeit mit einem Butterworth-Bandpassfilter definiert. Wenn es als Skript ausgeführt wird, werden zwei Diagramme erstellt. Einer zeigt den Frequenzgang bei mehreren Filterreihenfolgen bei gleicher Abtastrate und Grenzfrequenz. Das andere Diagramm zeigt die Wirkung des Filters (mit der Reihenfolge = 6) auf eine Stichprobenzeitreihe.

from scipy.signal import butter, lfilter


def butter_bandpass(lowcut, highcut, fs, order=5):
    nyq = 0.5 * fs
    low = lowcut / nyq
    high = highcut / nyq
    b, a = butter(order, [low, high], btype='band')
    return b, a


def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
    b, a = butter_bandpass(lowcut, highcut, fs, order=order)
    y = lfilter(b, a, data)
    return y


if __name__ == "__main__":
    import numpy as np
    import matplotlib.pyplot as plt
    from scipy.signal import freqz

    # Sample rate and desired cutoff frequencies (in Hz).
    fs = 5000.0
    lowcut = 500.0
    highcut = 1250.0

    # Plot the frequency response for a few different orders.
    plt.figure(1)
    plt.clf()
    for order in [3, 6, 9]:
        b, a = butter_bandpass(lowcut, highcut, fs, order=order)
        w, h = freqz(b, a, worN=2000)
        plt.plot((fs * 0.5 / np.pi) * w, abs(h), label="order = %d" % order)

    plt.plot([0, 0.5 * fs], [np.sqrt(0.5), np.sqrt(0.5)],
             '--', label='sqrt(0.5)')
    plt.xlabel('Frequency (Hz)')
    plt.ylabel('Gain')
    plt.grid(True)
    plt.legend(loc='best')

    # Filter a noisy signal.
    T = 0.05
    nsamples = T * fs
    t = np.linspace(0, T, nsamples, endpoint=False)
    a = 0.02
    f0 = 600.0
    x = 0.1 * np.sin(2 * np.pi * 1.2 * np.sqrt(t))
    x += 0.01 * np.cos(2 * np.pi * 312 * t + 0.1)
    x += a * np.cos(2 * np.pi * f0 * t + .11)
    x += 0.03 * np.cos(2 * np.pi * 2000 * t)
    plt.figure(2)
    plt.clf()
    plt.plot(t, x, label='Noisy signal')

    y = butter_bandpass_filter(x, lowcut, highcut, fs, order=6)
    plt.plot(t, y, label='Filtered signal (%g Hz)' % f0)
    plt.xlabel('time (seconds)')
    plt.hlines([-a, a], 0, T, linestyles='--')
    plt.grid(True)
    plt.axis('tight')
    plt.legend(loc='upper left')

    plt.show()

Hier sind die Diagramme, die von diesem Skript generiert werden:

Frequenzgang für mehrere Filteraufträge

Geben Sie hier die Bildbeschreibung ein

Warren Weckesser
quelle
1
Wissen Sie, warum die gefilterte Ausgabe immer bei Null beginnt? Ist es möglich, es mit dem tatsächlichen Eingabewert abzugleichen x[0]? Ich habe ähnliche Dinge mit dem Cheby1-Tiefpassfilter ausprobiert und das gleiche Problem festgestellt.
LWZ
2
@LWZ: Verwenden Sie die Funktion scipy.signal.lfilter_ziund das ziArgument zu lfilter. Einzelheiten finden Sie in der Dokumentzeichenfolge für lfilter_zi. TL; DR? Wechseln Sie einfach y = lfilter(b, a, data)zu zi = lfilter_zi(b, a); y, zo = lfilter(b, a, data, zi=zi*data[0]). (Aber dies könnte mit einem Bandpass oder Hochpassfilter keinen Unterschied machen.)
Warren Weckesser
1
Mir ist aufgefallen, dass der Ausgang von scipy.signal.lfiter()wrt zum ursprünglichen Signal und zum signal.filtfilt()Ausgang um 180 Grad phasenverschoben ist. Warum ist das so? Sollte ich filtfilt()stattdessen verwenden, wenn mir das Timing wichtig ist?
Jason
1
Das ist die Phasenverzögerung des Filters bei dieser Frequenz. Die Phasenverzögerung einer Sinuskurve durch ein Butterworth-Filter hängt nichtlinear von der Frequenz ab. Für Nullphasenverzögerung können Sie ja verwenden filtfilt(). Meine Antwort hier enthält ein Beispiel für die Verwendung filtfilt(), um eine durch den Filter verursachte Verzögerung zu vermeiden.
Warren Weckesser
1
Hey Jason, ich empfehle, unter dsp.stackexchange.com Fragen zur Signalverarbeitungstheorie zu stellen . Wenn Sie eine Frage zu einem von Ihnen geschriebenen Code haben, der nicht wie erwartet funktioniert, können Sie hier eine neue Frage zum Stackoverflow starten.
Warren Weckesser
40

Die Filterentwurfsmethode in der akzeptierten Antwort ist korrekt, weist jedoch einen Fehler auf. Mit b, a entworfene SciPy-Bandpassfilter sind instabil und können bei höheren Filterordnungen zu fehlerhaften Filtern führen .

Verwenden Sie stattdessen die sos-Ausgabe (Abschnitte zweiter Ordnung) des Filterdesigns.

from scipy.signal import butter, sosfilt, sosfreqz

def butter_bandpass(lowcut, highcut, fs, order=5):
        nyq = 0.5 * fs
        low = lowcut / nyq
        high = highcut / nyq
        sos = butter(order, [low, high], analog=False, btype='band', output='sos')
        return sos

def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
        sos = butter_bandpass(lowcut, highcut, fs, order=order)
        y = sosfilt(sos, data)
        return y

Sie können den Frequenzgang auch durch Ändern darstellen

b, a = butter_bandpass(lowcut, highcut, fs, order=order)
w, h = freqz(b, a, worN=2000)

zu

sos = butter_bandpass(lowcut, highcut, fs, order=order)
w, h = sosfreqz(sos, worN=2000)
user13107
quelle
+1, weil dies jetzt in vielen Fällen der bessere Weg ist. Wie in den Kommentaren zur akzeptierten Antwort ist es auch möglich, die Phasenverzögerung durch Vorwärts-Rückwärts-Filterung zu beseitigen. Ersetzen Sie einfach sosfiltmit sosfiltfilt.
Mike
@Mike und user13107 Betrifft der gleiche Fehler auch Hochpass- und Tiefpass-Butterworth-Filter? Und ist die Lösung dieselbe?
Dewarrn1
3
@ dewarrn1 Es ist eigentlich nicht richtig, es einen "Bug" zu nennen; Der Algorithmus ist korrekt implementiert, aber von Natur aus instabil, sodass es sich nur um eine schlechte Wahl des Algorithmus handelt. Aber ja, es betrifft jeden Filter höherer Ordnung - nicht nur Hoch- oder Tiefpassfilter und nicht nur Butterworth-Filter, sondern auch andere wie Chebyshev und so weiter. Im Allgemeinen ist es jedoch am besten, immer die sosAusgabe auszuwählen , da dies immer die Instabilität vermeidet. Und wenn Sie keine Echtzeitverarbeitung benötigen, sollten Sie diese immer verwenden sosfiltfilt.
Mike
Entschuldigung, ich hatte diese Antwort vor langer Zeit nicht bemerkt! @ user13107, ja, die Darstellung der Übertragungsfunktion (oder 'ba') eines linearen Filters weist einige schwerwiegende numerische Probleme auf, wenn die Filterreihenfolge groß ist. Selbst Filter relativ niedriger Ordnung können Probleme haben, wenn die gewünschte Bandbreite im Vergleich zur Abtastfrequenz klein ist. Meine ursprüngliche Antwort wurde geschrieben, bevor die SOS-Darstellung zu SciPy hinzugefügt wurde und bevor das fsArgument zu vielen Funktionen in hinzugefügt wurde scipy.signal. Die Antwort für ein Update ist längst überfällig.
Warren Weckesser
4

Für ein Bandpassfilter ist ws ein Tupel, das die Frequenzen der unteren und oberen Ecke enthält. Diese stellen die digitale Frequenz dar, bei der die Filterantwort 3 dB unter dem Durchlassbereich liegt.

wp ist ein Tupel, das die digitalen Frequenzen des Stoppbands enthält. Sie stellen den Ort dar, an dem die maximale Dämpfung beginnt.

gpass ist die maximale Dämpfung im Durchlassbereich in dB, während gstop die Aufmerksamkeit in den Stoppbändern ist.

Angenommen, Sie möchten einen Filter für eine Abtastrate von 8000 Abtastungen / s mit Eckfrequenzen von 300 und 3100 Hz entwerfen. Die Nyquist-Frequenz ist die Abtastrate geteilt durch zwei oder in diesem Beispiel 4000 Hz. Die äquivalente digitale Frequenz beträgt 1,0. Die beiden Eckfrequenzen sind dann 300/4000 und 3100/4000.

Nehmen wir nun an, Sie wollten, dass die Stoppbänder 30 dB +/- 100 Hz unter den Eckfrequenzen liegen. Somit würden Ihre Stoppbänder bei 200 und 3200 Hz beginnen, was zu den digitalen Frequenzen von 200/4000 und 3200/4000 führen würde.

Um Ihren Filter zu erstellen, würden Sie buttord als bezeichnen

fs = 8000.0
fso2 = fs/2
N,wn = scipy.signal.buttord(ws=[300/fso2,3100/fso2], wp=[200/fs02,3200/fs02],
   gpass=0.0, gstop=30.0)

Die Länge des resultierenden Filters hängt von der Tiefe der Stoppbänder und der Steilheit der Antwortkurve ab, die durch die Differenz zwischen der Eckfrequenz und der Stoppbandfrequenz bestimmt wird.

sizzzzlerz
quelle
Ich habe versucht, es zu implementieren, aber es fehlt noch etwas. Eine Sache ist, dass gpass=0.0eine Division durch einen Fehler von Null ausgelöst wird, also habe ich sie auf 0,1 geändert und der Fehler wurde gestoppt. Abgesehen davon buttersagen die Dokumente zum Beispiel : Passband and stopband edge frequencies, normalized from 0 to 1 (1 corresponds to pi radians / sample).Ich bin mir nicht sicher, ob Ihre Antwort die Berechnungen richtig gemacht hat, also arbeite ich noch daran und werde bald ein Feedback geben.
Heltonbiker
(auch, obwohl meine wsund wpjeweils zwei Elemente haben, führt der Filter nur Tief- oder Hochpass (über btypeArgument), aber keinen Bandpass durch)
Heltonbiker
1
Gemäß der Dokumentation unter docs.scipy.org/doc/scipy/reference/generated/… entwirft Buttord Low- , High- und Bandpassfilter. Was gpass angeht, lässt Buttord wohl keine Dämpfung von 0 dB im Durchlassbereich zu. Stellen Sie dann einen Wert ungleich Null ein.
Sizzzzlerz