1
+ # import sys
2
+ # 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)
33
+
34
+ # def __init__(self):
35
+ # super().__init__()
36
+ # self.running = True
37
+ # self.sampling_rate = None
38
+
39
+ # def run(self):
40
+ # global inlet
41
+ # print("Looking for LSL stream...")
42
+ # streams = resolve_stream('name', 'BioAmpDataStream')
43
+
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 # 40 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)
79
+ # 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()
87
+ # layout.addWidget(self.plot_widget)
88
+ # layout.addWidget(self.heart_rate_label)
89
+
90
+ # container = QtWidgets.QWidget()
91
+ # container.setLayout(layout)
92
+ # self.setCentralWidget(container)
93
+
94
+ # self.ecg_buffer = [] # Buffer to hold the ECG data
95
+ # self.r_peak_times = deque(maxlen=15) # Ring buffer for R-peak timestamps within 15 seconds
96
+ # self.peak_indices = deque() # Stores R-peak indices for the current 15-second window
97
+
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,800) # Set fixed y-axis limits
104
+
105
+ # # Timer to update heart rate every 15 seconds
106
+ # self.timer = QtCore.QTimer(self)
107
+ # self.timer.timeout.connect(self.calculate_and_reset_heart_rate)
108
+ # self.timer.start(15000) # 15 seconds
109
+
110
+ # def update_plot(self, ecg_data):
111
+ # self.ecg_buffer = ecg_data # Update buffer
112
+ # self.plot_widget.clear()
113
+
114
+ # # Set a fixed window (time axis) to ensure stable plotting
115
+ # self.plot_widget.plot(self.time_axis, self.ecg_buffer, pen='#000000') # Plot ECG data with black line
116
+ # self.plot_widget.setXRange(self.time_axis[0], self.time_axis[-1], padding=0)
117
+
118
+ # # Detect heartbeats in real time
119
+ # heartbeats = detect_heartbeats(np.array(self.ecg_buffer), self.data_collector.sampling_rate)
120
+ # # print(f"Detected R-peaks at indices: {heartbeats}")
121
+
122
+ # # Append the actual time of detection for each detected peak
123
+ # for index in heartbeats:
124
+ # # Append the time based on the index and the sampling rate
125
+ # self.r_peak_times.append(index / self.data_collector.sampling_rate)
126
+ # self.peak_indices.append(index) # Store peak indices for this 10-second window
127
+
128
+ # # Calculate the x-coordinates (time axis) for the R-peaks
129
+ # r_peak_times = self.time_axis[heartbeats]
130
+
131
+ # # Plot the R-peaks as red circles
132
+ # r_peak_scatter = pg.ScatterPlotItem(x=r_peak_times, y=self.ecg_buffer[heartbeats],
133
+ # symbol='o', size=10, pen='r', brush='r')
134
+ # self.plot_widget.addItem(r_peak_scatter) # Add scatter plot to the ECG plot
135
+
136
+ # def calculate_and_reset_heart_rate(self):
137
+ # if len(self.r_peak_times) > 1:
138
+ # # Calculate the time intervals between successive R-peaks
139
+ # intervals = np.diff(self.r_peak_times)
140
+
141
+ # print(f"R-peak times: {self.r_peak_times}") # Log R-peak times
142
+ # print(f"Intervals between peaks: {intervals}") # Log intervals
143
+
144
+ # # Filter intervals that are positive and non-zero to avoid issues
145
+ # valid_intervals = intervals[intervals > 0]
146
+
147
+ # if len(valid_intervals) > 0:
148
+ # bpm = 60 / np.mean(valid_intervals) # Mean interval is in seconds, converting to bpm
149
+ # self.heart_rate_label.setText(f"Heart rate: {bpm:.2f} bpm")
150
+ # else:
151
+ # self.heart_rate_label.setText("Heart rate: 0 bpm")
152
+ # else:
153
+ # self.heart_rate_label.setText("Heart rate: 0 bpm")
154
+
155
+ # # Clear the deque for the next 15-second window
156
+ # self.r_peak_times.clear()
157
+ # self.peak_indices.clear()
158
+
159
+ # def closeEvent(self, event):
160
+ # self.data_collector.stop() # Stop the data collector thread on close
161
+ # self.data_collector.wait() # Wait for the thread to finish
162
+ # event.accept() # Accept the close event
163
+
164
+ # def signal_handler(sig, frame):
165
+ # print("Exiting...")
166
+ # QtWidgets.QApplication.quit()
167
+
168
+ # if __name__ == "__main__":
169
+ # signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
170
+
171
+ # streams = resolve_stream('name', 'BioAmpDataStream')
172
+ # if not streams:
173
+ # print("No LSL Stream found! Exiting...")
174
+ # sys.exit(0)
175
+
176
+ # app = QtWidgets.QApplication(sys.argv)
177
+
178
+ # window = ECGApp()
179
+ # window.setWindowTitle("Real-Time ECG Monitoring")
180
+ # window.resize(800, 600)
181
+ # window.show()
182
+
183
+ # sys.exit(app.exec_())
184
+
1
185
import sys
2
186
import numpy as np
3
187
import time
@@ -50,7 +234,7 @@ def run(self):
50
234
print (f"Detected sampling rate: { self .sampling_rate } Hz" )
51
235
52
236
# Create and design filters
53
- low_cutoff = 20.0 # 40 Hz low-pass filter
237
+ low_cutoff = 20.0 # 20 Hz low-pass filter
54
238
self .low_b , self .low_a = butter_filter (low_cutoff , self .sampling_rate , order = 4 , btype = 'low' )
55
239
56
240
while self .running :
@@ -91,21 +275,22 @@ def __init__(self):
91
275
container .setLayout (layout )
92
276
self .setCentralWidget (container )
93
277
94
- self .ecg_buffer = [] # Buffer to hold the ECG data
95
- self .r_peak_times = deque (maxlen = 15 ) # Ring buffer for R-peak timestamps within 15 seconds
96
- self .peak_indices = deque () # Stores R-peak indices for the current 15-second window
278
+ self .ecg_buffer = []
279
+ self .r_peak_times = deque (maxlen = 10 ) # Deque to store recent R-peak times
280
+ self .peak_intervals = deque (maxlen = 9 ) # Deque to store intervals between R-peaks
97
281
98
282
self .data_collector = DataCollector () # Data collector thread
99
283
self .data_collector .data_ready .connect (self .update_plot )
100
284
self .data_collector .start ()
101
285
102
286
self .time_axis = np .linspace (0 , 2000 / 200 , 2000 ) # Store the x-axis time window
103
- self .plot_widget .setYRange (0 ,800 ) # Set fixed y-axis limits
287
+ self .plot_widget .setYRange (0 ,1000 ) # Set fixed y-axis limits
288
+
289
+ # Time to start heart rate calculation after 10 seconds
290
+ self .start_time = time .time ()
291
+ self .initial_period = 10 # First 10 seconds to gather enough R-peaks
104
292
105
- # Timer to update heart rate every 15 seconds
106
- self .timer = QtCore .QTimer (self )
107
- self .timer .timeout .connect (self .calculate_and_reset_heart_rate )
108
- self .timer .start (15000 ) # 15 seconds
293
+ self .total_interval_sum = 0 # To track the sum of intervals for efficient heart rate calculation
109
294
110
295
def update_plot (self , ecg_data ):
111
296
self .ecg_buffer = ecg_data # Update buffer
@@ -115,47 +300,44 @@ def update_plot(self, ecg_data):
115
300
self .plot_widget .plot (self .time_axis , self .ecg_buffer , pen = '#000000' ) # Plot ECG data with black line
116
301
self .plot_widget .setXRange (self .time_axis [0 ], self .time_axis [- 1 ], padding = 0 )
117
302
118
- # Detect heartbeats in real time
119
303
heartbeats = detect_heartbeats (np .array (self .ecg_buffer ), self .data_collector .sampling_rate )
120
- # print(f"Detected R-peaks at indices: {heartbeats}")
121
304
122
- # Append the actual time of detection for each detected peak
123
305
for index in heartbeats :
124
- # Append the time based on the index and the sampling rate
125
- self .r_peak_times .append (index / self .data_collector .sampling_rate )
126
- self .peak_indices .append (index ) # Store peak indices for this 10-second window
306
+ r_time = index / self .data_collector .sampling_rate
307
+ self .r_peak_times .append (r_time )
127
308
128
- # Calculate the x-coordinates (time axis) for the R-peaks
129
- r_peak_times = self .time_axis [heartbeats ]
309
+ if len (self .r_peak_times ) > 1 :
310
+ # Calculate the interval between consecutive R-peaks
311
+ new_interval = self .r_peak_times [- 1 ] - self .r_peak_times [- 2 ]
130
312
131
- # Plot the R-peaks as red circles
132
- r_peak_scatter = pg .ScatterPlotItem (x = r_peak_times , y = self .ecg_buffer [heartbeats ],
133
- symbol = 'o' , size = 10 , pen = 'r' , brush = 'r' )
134
- self .plot_widget .addItem (r_peak_scatter ) # Add scatter plot to the ECG plot
135
-
136
- def calculate_and_reset_heart_rate (self ):
137
- if len (self .r_peak_times ) > 1 :
138
- # Calculate the time intervals between successive R-peaks
139
- intervals = np .diff (self .r_peak_times )
140
-
141
- print (f"R-peak times: { self .r_peak_times } " ) # Log R-peak times
142
- print (f"Intervals between peaks: { intervals } " ) # Log intervals
313
+ if len (self .peak_intervals ) == self .peak_intervals .maxlen :
314
+ # Remove the oldest interval from the sum
315
+ oldest_interval = self .peak_intervals .popleft ()
316
+ self .total_interval_sum -= oldest_interval #Minus the oldest
143
317
144
- # Filter intervals that are positive and non-zero to avoid issues
145
- valid_intervals = intervals [intervals > 0 ]
318
+ # Add the new interval to the deque and update the sum
319
+ self .peak_intervals .append (new_interval )
320
+ self .total_interval_sum += new_interval # Plus the new
146
321
147
- if len (valid_intervals ) > 0 :
148
- bpm = 60 / np .mean (valid_intervals ) # Mean interval is in seconds, converting to bpm
149
- self .heart_rate_label .setText (f"Heart rate: { bpm :.2f} bpm" )
150
- else :
151
- self .heart_rate_label .setText ("Heart rate: 0 bpm" )
322
+ # Plot detected R-peaks
323
+ r_peak_times = self .time_axis [heartbeats ]
324
+ r_peak_scatter = pg .ScatterPlotItem (x = r_peak_times , y = self .ecg_buffer [heartbeats ],
325
+ symbol = 'o' , size = 10 , pen = 'r' , brush = 'r' )
326
+ self .plot_widget .addItem (r_peak_scatter )
327
+
328
+ # Start heart rate calculation after 10 seconds
329
+ if time .time () - self .start_time >= self .initial_period :
330
+ self .update_heart_rate ()
331
+
332
+ def update_heart_rate (self ):
333
+ if len (self .peak_intervals ) > 0 :
334
+ # Efficiently calculate the heart rate using the sum of intervals
335
+ avg_interval = self .total_interval_sum / len (self .peak_intervals )
336
+ bpm = 60 / avg_interval # Convert to beats per minute
337
+ self .heart_rate_label .setText (f"Heart rate: { bpm :.2f} bpm" )
152
338
else :
153
339
self .heart_rate_label .setText ("Heart rate: 0 bpm" )
154
340
155
- # Clear the deque for the next 15-second window
156
- self .r_peak_times .clear ()
157
- self .peak_indices .clear ()
158
-
159
341
def closeEvent (self , event ):
160
342
self .data_collector .stop () # Stop the data collector thread on close
161
343
self .data_collector .wait () # Wait for the thread to finish
0 commit comments