Ein frei kopier- und anpassbares Lehrmittel von eduskript.org

USB-Mikroskop als PPG-Sensor

Lernziele
  • SuS können ein alternatives Messsetup mit USB-Mikroskop aufbauen
  • SuS verstehen Vorteile kontrollierter Beleuchtung
  • SuS können Live-Videostream in Python verarbeiten
  • SuS können verschiedene Körperstellen auf Signalqualität vergleichen

Warum ein USB-Mikroskop?

In den vorherigen Lektionen haben wir mit dem Smartphone experimentiert. Das hat gut funktioniert, aber es gibt einige Limitationen:

  • Ungleichmässige Beleuchtung durch den Blitz
  • Schwierige Positionierung des Fingers
  • Keine präzise Kontrolle über die Aufnahmequalität

Ein USB-Mikroskop bietet uns mehrere Vorteile:

Kontrollierte Beleuchtung: Die meisten USB-Mikroskope haben eingebaute LED-Beleuchtung, die konstant und gleichmässig ist – genau wie bei professionellen Pulsmessern.

Stabile Positionierung: Das Mikroskop steht fest auf dem Tisch, der Finger kann entspannt aufgelegt werden.

Bessere Bildqualität: Durch die Nahaufnahme sehen wir die Hautstruktur detaillierter, was zu einem klareren PPG-Signal führen kann.

Experimenteller Aufbau

Hardware-Setup

  1. USB-Mikroskop anschliessen: Verbindet das Mikroskop mit eurem Computer
  2. Positionierung: Stellt das Mikroskop so auf, dass ihr die Fingerkuppe oder das Ohrläppchen scharf sehen könnt
  3. Beleuchtung: Aktiviert die LED-Beleuchtung des Mikroskops
  4. Abstand: Experimentiert mit verschiedenen Abständen – die Haut soll gut ausgeleuchtet, aber nicht überbelichtet sein
Tipp: Beste Körperstellen
  • Fingerkuppe: Gute Durchblutung, einfach zu positionieren
  • Ohrläppchen: Sehr dünne Haut, starkes Signal
  • Handgelenk: Wie bei einer Smartwatch, aber schwieriger zu messen

Software-Vorbereitung

Wir benötigen OpenCV für die Kamera-Kommunikation:

import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, filtfilt
import time
Installation

Falls OpenCV noch nicht installiert ist:

pip install opencv-python

Live-Videostream verarbeiten

Kamera initialisieren

PythonLoading editor…
import cv2
import numpy as np

# Kamera initialisieren (USB-Mikroskop)
cap = cv2.VideoCapture(0)  # Probiert 0, 1, 2 je nach USB-Port

# Prüfen ob Kamera funktioniert
if not cap.isOpened():
    print("Fehler: Kamera konnte nicht geöffnet werden")
    exit()

print("Kamera gefunden! Drückt 'q' zum Beenden.")

# Live-Vorschau
while True:
    ret, frame = cap.read()
    if not ret:
        break
    
    cv2.imshow('USB-Mikroskop', frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

PPG-Daten sammeln

Jetzt sammeln wir systematisch die Rotwerte für unser PPG-Signal:

PythonLoading editor…
def collect_ppg_data(duration_seconds=20):
    """
    Sammelt PPG-Daten vom USB-Mikroskop
    
    Args:
        duration_seconds: Aufnahmedauer in Sekunden
    
    Returns:
        red_values: Liste der durchschnittlichen Rotwerte
        timestamps: Zeitstempel für jeden Frame
    """
    cap = cv2.VideoCapture(0)
    
    if not cap.isOpened():
        print("Fehler: Kamera konnte nicht geöffnet werden")
        return None, None
    
    # FPS der Kamera ermitteln
    fps = cap.get(cv2.CAP_PROP_FPS)
    if fps == 0:  # Fallback falls FPS nicht erkannt wird
        fps = 30
    
    print(f"Kamera läuft mit {fps} FPS")
    print(f"Sammle {duration_seconds} Sekunden Daten...")
    print("Legt euren Finger ruhig vor das Mikroskop!")
    
    red_values = []
    timestamps = []
    start_time = time.time()
    
    # Countdown
    for i in range(3, 0, -1):
        print(f"Start in {i}...")
        time.sleep(1)
    
    print("Aufnahme läuft!")
    
    while len(red_values) < duration_seconds * fps:
        ret, frame = cap.read()
        if not ret:
            break
        
        # Durchschnittlicher Rotwert berechnen
        # OpenCV verwendet BGR-Format, also Index 2 = Rot
        red_mean = np.mean(frame[:, :, 2])
        red_values.append(red_mean)
        
        # Zeitstempel
        current_time = time.time() - start_time
        timestamps.append(current_time)
        
        # Live-Anzeige mit aktuellem Rotwert
        cv2.putText(frame, f"Rot: {red_mean:.1f}", (10, 30), 
                   cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
        cv2.imshow('PPG-Aufnahme', frame)
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()
    
    print(f"Aufnahme beendet. {len(red_values)} Frames gesammelt.")
    return red_values, timestamps

# Daten sammeln
red_values, timestamps = collect_ppg_data(20)

# Rohsignal plotten
plt.figure(figsize=(12, 4))
plt.plot(timestamps, red_values)
plt.xlabel('Zeit (s)')
plt.ylabel('Rotwert')
plt.title('PPG-Rohsignal vom USB-Mikroskop')
plt.grid(True)
plt.show()

Signalverarbeitung und Pulsberechnung

Verwenden wir dieselben Techniken wie in Lektion 3:

PythonLoading editor…
def bandpass_filter(data, lowcut, highcut, fs, order=4):
    """Bandpass-Filter 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)

def calculate_heart_rate(red_values, fps):
    """
    Berechnet Herzfrequenz aus PPG-Signal
    
    Args:
        red_values: Liste der Rotwerte
        fps: Bildrate der Aufnahme
    
    Returns:
        heart_rate: Herzfrequenz in bpm
        frequencies: Frequenz-Array für FFT
        fft_magnitude: FFT-Magnitude für Plotting
    """
    # Bandpass-Filter: 0.7-3 Hz (42-180 bpm)
    filtered_signal = bandpass_filter(red_values, 0.7, 3.0, fps)
    
    # FFT berechnen
    fft_result = np.fft.fft(filtered_signal)
    fft_magnitude = np.abs(fft_result)
    frequencies = np.fft.fftfreq(len(filtered_signal), 1/fps)
    
    # Nur positive Frequenzen im relevanten Bereich
    valid_indices = (frequencies > 0.7) & (frequencies < 3.0)
    valid_freqs = frequencies[valid_indices]
    valid_magnitudes = fft_magnitude[valid_indices]
    
    # Peak finden
    peak_index = np.argmax(valid_magnitudes)
    peak_frequency = valid_freqs[peak_index]
    
    # In bpm umrechnen
    heart_rate = peak_frequency * 60
    
    return heart_rate, frequencies, fft_magnitude, filtered_signal

# Herzfrequenz berechnen
if red_values:
    fps = len(red_values) / timestamps[-1]  # Tatsächliche FPS berechnen
    heart_rate, freqs, fft_mag, filtered = calculate_heart_rate(red_values, fps)
    
    print(f"Geschätzte Herzfrequenz: {heart_rate:.0f} bpm")
    
    # Visualisierung
    fig, axes = plt.subplots(3, 1, figsize=(12, 10))
    
    # Rohsignal
    axes[0].plot(timestamps, red_values, 'b-', alpha=0.7)
    axes[0].set_title('Rohsignal')
    axes[0].set_ylabel('Rotwert')
    axes[0].grid(True)
    
    # Gefiltertes Signal
    axes[1].plot(timestamps, filtered, 'r-')
    axes[1].set_title('Gefiltertes Signal (0.7-3 Hz)')
    axes[1].set_ylabel('Amplitude')
    axes[1].set_xlabel('Zeit (s)')
    axes[1].grid(True)
    
    # FFT
    positive_freqs = freqs[freqs > 0]
    positive_fft = fft_mag[freqs > 0]
    axes[2].plot(positive_freqs * 60, positive_fft)  # Frequenz in bpm
    axes[2].axvline(heart_rate, color='r', linestyle='--', 
                   label=f'Peak: {heart_rate:.0f} bpm')
    axes[2].set_xlim(40, 180)
    axes[2].set_title('Frequenzspektrum')
    axes[2].set_xlabel('Frequenz (bpm)')
    axes[2].set_ylabel('Amplitude')
    axes[2].legend()
    axes[2].grid(True)
    
    plt.tight_layout()
    plt.show()

Vergleich verschiedener Körperstellen

Jetzt testen wir systematisch verschiedene Messstellen:

PythonLoading editor…
def compare_body_locations():
    """Vergleicht PPG-Signalqualität an verschiedenen Körperstellen"""
    locations = ['Fingerkuppe', 'Ohrläppchen', 'Handgelenk']
    results = {}
    
    for location in locations:
        print(f"\n=== Messung: {location} ===")
        print(f"Positioniert das Mikroskop über eurem {location}")
        input("Drückt Enter wenn bereit...")
        
        # Kurze Messung (15 Sekunden)
        red_values, timestamps = collect_ppg_data(15)
        
        if red_values:
            fps = len(red_values) / timestamps[-1]
            heart_rate, _, _, filtered = calculate_heart_rate(red_values, fps)
            
            # Signalqualität bewerten
            signal_std = np.std(filtered)  # Standardabweichung als Qualitätsmass
            
            results[location] = {
                'heart_rate': heart_rate,
                'signal_quality': signal_std,
                'raw_signal': red_values,
                'filtered_signal': filtered,
                'timestamps': timestamps
            }
            
            print(f"Herzfrequenz: {heart_rate:.0f} bpm")
            print(f"Signalqualität: {signal_std:.2f}")
    
    return results

# Vergleichsmessungen durchführen
print("Wir testen jetzt verschiedene Körperstellen!")
print("Haltet bei jeder Messung den Körperteil ruhig.")

comparison_results = compare_body_locations()

# Ergebnisse visualisieren
if comparison_results:
    fig, axes = plt.subplots(len(comparison_results), 2, 
                           figsize=(14, 4*len(comparison_results)))
    
    if len(comparison_results) == 1:
        axes = axes.reshape(1, -1)
    
    for i, (location, data) in enumerate(comparison_results.items()):
        # Rohsignal
        axes[i, 0].plot(data['timestamps'], data['raw_signal'])
        axes[i, 0].set_title(f'{location} - Rohsignal')
        axes[i, 0].set_ylabel('Rotwert')
        axes[i, 0].grid(True)
        
        # Gefiltertes Signal
        axes[i, 1].plot(data['timestamps'], data['filtered_signal'])
        axes[i, 1].set_title(f'{location} - Gefiltert ({data["heart_rate"]:.0f} bpm)')
        axes[i, 1].set_ylabel('Amplitude')
        axes[i, 1].grid(True)
    
    axes[-1, 0].set_xlabel('Zeit (s)')
    axes[-1, 1].set_xlabel('Zeit (s)')
    
    plt.tight_layout()
    plt.show()
    
    # Zusammenfassung
    print("\n=== VERGLEICH ===")
    for location, data in comparison_results.items():
        print(f"{location:12}: {data['heart_rate']:3.0f} bpm, "
              f"Qualität: {data['signal_quality']:5.2f}")

Diskussion und Verbesserungen

Reflexionsfragen
  1. Welche Körperstelle lieferte das klarste Signal?
  2. Warum könnten manche Stellen besser funktionieren als andere?
  3. Wie unterscheidet sich die Signalqualität vom Smartphone-Setup?

Faktoren für Signalqualität

Anatomische Faktoren:

  • Durchblutung: Fingerkuppen und Ohrläppchen haben viele kleine Blutgefässe
  • Hautdicke: Dünne Haut lässt mehr Licht durch
  • Bewegung: Ruhigere Körperteile geben stabilere Signale

Technische Faktoren:

  • Beleuchtungsstärke: Konstante LED vs. schwankender Smartphone-Blitz
  • Abstand: Optimaler Fokus für beste Lichtausbeute
  • Aufnahmequalität: Höhere Bildrate = bessere Zeitauflösung
Verbesserungsvorschläge
  • Infrarot-LED: Dringt tiefer in die Haut ein
  • Mehrere Wellenlängen: Kombination aus rotem und infrarotem Licht
  • Bewegungskompensation: Algorithmen zur Erkennung von Bewegungsartefakten
  • Längere Messung: 60+ Sekunden für stabilere FFT-Ergebnisse

Code-Erweiterungen für Fortgeschrittene

PythonLoading editor…
def advanced_ppg_analysis(red_values, fps):
    """Erweiterte PPG-Analyse mit zusätzlichen Metriken"""
    
    # Herzratenvariabilität (HRV) schätzen
    filtered_signal = bandpass_filter(red_values, 0.7, 3.0, fps)
    
    # Peak-Detection für R-R-Intervalle
    from scipy.signal import find_peaks
    
    peaks, _ = find_peaks(filtered_signal, height=np.std(filtered_signal))
    
    if len(peaks) > 1:
        # R-R-Intervalle in Millisekunden
        rr_intervals = np.diff(peaks) / fps * 1000
        
        # HRV-Metriken
        rmssd = np.sqrt(np.mean(np.diff(rr_intervals)**2))  # RMSSD
        hr_variability = np.std(rr_intervals)
        
        print(f"Gefundene Herzschläge: {len(peaks)}")
        print(f"Mittleres R-R-Intervall: {np.mean(rr_intervals):.0f} ms")
        print(f"RMSSD (HRV): {rmssd:.1f} ms")
        print(f"HR-Variabilität: {hr_variability:.1f} ms")
        
        # Visualisierung
        plt.figure(figsize=(12, 6))
        time_axis = np.arange(len(filtered_signal)) / fps
        plt.plot(time_axis, filtered_signal)
        plt.plot(peaks / fps, filtered_signal[peaks], 'ro', markersize=8)
        plt.xlabel('Zeit (s)')
        plt.ylabel('PPG-Amplitude')
        plt.title('Peak-Detection für Herzschlag-Intervalle')
        plt.grid(True)
        plt.show()
        
        return rr_intervals
    else:
        print("Zu wenige Peaks für HRV-Analyse gefunden")
        return None

# Für Fortgeschrittene: HRV-Analyse
if red_values:
    rr_intervals = advanced_ppg_analysis(red_values, fps)
Aufgaben

Basis-Aufgaben:

  1. Führt Messungen an mindestens zwei verschiedenen Körperstellen durch
  2. Vergleicht die Herzfrequenz mit manueller Pulsmessung (15 Sek × 4)
  3. Dokumentiert, welche Stelle das beste Signal liefert

Erweiterte Aufgaben:
4. Testet verschiedene Beleuchtungsstärken am Mikroskop
5. Experimentiert mit verschiedenen Abständen zur Haut
6. Implementiert eine Live-Anzeige der aktuellen Herzfrequenz

Für Profis:
7. Versucht die Peak-Detection zu verbessern
8. Implementiert eine einfache Bewegungserkennung
9. Vergleicht euer System mit einer Smartwatch oder Fitness-App

Fazit

Das USB-Mikroskop bietet uns eine stabilere und kontrollierbarere Plattform für PPG-Messungen als das Smartphone. Die konstante LED-Beleuchtung und die feste Positionierung führen zu klareren Signalen und zuverlässigeren Messungen.

Wichtige Erkenntnisse:

  • Verschiedene Körperstellen haben unterschiedliche Signalqualität
  • Kontrollierte Beleuchtung ist entscheidend für gute PPG-Messungen
  • Die richtige Positionierung und Ruhe des Messsubjekts sind kritisch

In der nächsten Lektion werden wir uns ansehen, wie wir unser System weiter verbessern und professionellere Algorithmen implementieren können.