Skip to content

Commit bde6b17

Browse files
committed
Fully working code of hearbeat_ecg.py. neurokit2 is used for r-peaks
1 parent ed0102c commit bde6b17

File tree

1 file changed

+110
-161
lines changed

1 file changed

+110
-161
lines changed

applications/heartbeat_ecg.py

Lines changed: 110 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,130 @@
1-
import sys
21
import numpy as np
3-
import time
4-
from pylsl import StreamInlet, resolve_stream
5-
from scipy.signal import butter, lfilter, find_peaks
6-
import pyqtgraph as pg # For real-time plotting
7-
from pyqtgraph.Qt import QtWidgets, QtCore # PyQt components for GUI
8-
import signal # For handling Ctrl+C
9-
from collections import deque # For creating a ring buffer
10-
11-
# Initialize global variables
12-
inlet = None
13-
data_buffer = np.zeros(2000) # Buffer to hold the last 2000 samples for ECG data
14-
15-
# Function to design a Butterworth filter
16-
def butter_filter(cutoff, fs, order=4, btype='low'):
17-
nyquist = 0.5 * fs # Nyquist frequency
18-
normal_cutoff = cutoff / nyquist
19-
b, a = butter(order, normal_cutoff, btype=btype, analog=False)
20-
return b, a
21-
22-
# Apply the Butterworth filter to the data
23-
def apply_filter(data, b, a):
24-
return lfilter(b, a, data)
25-
26-
# Function to detect heartbeats using peak detection
27-
def detect_heartbeats(ecg_data, sampling_rate):
28-
peaks, _ = find_peaks(ecg_data, distance=sampling_rate * 0.6, prominence=0.5) # Adjust as necessary
29-
return peaks
30-
31-
class DataCollector(QtCore.QThread):
32-
data_ready = QtCore.pyqtSignal(np.ndarray)
2+
from scipy.signal import butter, filtfilt
3+
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QLabel, QMainWindow, QWidget
4+
from PyQt5.QtCore import Qt
5+
from pyqtgraph import PlotWidget
6+
import pyqtgraph as pg
7+
import pylsl
8+
import neurokit2 as nk
9+
from collections import deque
10+
import sys
3311

12+
class ECGMonitor(QMainWindow):
3413
def __init__(self):
3514
super().__init__()
36-
self.running = True
37-
self.sampling_rate = None
3815

39-
def run(self):
40-
global inlet
41-
print("Looking for LSL stream...")
42-
streams = resolve_stream('name', 'BioAmpDataStream')
16+
# Set up GUI window
17+
self.setWindowTitle("Real-Time ECG Monitor")
18+
self.setGeometry(100, 100, 800, 600)
4319

44-
if not streams:
45-
print("No LSL Stream found! Exiting...")
46-
sys.exit(0)
47-
48-
inlet = StreamInlet(streams[0])
49-
self.sampling_rate = inlet.info().nominal_srate()
50-
print(f"Detected sampling rate: {self.sampling_rate} Hz")
51-
52-
# Create and design filters
53-
low_cutoff = 20.0 # 20 Hz low-pass filter
54-
self.low_b, self.low_a = butter_filter(low_cutoff, self.sampling_rate, order=4, btype='low')
55-
56-
while self.running:
57-
# Pull multiple samples at once
58-
samples, _ = inlet.pull_chunk(timeout=0.0, max_samples=10) # Pull up to 10 samples
59-
if samples:
60-
global data_buffer
61-
data_buffer = np.roll(data_buffer, -len(samples)) # Shift data left
62-
data_buffer[-len(samples):] = [sample[0] for sample in samples] # Add new samples to the end
63-
64-
filtered_data = apply_filter(data_buffer, self.low_b, self.low_a) # Low-pass Filter
65-
self.data_ready.emit(filtered_data) # Emit the filtered data for plotting
66-
67-
time.sleep(0.01)
68-
69-
def stop(self):
70-
self.running = False
71-
72-
class ECGApp(QtWidgets.QMainWindow):
73-
def __init__(self):
74-
super().__init__()
75-
76-
# Create a plot widget
77-
self.plot_widget = pg.PlotWidget(title="ECG Signal")
78-
self.setCentralWidget(self.plot_widget)
20+
self.plot_widget = PlotWidget(self)
7921
self.plot_widget.setBackground('w')
80-
81-
# Create a label to display heart rate
82-
self.heart_rate_label = QtWidgets.QLabel("Heart rate: - bpm", self)
83-
self.heart_rate_label.setStyleSheet("font-size: 20px; font-weight: bold;")
84-
self.heart_rate_label.setAlignment(QtCore.Qt.AlignCenter)
85-
86-
layout = QtWidgets.QVBoxLayout()
22+
self.plot_widget.showGrid(x=True, y=True)
23+
24+
# Add a label to display the heart rate in bold at the bottom center
25+
self.heart_rate_label = QLabel(self)
26+
self.heart_rate_label.setStyleSheet("font-size: 20px; font-weight: bold; color: black;")
27+
self.heart_rate_label.setAlignment(Qt.AlignCenter)
28+
29+
layout = QVBoxLayout()
8730
layout.addWidget(self.plot_widget)
8831
layout.addWidget(self.heart_rate_label)
8932

90-
container = QtWidgets.QWidget()
91-
container.setLayout(layout)
92-
self.setCentralWidget(container)
33+
central_widget = QWidget()
34+
central_widget.setLayout(layout)
35+
self.setCentralWidget(central_widget)
9336

94-
self.ecg_buffer = []
95-
self.r_peak_times = deque(maxlen=10) # Deque to store recent R-peak times
96-
self.peak_intervals = deque(maxlen=9) # Deque to store intervals between R-peaks
37+
# Data and buffers
38+
self.ecg_data = deque(maxlen=2500) # 10 seconds of data at 250 Hz
39+
self.time_data = deque(maxlen=2500)
40+
self.r_peaks = []
41+
self.heart_rate = None
9742

98-
self.data_collector = DataCollector() # Data collector thread
99-
self.data_collector.data_ready.connect(self.update_plot)
100-
self.data_collector.start()
101-
102-
self.time_axis = np.linspace(0, 2000/200, 2000) # Store the x-axis time window
103-
self.plot_widget.setYRange(0,1000) # Set fixed y-axis limits
104-
105-
# Time to start heart rate calculation after 10 seconds
106-
self.start_time = time.time()
107-
self.initial_period = 10 # First 10 seconds to gather enough R-peaks
108-
109-
self.total_interval_sum = 0 # To track the sum of intervals for efficient heart rate calculation
110-
111-
def update_plot(self, ecg_data):
112-
self.ecg_buffer = ecg_data # Update buffer
113-
self.plot_widget.clear()
114-
115-
# Set a fixed window (time axis) to ensure stable plotting
116-
self.plot_widget.plot(self.time_axis, self.ecg_buffer, pen='#000000') # Plot ECG data with black line
117-
self.plot_widget.setXRange(self.time_axis[0], self.time_axis[-1], padding=0)
118-
119-
heartbeats = detect_heartbeats(np.array(self.ecg_buffer), self.data_collector.sampling_rate)
120-
121-
for index in heartbeats:
122-
r_time = index / self.data_collector.sampling_rate
123-
self.r_peak_times.append(r_time)
124-
125-
if len(self.r_peak_times) > 1:
126-
# Calculate the interval between consecutive R-peaks
127-
new_interval = self.r_peak_times[-1] - self.r_peak_times[-2]
128-
129-
if len(self.peak_intervals) == self.peak_intervals.maxlen:
130-
# Remove the oldest interval from the sum
131-
oldest_interval = self.peak_intervals.popleft()
132-
self.total_interval_sum -= oldest_interval #Minus the oldest
133-
134-
# Add the new interval to the deque and update the sum
135-
self.peak_intervals.append(new_interval)
136-
self.total_interval_sum += new_interval # Plus the new
137-
138-
# Plot detected R-peaks
139-
r_peak_times = self.time_axis[heartbeats]
140-
r_peak_scatter = pg.ScatterPlotItem(x=r_peak_times, y=self.ecg_buffer[heartbeats],
141-
symbol='o', size=10, pen='r', brush='r')
142-
self.plot_widget.addItem(r_peak_scatter)
143-
144-
# Start heart rate calculation after 10 seconds
145-
if time.time() - self.start_time >= self.initial_period:
146-
self.update_heart_rate()
43+
# Set up LSL stream inlet
44+
print("Looking for an ECG stream...")
45+
streams = pylsl.resolve_stream('name', 'BioAmpDataStream')
46+
if not streams:
47+
print("No LSL stream found!")
48+
sys.exit(0)
49+
self.inlet = pylsl.StreamInlet(streams[0])
14750

148-
def update_heart_rate(self):
149-
if len(self.peak_intervals) > 0:
150-
# Efficiently calculate the heart rate using the sum of intervals
151-
avg_interval = self.total_interval_sum / len(self.peak_intervals)
152-
bpm = 60 / avg_interval # Convert to beats per minute
153-
self.heart_rate_label.setText(f"Heart rate: {bpm:.2f} bpm")
51+
# Sampling rate
52+
self.sampling_rate = self.inlet.info().nominal_srate()
53+
if self.sampling_rate == pylsl.IRREGULAR_RATE:
54+
print("Irregular sampling rate detected!")
55+
sys.exit(0)
56+
print(f"Sampling rate: {self.sampling_rate} Hz")
57+
58+
# Timer for updating the GUI
59+
self.timer = pg.QtCore.QTimer()
60+
self.timer.timeout.connect(self.update_plot)
61+
self.timer.start(10) # Update every 10 ms
62+
63+
# Low-pass filter coefficients
64+
self.b, self.a = butter(4, 20.0 / (0.5 * self.sampling_rate), btype='low')
65+
66+
# Plot configuration
67+
self.plot_window = 10 # Plot window of 10 seconds
68+
self.buffer_size = self.plot_window * self.sampling_rate # 10 seconds at 250 Hz sampling rate
69+
70+
# Set y-axis limits based on sampling rate
71+
if self.sampling_rate == 250:
72+
self.plot_widget.setYRange(0, 2**10) #for R3
73+
elif self.sampling_rate == 500:
74+
self.plot_widget.setYRange(0, 2**14) #for R4
75+
76+
def update_plot(self):
77+
samples, timestamps = self.inlet.pull_chunk(timeout=0.0, max_samples=32)
78+
if samples:
79+
for sample, timestamp in zip(samples, timestamps):
80+
self.ecg_data.append(sample[0])
81+
self.time_data.append(timestamp)
82+
83+
# Convert deque to numpy array for processing
84+
ecg_array = np.array(self.ecg_data)
85+
filtered_ecg = filtfilt(self.b, self.a, ecg_array) # Apply low-pass filter
86+
self.r_peaks = self.detect_r_peaks(filtered_ecg) # Detect R-peaks using NeuroKit2
87+
self.calculate_heart_rate() # Calculate heart rate
88+
89+
# Update plot immediately with whatever data is available
90+
self.plot_widget.clear()
91+
current_time = np.linspace(0, len(ecg_array)/self.sampling_rate, len(ecg_array))
92+
self.plot_widget.setXRange(0, self.plot_window) # Fixed x-axis range
93+
self.plot_widget.plot(current_time, filtered_ecg, pen=pg.mkPen('k', width=1))
94+
95+
# Mark R-peaks on the plot
96+
if len(self.r_peaks) > 0:
97+
self.plot_widget.plot(current_time[self.r_peaks], filtered_ecg[self.r_peaks], pen=None, symbol='o', symbolBrush='r')
98+
99+
# Update heart rate display
100+
if self.heart_rate:
101+
self.heart_rate_label.setText(f"Heart Rate: {int(self.heart_rate)} BPM")
102+
else:
103+
self.heart_rate_label.setText("Heart Rate: Calculating...")
154104
else:
155-
self.heart_rate_label.setText("Heart rate: 0 bpm")
105+
self.heart_rate_label.setText("Heart Rate: Collecting data...")
156106

157-
def closeEvent(self, event):
158-
self.data_collector.stop() # Stop the data collector thread on close
159-
self.data_collector.wait() # Wait for the thread to finish
160-
event.accept() # Accept the close event
107+
def detect_r_peaks(self, ecg_signal):
108+
# Using NeuroKit2 to detect R-peaks
109+
r_peaks = nk.ecg_findpeaks(ecg_signal, sampling_rate=self.sampling_rate)
161110

162-
def signal_handler(sig, frame):
163-
print("Exiting...")
164-
QtWidgets.QApplication.quit()
111+
if 'ECG_R_Peaks' in r_peaks:
112+
return r_peaks['ECG_R_Peaks']
113+
else:
114+
print("No R-peaks detected. Please check the input ECG signal.")
115+
return []
116+
117+
def calculate_heart_rate(self):
118+
if len(self.r_peaks) > 1:
119+
peak_times = np.array([self.time_data[i] for i in self.r_peaks])
120+
rr_intervals = np.diff(peak_times)
121+
avg_rr_interval = np.mean(rr_intervals)
122+
self.heart_rate = 60.0 / avg_rr_interval
123+
else:
124+
self.heart_rate = None
165125

166126
if __name__ == "__main__":
167-
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
168-
169-
streams = resolve_stream('name', 'BioAmpDataStream')
170-
if not streams:
171-
print("No LSL Stream found! Exiting...")
172-
sys.exit(0)
173-
174-
app = QtWidgets.QApplication(sys.argv)
175-
176-
window = ECGApp()
177-
window.setWindowTitle("Real-Time ECG Monitoring")
178-
window.resize(800, 600)
127+
app = QApplication(sys.argv)
128+
window = ECGMonitor()
179129
window.show()
180-
181130
sys.exit(app.exec_())

0 commit comments

Comments
 (0)