Skip to content

Commit ae92be5

Browse files
committed
Updated code
1 parent 0d7287f commit ae92be5

File tree

2 files changed

+369
-223
lines changed

2 files changed

+369
-223
lines changed

applications/ecgg.py

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
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

Comments
 (0)