Ein frei kopier- und anpassbares Lehrmittel von eduskript.org

FFT – Automatische Pulsberechnung

Lernziele
  • SuS verstehen konzeptuell, was die Fourier-Transformation macht
  • SuS können die dominante Frequenz aus einem Signal extrahieren
  • SuS können Frequenz in Herzschläge pro Minute umrechnen

Im vorherigen Experiment haben wir das PPG-Signal aufgenommen und visualisiert. Dabei konnten wir das pulsierende Signal sehen, mussten aber die Herzschläge manuell zählen. Das ist mühsam und ungenau – Zeit für Automatisierung!

Was ist die Fourier-Transformation?

Stellen Sie sich vor, Sie hören einen Akkord am Klavier. Ihr Ohr kann automatisch die einzelnen Töne heraushören – etwa ein C, ein E und ein G. Genau das macht die Fourier-Transformation (FFT) mathematisch mit Signalen: Sie zerlegt komplexe Wellenformen in ihre Grundfrequenzen.

Analogie: Musikakkord
  • Zeitbereich: "Wann erklingt welcher Ton?"
  • Frequenzbereich: "Welche Töne stecken im Akkord?"

Unser PPG-Signal enthält verschiedene Frequenzen:

  • Drift (~0 Hz): Langsame Helligkeitsänderungen
  • Atmung (~0.2 Hz): 12 Atemzüge pro Minute
  • Puls (~1-1.5 Hz): 60-90 Herzschläge pro Minute
  • Rauschen: Zufällige Störungen

Die FFT zeigt uns, welche Frequenz am stärksten ist – das ist unser Puls!

Theorie: Vom Signal zur Herzfrequenz

PythonLoading editor…
import numpy as np
import matplotlib.pyplot as plt

# Simuliertes PPG-Signal mit verschiedenen Frequenzen
time = np.linspace(0, 10, 1000)  # 10 Sekunden
drift = 0.1 * time  # Langsame Drift
atmung = 0.05 * np.sin(2 * np.pi * 0.2 * time)  # Atmung (0.2 Hz)
puls = 0.3 * np.sin(2 * np.pi * 1.2 * time)  # Puls (1.2 Hz = 72 bpm)
rauschen = 0.02 * np.random.randn(1000)

signal = drift + atmung + puls + rauschen

plt.figure(figsize=(12, 4))
plt.plot(time, signal)
plt.title('Simuliertes PPG-Signal (Zeitbereich)')
plt.xlabel('Zeit [s]')
plt.ylabel('Amplitude')
plt.grid(True)
plt.show()

Jetzt wenden wir die FFT an, um die Frequenzen zu extrahieren:

PythonLoading editor…
# FFT berechnen
fft_result = np.abs(np.fft.fft(signal))
freqs = np.fft.fftfreq(len(signal), time[1] - time[0])

# Nur positive Frequenzen anzeigen
positive_freqs = freqs[:len(freqs)//2]
positive_fft = fft_result[:len(fft_result)//2]

plt.figure(figsize=(12, 4))
plt.plot(positive_freqs, positive_fft)
plt.title('FFT-Spektrum (Frequenzbereich)')
plt.xlabel('Frequenz [Hz]')
plt.ylabel('Amplitude')
plt.xlim(0, 3)  # Nur relevanter Bereich
plt.grid(True)
plt.show()

# Dominante Frequenz finden
puls_bereich = (positive_freqs >= 0.7) & (positive_freqs <= 3.0)
dominante_freq = positive_freqs[puls_bereich][np.argmax(positive_fft[puls_bereich])]
herzfrequenz = dominante_freq * 60

print(f"Dominante Frequenz: {dominante_freq:.2f} Hz")
print(f"Herzfrequenz: {herzfrequenz:.0f} bpm")
Umrechnung Frequenz → bpm

Frequenz [Hz] × 60 = Herzschläge pro Minute [bpm]

Beispiel: 1.2 Hz × 60 = 72 bpm

Praxis: Echtes PPG-Signal verarbeiten

Laden wir das Signal aus dem vorherigen Experiment und wenden die FFT-Analyse an:

PythonLoading editor…
import imageio
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, filtfilt

def bandpass_filter(data, lowcut, highcut, fs, order=4):
    """Bandpassfilter für PPG-Signal"""
    nyquist = 0.5 * fs
    low = lowcut / nyquist
    high = highcut / nyquist
    b, a = butter(order, [low, high], btype='band')
    return filtfilt(b, a, data)

# Video laden (ersetzen Sie mit Ihrem Dateinamen)
try:
    video = imageio.get_reader('video.mp4', 'ffmpeg')
    fps = video.get_meta_data()['fps']
    
    # Roten Farbkanal extrahieren
    red_values = []
    for frame in video:
        red_mean = np.mean(frame[:, :, 0])  # Roter Kanal
        red_values.append(red_mean)
    
    video.close()
    
    # Zeit-Achse erstellen
    time = np.arange(len(red_values)) / fps
    
    print(f"Video: {len(red_values)} Frames, {fps} fps, {time[-1]:.1f}s Dauer")
    
except:
    # Fallback: Simulierte Daten wenn kein Video vorhanden
    print("Kein Video gefunden - verwende simulierte Daten")
    fps = 30
    time = np.linspace(0, 20, 600)
    red_values = 100 + 5*np.sin(2*np.pi*1.3*time) + 2*np.random.randn(600)

# Originalsignal plotten
plt.figure(figsize=(12, 8))

plt.subplot(3, 1, 1)
plt.plot(time, red_values, 'r-', alpha=0.7)
plt.title('Original PPG-Signal')
plt.ylabel('Helligkeit (rot)')
plt.grid(True)

Jetzt filtern wir das Signal und berechnen die Herzfrequenz:

PythonLoading editor…
# Bandpassfilter anwenden (42-180 bpm entspricht 0.7-3 Hz)
filtered_signal = bandpass_filter(red_values, 0.7, 3.0, fps)

plt.subplot(3, 1, 2)
plt.plot(time, filtered_signal, 'b-')
plt.title('Gefiltertes Signal (0.7-3 Hz Bandpass)')
plt.ylabel('Amplitude')
plt.grid(True)

# FFT berechnen
fft_result = np.abs(np.fft.fft(filtered_signal))
freqs = np.fft.fftfreq(len(filtered_signal), 1/fps)

# Nur positive Frequenzen
positive_freqs = freqs[:len(freqs)//2]
positive_fft = fft_result[:len(fft_result)//2]

plt.subplot(3, 1, 3)
plt.plot(positive_freqs, positive_fft, 'g-')
plt.title('FFT-Spektrum')
plt.xlabel('Frequenz [Hz]')
plt.ylabel('Amplitude')
plt.xlim(0.5, 3.5)
plt.grid(True)

plt.tight_layout()
plt.show()

# Herzfrequenz bestimmen
puls_bereich = (positive_freqs >= 0.7) & (positive_freqs <= 3.0)
if np.any(puls_bereich):
    peak_index = np.argmax(positive_fft[puls_bereich])
    dominante_freq = positive_freqs[puls_bereich][peak_index]
    herzfrequenz = dominante_freq * 60
    
    print(f"\n🔍 FFT-Analyse:")
    print(f"Dominante Frequenz: {dominante_freq:.3f} Hz")
    print(f"Berechnete Herzfrequenz: {herzfrequenz:.0f} bpm")
else:
    print("Kein klarer Puls-Peak gefunden!")

Validierung: Vergleich mit manueller Zählung

Aufgabe: Genauigkeit überprüfen
  1. Zählen Sie in Ihrem ursprünglichen Plot manuell die Herzschläge
  2. Rechnen Sie auf 1 Minute hoch: (Gezählte Peaks / Videodauer) × 60
  3. Vergleichen Sie mit der FFT-Berechnung
PythonLoading editor…
# Manuelle Validierung
manual_count = input("Wie viele Herzschläge haben Sie im Video gezählt? ")
if manual_count.isdigit():
    manual_count = int(manual_count)
    video_duration = len(red_values) / fps
    manual_bpm = (manual_count / video_duration) * 60
    
    print(f"\n📊 Vergleich:")
    print(f"Manuelle Zählung: {manual_bpm:.0f} bpm")
    print(f"FFT-Berechnung:   {herzfrequenz:.0f} bpm")
    print(f"Unterschied:      {abs(herzfrequenz - manual_bpm):.0f} bpm")
    
    if abs(herzfrequenz - manual_bpm) <= 5:
        print("✅ Ausgezeichnet! Unterschied ≤ 5 bpm")
    elif abs(herzfrequenz - manual_bpm) <= 10:
        print("👍 Gut! Unterschied ≤ 10 bpm")
    else:
        print("⚠️  Grosse Abweichung - Signal eventuell zu verrauscht")

Warum funktioniert die FFT so gut?

Die Fourier-Transformation ist ideal für periodische Signale wie den Herzschlag:

Vorteile der FFT-Methode
  • Automatisch: Keine manuelle Peakzählung nötig
  • Robust: Filtert Rauschen und Störungen heraus
  • Genau: Kann auch bei schwachen Signalen arbeiten
  • Schnell: Efficient implementiert in NumPy

Limitationen:

  • Benötigt gleichmässigen Herzrhythmus
  • Mindestens 10-15 Sekunden Aufnahmedauer
  • Störungen durch Bewegung können Ergebnis verfälschen

Weiterführende Experimente

Erweiterungen
  1. Verschiedene Körperstellen: Testen Sie Finger, Ohrläppchen, Handgelenk
  2. Belastungstest: Messen Sie vor und nach körperlicher Aktivität
  3. Signalqualität: Vergleichen Sie verschiedene Beleuchtungsarten
  4. Zeitvarianz: Analysieren Sie längere Aufnahmen in 30-Sekunden-Fenstern

Im nächsten Kapitel werden wir ein Live-System entwickeln, das den Puls in Echtzeit anzeigt!