Inżynieria

Wykres EKG i oddechu w Pythonie

Author avatar

Jakub Domaszewicz

Ostatnia aktualizacja3 grudnia 2021

Wykres EKG i oddechu w Pythonie

Narysuj EKG i oddech na wspólnej osi czasu z Aidlab Python SDK i matplotlib. Zapisz próbki w CSV i pobierz pomiary z pamięci urządzenia.

W tym poradniku narysujesz na żywo EKG i krzywą oddechową z Aidlaba. Zapiszesz też próbki w pliku CSV. Znaczniki czasu z urządzenia pozwolą umieścić oba sygnały na wspólnej osi czasu.

Użyjemy Aidlab Python SDK i matplotlib. Na końcu pokażemy również, jak pobrać pomiary zapisane w pamięci urządzenia.

Czego potrzebujesz

Potrzebujesz Pythona 3.10 lub nowszego, komputera z Bluetooth 4.0+ i Aidlaba obsługującego EKG oraz pomiar oddechu. Do zapisu offline potrzebny jest model z pamięcią pomiarów. Sprawdź pojemności pamięci Aidlab 1.

Uruchom skrypt na komputerze, na którym matplotlib może otworzyć okno wykresu, na przykład przez TkAgg, QtAgg lub MacOSX. Zainstaluj potrzebne pakiety:

python -m pip install aidlabsdk matplotlib

Załóż pas tak, aby elektrody stykały się ze skórą. Próbki pojawią się, gdy urządzenie wykryje, że jest założone. Jeśli w pobliżu jest kilka urządzeń, wybierz swoje z wyników skanowania zamiast polegać na pierwszej pozycji.

Co zawierają oba sygnały

Sygnał Wartość z SDK Co pokazuje wykres
DataType.ECG Przebieg elektryczny w woltach Te same próbki przeliczone na miliwolty do wyświetlenia
DataType.RESPIRATION Przebieg impedancji w omach Zmiany krzywej oddechowej w czasie

Krzywa oddechowa pokazuje zmiany impedancji klatki piersiowej podczas oddychania. Aby uzyskać liczbę oddechów na minutę, osobno wybierz DataType.RESPIRATION_RATE. Amplituda krzywej jest pomiarem impedancji, a nie skalibrowaną objętością powietrza w litrach. Zobacz typy danych i częstotliwości próbkowania.

Kompletny wykres na żywo z eksportem CSV

Zapisz poniższy kod jako respiration_plotting.py. Zbiera oba sygnały, rysuje ostatnie dziesięć sekund każdego z nich i zapisuje wszystkie odebrane próbki w nowym pliku CSV.

import asyncio
import csv
from collections import deque
from datetime import datetime

import matplotlib.pyplot as plt
from aidlab import AidlabManager, DataType, DeviceDelegate


class Recorder(DeviceDelegate):
    def __init__(self, writer):
        self.writer = writer
        self.samples = {"ecg": deque(), "respiration": deque()}
        self.first_timestamp = None

    def add_sample(self, signal, timestamp, value):
        self.writer.writerow([signal, timestamp, value, "live"])
        if self.first_timestamp is None:
            self.first_timestamp = timestamp
        samples = self.samples[signal]
        samples.append((timestamp, value))
        while samples and timestamp - samples[0][0] > 10_000:
            samples.popleft()

    def did_receive_ecg(self, device, timestamp, value):
        self.add_sample("ecg", timestamp, value)

    def did_receive_respiration(self, device, timestamp, value):
        self.add_sample("respiration", timestamp, value)

    def did_disconnect(self, device, reason):
        print("Disconnected:", reason)


async def main():
    devices = await AidlabManager().scan()
    if not devices:
        print("No device found. Turn Aidlab on and check Bluetooth.")
        return

    device = devices[0]
    print("Connecting to:", device.name, device.address)
    filename = f"signals-{datetime.now():%Y%m%d-%H%M%S-%f}.csv"
    plt.ion()
    figure, axes = plt.subplots(2, 1, sharex=True, layout="constrained")
    lines = [axis.plot([], [])[0] for axis in axes]
    axes[0].set_ylabel("ECG (mV)")
    axes[1].set_ylabel("Respiration (Ω)")
    axes[1].set_xlabel("Time since first sample (s)")
    plt.show(block=False)

    with open(filename, "x", newline="") as output:
        writer = csv.writer(output)
        writer.writerow(["signal", "timestamp_ms", "value", "source"])
        recorder = Recorder(writer)
        try:
            await device.connect(recorder)
            signals = [DataType.ECG, DataType.RESPIRATION]
            await device.collect(signals, [])
            print("Recording to:", filename, "— close the plot to finish.")
            while device.is_connected and plt.fignum_exists(figure.number):
                for axis, line, signal, scale in zip(
                    axes, lines, ("ecg", "respiration"), (1000, 1)
                ):
                    samples = recorder.samples[signal]
                    if samples:
                        line.set_data(
                            [(ts - recorder.first_timestamp) / 1000 for ts, _ in samples],
                            [value * scale for _, value in samples],
                        )
                        axis.relim()
                        axis.autoscale_view()
                output.flush()
                figure.canvas.draw_idle()
                figure.canvas.flush_events()
                await asyncio.sleep(0.1)
        finally:
            await device.disconnect()
            plt.close(figure)

    print("Saved:", filename)


if __name__ == "__main__":
    asyncio.run(main())

Uruchom go z terminala:

python respiration_plotting.py

Górny wykres pokazuje EKG, a dolny — oddech. Krzywa oddechowa będzie podobna do dotychczasowego przykładu poniżej; jej kształt i amplituda zależą od kontaktu pasa ze skórą, oddychania i ruchu.

Krzywa oddechowa zarejestrowana za pomocą Aidlaba

Wykres odświeża się około dziesięciu razy na sekundę. Ten odstęp steruje wyświetlaniem, a nie częstotliwością próbkowania urządzenia. Aktualizujemy dwa istniejące obiekty linii; kolejki przechowują tylko dziesięciosekundowe okno, a CSV — całą sesję. Inne sposoby rysowania opisuje przewodnik po interaktywnych wykresach matplotlib.

Wspólna oś czasu dla EKG i oddechu

Do umieszczenia próbki na osi czasu używaj jej timestamp, wyrażonego w milisekundach czasu Unix. Czas wywołania callbacka może różnić się od czasu pomiaru, ponieważ próbki mogą być dostarczane partiami.

Traktuj oba sygnały jako osobne serie. Ich częstotliwości próbkowania mogą się różnić, więc łączenie próbek EKG i oddechu według indeksu na liście zniekształciłoby oś czasu. W tym przykładzie oba wykresy mają wspólny początek czasu i oś x. Nie interpolujemy ani nie zmieniamy częstotliwości próbkowania.

W pliku CSV próbki EKG pozostają w woltach, a sygnał oddechowy — w omach. Plik zawiera następujące kolumny:

Kolumna Znaczenie
signal ecg lub respiration
timestamp_ms Czas pomiaru w milisekundach czasu Unix
value Oryginalna wartość z SDK
source live albo memory po dodaniu rozszerzenia poniżej

Przed analizą podziel próbki na EKG i oddech, a każdą serię posortuj według timestamp_ms. Wiersze są zapisywane w kolejności wywołań callbacków, więc cały plik nie musi być uporządkowany chronologicznie.

Zapis EKG i oddechu bez komputera

Dwie listy przekazywane do collect pełnią różne funkcje: pierwsza wybiera sygnały przesyłane na żywo, a druga — zapisywane w pamięci urządzenia po rozłączeniu. Aby ustawić zapis offline EKG i oddechu, zastąp linię rozpoczynającą pomiar:

await device.collect(signals, signals)

Po wykonaniu polecenia zamknij wykres, aby się rozłączyć, i pozostaw Aidlab założony podczas zapisu. Ustawienia zapisu offline pozostają w urządzeniu po rozłączeniu.

Czas zapisu zależy od pamięci urządzenia i wybranych sygnałów. Około ośmiu godzin dla wariantu Aidlab 1 z pamięcią 64 MB dotyczy konfiguracji oficjalnej aplikacji mobilnej; własny zapis EKG i oddechu ma inne zapotrzebowanie na pamięć. Zobacz pojemność pamięci wewnętrznej.

Pobieranie zapisanego pomiaru

Przed ponownym połączeniem dodaj te metody wewnątrz klasy Recorder:

    def did_receive_past_ecg(self, device, timestamp, value):
        self.writer.writerow(["ecg", timestamp, value, "memory"])

    def did_receive_past_respiration(self, device, timestamp, value):
        self.writer.writerow(["respiration", timestamp, value, "memory"])

    def sync_state_did_change(self, device, state):
        print("Synchronization:", state.name)

Przy kolejnym uruchomieniu wróć do pomiaru na żywo przez await device.collect(signals, []). Bezpośrednio po tej linii dodaj:

await device.start_synchronization()

Wykres na żywo nadal działa, a zapisane wcześniej próbki trafiają do CSV z source=memory. Pozostaw okno otwarte do komunikatu synchronizacji END; EMPTY oznacza brak zapisanego pomiaru do pobrania. Stan STOP lub UNAVAILABLE nie oznacza zakończenia pobierania.

W firmware 3.7.84–3.7.110 rozpoczęcie pomiaru automatycznie uruchamia synchronizację: pomiń osobne wywołanie start_synchronization(). Przewodnik synchronizacji opisuje ten wyjątek i stany transferu. Pobranie danych i czyszczenie pamięci urządzenia to osobne operacje; ten przykład nie usuwa zapisanych próbek.

Co możesz zbudować dalej

Jeśli tworzysz aplikację sterowaną oddechem, zobacz Breathing Sky i poradnik pomiaru oddechu w Unity. Do analizy odstępów między uderzeniami wybierz pochodzące z EKG DataType.RR i przeczytaj jak mierzyć HRV.

Jeśli wybierasz sprzęt do takiego projektu, porównaj Aidlab i Polar lub Aidlab i Shimmer, a następnie sprawdź sygnały i pamięć konkretnego urządzenia.