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
1
import sys
171
2
import numpy as np
172
3
import time
@@ -219,7 +50,7 @@ def run(self):
219
50
print (f"Detected sampling rate: { self .sampling_rate } Hz" )
220
51
221
52
# Create and design filters
222
- low_cutoff = 20.0 # 20 Hz low-pass filter
53
+ low_cutoff = 20.0 # 40 Hz low-pass filter
223
54
self .low_b , self .low_a = butter_filter (low_cutoff , self .sampling_rate , order = 4 , btype = 'low' )
224
55
225
56
while self .running :
@@ -238,43 +69,6 @@ def run(self):
238
69
def stop (self ):
239
70
self .running = False
240
71
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
72
class ECGApp (QtWidgets .QMainWindow ):
279
73
def __init__ (self ):
280
74
super ().__init__ ()
@@ -298,47 +92,73 @@ def __init__(self):
298
92
self .setCentralWidget (container )
299
93
300
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
301
97
302
- self .data_collector = DataCollector () # Data collector thread
98
+ self .data_collector = DataCollector () # Data collector thread
303
99
self .data_collector .data_ready .connect (self .update_plot )
304
100
self .data_collector .start ()
305
101
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 )
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
309
104
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
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
312
109
313
110
def update_plot (self , ecg_data ):
314
111
self .ecg_buffer = ecg_data # Update buffer
315
112
self .plot_widget .clear ()
113
+
316
114
# Set a fixed window (time axis) to ensure stable plotting
317
115
self .plot_widget .plot (self .time_axis , self .ecg_buffer , pen = '#000000' ) # Plot ECG data with black line
318
116
self .plot_widget .setXRange (self .time_axis [0 ], self .time_axis [- 1 ], padding = 0 )
319
117
320
118
# Detect heartbeats in real time
321
119
heartbeats = detect_heartbeats (np .array (self .ecg_buffer ), self .data_collector .sampling_rate )
120
+ print (f"Detected R-peaks at indices: { heartbeats } " )
322
121
323
- # Mark detected R-peaks and store in HeartRateCalculator
122
+ # Append the actual time of detection for each detected peak
324
123
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" )
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" )
334
152
else :
335
153
self .heart_rate_label .setText ("Heart rate: 0 bpm" )
336
154
155
+ # Clear the deque for the next 10-second window
156
+ self .r_peak_times .clear ()
157
+ self .peak_indices .clear ()
158
+
337
159
def closeEvent (self , event ):
338
160
self .data_collector .stop () # Stop the data collector thread on close
339
161
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
162
event .accept () # Accept the close event
343
163
344
164
def signal_handler (sig , frame ):
0 commit comments