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 # 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)
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=10) # Store up to 20 R-peaks
96
+ # self.last_update_time = time.time()
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, 700) # Set fixed y-axis limits
104
+
105
+ # def update_plot(self, ecg_data):
106
+ # self.ecg_buffer = ecg_data # Update buffer
107
+ # self.plot_widget.clear()
108
+ # # Set a fixed window (time axis) to ensure stable plotting
109
+ # self.plot_widget.plot(self.time_axis, self.ecg_buffer, pen='#000000') # Plot ECG data with black line
110
+ # self.plot_widget.setXRange(self.time_axis[0], self.time_axis[-1], padding=0)
111
+
112
+ # # Detect heartbeats in real time
113
+ # heartbeats = detect_heartbeats(np.array(self.ecg_buffer), self.data_collector.sampling_rate)
114
+
115
+ # # Mark detected R-peaks
116
+ # for index in heartbeats:
117
+ # time_point = index / self.data_collector.sampling_rate # Convert index to time
118
+ # self.r_peak_times.append(time_point) # Store the time of the detected R-peak
119
+
120
+ # # Plot a red circle at the R-peak position
121
+ # self.plot_widget.plot([self.time_axis[index]], [self.ecg_buffer[index]], pen=None, symbol='o', symbolBrush='r', symbolSize=8)
122
+
123
+ # self.calculate_heart_rate() # Calculate heart rate after detecting R-peaks
124
+
125
+ # def calculate_heart_rate(self):
126
+ # current_time = time.time()
127
+ # if current_time - self.last_update_time >= 10: # Update every 10 seconds
128
+ # if len(self.r_peak_times) > 1:
129
+ # # Calculate the time intervals between successive R-peaks
130
+ # intervals = np.diff(self.r_peak_times)
131
+ # valid_intervals = intervals[intervals > 0] # Filter positive intervals
132
+
133
+ # if len(valid_intervals) > 0:
134
+ # bpm = 60 / np.mean(valid_intervals) # Mean interval is in seconds, converting to bpm
135
+ # self.heart_rate_label.setText(f"Heart rate: {bpm:.2f} bpm")
136
+ # else:
137
+ # self.heart_rate_label.setText("Heart rate: 0 bpm")
138
+ # else:
139
+ # self.heart_rate_label.setText("Heart rate: 0 bpm")
140
+
141
+ # self.r_peak_times.clear() # Clear the deque after calculation
142
+ # self.last_update_time = current_time # Update last update time
143
+
144
+ # def closeEvent(self, event):
145
+ # self.data_collector.stop() # Stop the data collector thread on close
146
+ # self.data_collector.wait() # Wait for the thread to finish
147
+ # event.accept() # Accept the close event
148
+
149
+ # def signal_handler(sig, frame):
150
+ # print("Exiting...")
151
+ # QtWidgets.QApplication.quit()
152
+
153
+ # if __name__ == "__main__":
154
+ # signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
155
+
156
+ # streams = resolve_stream('name', 'BioAmpDataStream')
157
+ # if not streams:
158
+ # print("No LSL Stream found! Exiting...")
159
+ # sys.exit(0)
160
+
161
+ # app = QtWidgets.QApplication(sys.argv)
162
+
163
+ # window = ECGApp()
164
+ # window.setWindowTitle("Real-Time ECG Monitoring")
165
+ # window.resize(800, 600)
166
+ # window.show()
167
+
168
+ # sys.exit(app.exec_())
169
+
170
+ import sys
171
+ import numpy as np
172
+ import time
173
+ from pylsl import StreamInlet , resolve_stream
174
+ from scipy .signal import butter , lfilter , find_peaks
175
+ import pyqtgraph as pg # For real-time plotting
176
+ from pyqtgraph .Qt import QtWidgets , QtCore # PyQt components for GUI
177
+ import signal # For handling Ctrl+C
178
+ from collections import deque # For creating a ring buffer
179
+
180
+ # Initialize global variables
181
+ inlet = None
182
+ data_buffer = np .zeros (2000 ) # Buffer to hold the last 2000 samples for ECG data
183
+
184
+ # Function to design a Butterworth filter
185
+ def butter_filter (cutoff , fs , order = 4 , btype = 'low' ):
186
+ nyquist = 0.5 * fs # Nyquist frequency
187
+ normal_cutoff = cutoff / nyquist
188
+ b , a = butter (order , normal_cutoff , btype = btype , analog = False )
189
+ return b , a
190
+
191
+ # Apply the Butterworth filter to the data
192
+ def apply_filter (data , b , a ):
193
+ return lfilter (b , a , data )
194
+
195
+ # Function to detect heartbeats using peak detection
196
+ def detect_heartbeats (ecg_data , sampling_rate ):
197
+ peaks , _ = find_peaks (ecg_data , distance = sampling_rate * 0.6 , prominence = 0.5 ) # Adjust as necessary
198
+ return peaks
199
+
200
+ class DataCollector (QtCore .QThread ):
201
+ data_ready = QtCore .pyqtSignal (np .ndarray )
202
+
203
+ def __init__ (self ):
204
+ super ().__init__ ()
205
+ self .running = True
206
+ self .sampling_rate = None
207
+
208
+ def run (self ):
209
+ global inlet
210
+ print ("Looking for LSL stream..." )
211
+ streams = resolve_stream ('name' , 'BioAmpDataStream' )
212
+
213
+ if not streams :
214
+ print ("No LSL Stream found! Exiting..." )
215
+ sys .exit (0 )
216
+
217
+ inlet = StreamInlet (streams [0 ])
218
+ self .sampling_rate = inlet .info ().nominal_srate ()
219
+ print (f"Detected sampling rate: { self .sampling_rate } Hz" )
220
+
221
+ # Create and design filters
222
+ low_cutoff = 20.0 # 20 Hz low-pass filter
223
+ self .low_b , self .low_a = butter_filter (low_cutoff , self .sampling_rate , order = 4 , btype = 'low' )
224
+
225
+ while self .running :
226
+ # Pull multiple samples at once
227
+ samples , _ = inlet .pull_chunk (timeout = 0.0 , max_samples = 10 ) # Pull up to 10 samples
228
+ if samples :
229
+ global data_buffer
230
+ data_buffer = np .roll (data_buffer , - len (samples )) # Shift data left
231
+ data_buffer [- len (samples ):] = [sample [0 ] for sample in samples ] # Add new samples to the end
232
+
233
+ filtered_data = apply_filter (data_buffer , self .low_b , self .low_a ) # Low-pass Filter
234
+ self .data_ready .emit (filtered_data ) # Emit the filtered data for plotting
235
+
236
+ time .sleep (0.01 )
237
+
238
+ def stop (self ):
239
+ self .running = False
240
+
241
+ class HeartRateCalculator (QtCore .QThread ):
242
+ heart_rate_ready = QtCore .pyqtSignal (float )
243
+
244
+ def __init__ (self ):
245
+ super ().__init__ ()
246
+ self .running = True
247
+ self .r_peak_times = deque (maxlen = 20 ) # Store R-peaks times
248
+ self .last_update_time = time .time ()
249
+
250
+ def run (self ):
251
+ while self .running :
252
+ time .sleep (1 ) # Check every second
253
+ current_time = time .time ()
254
+
255
+ if current_time - self .last_update_time >= 10 : # Calculate heart rate every 10 seconds
256
+ if len (self .r_peak_times ) > 1 :
257
+ # Calculate the time intervals between successive R-peaks
258
+ intervals = np .diff (self .r_peak_times )
259
+ valid_intervals = intervals [intervals > 0 ] # Filter positive intervals
260
+
261
+ if len (valid_intervals ) > 0 :
262
+ bpm = 60 / np .mean (valid_intervals ) # Mean interval is in seconds, converting to bpm
263
+ self .heart_rate_ready .emit (bpm )
264
+ else :
265
+ self .heart_rate_ready .emit (0 ) # No valid intervals, heart rate is 0
266
+ else :
267
+ self .heart_rate_ready .emit (0 ) # Not enough R-peaks to calculate heart rate
268
+
269
+ self .r_peak_times .clear () # Clear R-peaks after calculation
270
+ self .last_update_time = current_time # Update last update time
271
+
272
+ def add_r_peak_time (self , time ):
273
+ self .r_peak_times .append (time ) # Add R-peak time to the deque
274
+
275
+ def stop (self ):
276
+ self .running = False
277
+
278
+ class ECGApp (QtWidgets .QMainWindow ):
279
+ def __init__ (self ):
280
+ super ().__init__ ()
281
+
282
+ # Create a plot widget
283
+ self .plot_widget = pg .PlotWidget (title = "ECG Signal" )
284
+ self .setCentralWidget (self .plot_widget )
285
+ self .plot_widget .setBackground ('w' )
286
+
287
+ # Create a label to display heart rate
288
+ self .heart_rate_label = QtWidgets .QLabel ("Heart rate: - bpm" , self )
289
+ self .heart_rate_label .setStyleSheet ("font-size: 20px; font-weight: bold;" )
290
+ self .heart_rate_label .setAlignment (QtCore .Qt .AlignCenter )
291
+
292
+ layout = QtWidgets .QVBoxLayout ()
293
+ layout .addWidget (self .plot_widget )
294
+ layout .addWidget (self .heart_rate_label )
295
+
296
+ container = QtWidgets .QWidget ()
297
+ container .setLayout (layout )
298
+ self .setCentralWidget (container )
299
+
300
+ self .ecg_buffer = [] # Buffer to hold the ECG data
301
+
302
+ self .data_collector = DataCollector () # Data collector thread
303
+ self .data_collector .data_ready .connect (self .update_plot )
304
+ self .data_collector .start ()
305
+
306
+ self .heart_rate_calculator = HeartRateCalculator () # Heart rate calculation thread
307
+ self .heart_rate_calculator .start ()
308
+ self .heart_rate_calculator .heart_rate_ready .connect (self .update_heart_rate )
309
+
310
+ self .time_axis = np .linspace (0 , 2000 / 200 , 2000 ) # Store the x-axis time window
311
+ self .plot_widget .setYRange (0 , 700 ) # Set fixed y-axis limits
312
+
313
+ def update_plot (self , ecg_data ):
314
+ self .ecg_buffer = ecg_data # Update buffer
315
+ self .plot_widget .clear ()
316
+ # Set a fixed window (time axis) to ensure stable plotting
317
+ self .plot_widget .plot (self .time_axis , self .ecg_buffer , pen = '#000000' ) # Plot ECG data with black line
318
+ self .plot_widget .setXRange (self .time_axis [0 ], self .time_axis [- 1 ], padding = 0 )
319
+
320
+ # Detect heartbeats in real time
321
+ heartbeats = detect_heartbeats (np .array (self .ecg_buffer ), self .data_collector .sampling_rate )
322
+
323
+ # Mark detected R-peaks and store in HeartRateCalculator
324
+ for index in heartbeats :
325
+ time_point = index / self .data_collector .sampling_rate # Convert index to time
326
+ self .heart_rate_calculator .add_r_peak_time (time_point ) # Store the time of the detected R-peak
327
+
328
+ # Plot a red circle at the R-peak position
329
+ self .plot_widget .plot ([self .time_axis [index ]], [self .ecg_buffer [index ]], pen = None , symbol = 'o' , symbolBrush = 'r' , symbolSize = 8 )
330
+
331
+ def update_heart_rate (self , bpm ):
332
+ if bpm > 0 :
333
+ self .heart_rate_label .setText (f"Heart rate: { bpm :.2f} bpm" )
334
+ else :
335
+ self .heart_rate_label .setText ("Heart rate: 0 bpm" )
336
+
337
+ def closeEvent (self , event ):
338
+ self .data_collector .stop () # Stop the data collector thread on close
339
+ self .data_collector .wait () # Wait for the thread to finish
340
+ self .heart_rate_calculator .stop () # Stop the heart rate calculator
341
+ self .heart_rate_calculator .wait () # Wait for the thread to finish
342
+ event .accept () # Accept the close event
343
+
344
+ def signal_handler (sig , frame ):
345
+ print ("Exiting..." )
346
+ QtWidgets .QApplication .quit ()
347
+
348
+ if __name__ == "__main__" :
349
+ signal .signal (signal .SIGINT , signal_handler ) # Handle Ctrl+C
350
+
351
+ streams = resolve_stream ('name' , 'BioAmpDataStream' )
352
+ if not streams :
353
+ print ("No LSL Stream found! Exiting..." )
354
+ sys .exit (0 )
355
+
356
+ app = QtWidgets .QApplication (sys .argv )
357
+
358
+ window = ECGApp ()
359
+ window .setWindowTitle ("Real-Time ECG Monitoring" )
360
+ window .resize (800 , 600 )
361
+ window .show ()
362
+
363
+ sys .exit (app .exec_ ())
0 commit comments