Skip to content

Commit 6963895

Browse files
authored
Merge pull request #16 from PayalLakra/bio_amptool
ffteeg and emgenvelope
2 parents 51395fd + 06fd1ad commit 6963895

File tree

4 files changed

+250
-3
lines changed

4 files changed

+250
-3
lines changed

app_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pyqtgraph==0.13.7
2+
PyQt5==5.15.11
23
PySide2==5.15.2.1
34
keyboard==0.13.5
45
scipy==1.14.1

applications/emgenvelope.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import numpy as np
2+
from scipy.signal import butter, filtfilt
3+
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QMainWindow, QWidget
4+
from pyqtgraph import PlotWidget
5+
import pyqtgraph as pg
6+
import pylsl
7+
import sys
8+
9+
class EMGMonitor(QMainWindow):
10+
def __init__(self):
11+
super().__init__()
12+
13+
self.setWindowTitle("Real-Time EMG Monitor with EMG Envelope")
14+
self.setGeometry(100, 100, 800, 600)
15+
16+
self.plot_widget = PlotWidget(self)
17+
self.plot_widget.setBackground('w')
18+
self.plot_widget.showGrid(x=True, y=True)
19+
20+
layout = QVBoxLayout()
21+
layout.addWidget(self.plot_widget)
22+
23+
central_widget = QWidget()
24+
central_widget.setLayout(layout)
25+
self.setCentralWidget(central_widget)
26+
27+
# Set up LSL stream inlet
28+
streams = pylsl.resolve_stream('name', 'BioAmpDataStream')
29+
if not streams:
30+
print("No LSL stream found!")
31+
sys.exit(0)
32+
self.inlet = pylsl.StreamInlet(streams[0])
33+
34+
# Sampling rate
35+
self.sampling_rate = int(self.inlet.info().nominal_srate())
36+
print(f"Sampling rate: {self.sampling_rate} Hz")
37+
38+
# Data and buffers
39+
self.buffer_size = self.sampling_rate * 10 # Fixed-size buffer for 10 seconds
40+
self.emg_data = np.zeros(self.buffer_size) # Fixed-size array for circular buffer
41+
self.time_data = np.linspace(0, 10, self.buffer_size) # Fixed time array for plotting
42+
self.current_index = 0 # Index for overwriting data
43+
44+
self.b, self.a = butter(4, 70.0 / (0.5 * self.sampling_rate), btype='high')
45+
46+
# Moving RMS window size (50 for 250 sampling rate and 100 for 500 sampling rate)
47+
self.rms_window_size = int(0.1 * self.sampling_rate)
48+
49+
# Set fixed axis ranges
50+
self.plot_widget.setXRange(0, 10, padding=0)
51+
52+
# Set y-axis limits based on sampling rate
53+
if self.sampling_rate == 250:
54+
self.plot_widget.setYRange(400,600 ,padding=0) # for R3 & ensuring no extra spaces at end
55+
elif self.sampling_rate == 500:
56+
self.plot_widget.setYRange(400, 10000,padding=0) # for R4 & ensuring no extra spaces at end
57+
58+
# Plot curves for EMG data and envelope
59+
self.emg_curve = self.plot_widget.plot(self.time_data, self.emg_data, pen=pg.mkPen('b', width=1))
60+
self.envelope_curve = self.plot_widget.plot(self.time_data, self.emg_data, pen=pg.mkPen('r', width=2))
61+
62+
# Timer for plot update
63+
self.timer = pg.QtCore.QTimer()
64+
self.timer.timeout.connect(self.update_plot)
65+
self.timer.start(15)
66+
67+
def calculate_moving_rms(self, signal, window_size):
68+
# Calculate RMS using a moving window
69+
rms = np.sqrt(np.convolve(signal**2, np.ones(window_size) / window_size, mode='valid'))
70+
return np.pad(rms, (len(signal) - len(rms), 0), 'constant')
71+
72+
def update_plot(self):
73+
samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=30)
74+
if samples:
75+
for sample in samples:
76+
# Overwrite the oldest data point in the buffer
77+
self.emg_data[self.current_index] = sample[0]
78+
self.current_index = (self.current_index + 1) % self.buffer_size # Circular increment
79+
80+
# Filter the EMG data
81+
filtered_emg = filtfilt(self.b, self.a, self.emg_data)
82+
# print(filtered_emg)
83+
84+
# Take absolute value before calculating RMS envelope
85+
abs_filtered_emg = np.abs(filtered_emg)
86+
# print(abs_filtered_emg)
87+
88+
# Calculate the RMS envelope
89+
rms_envelope = self.calculate_moving_rms(abs_filtered_emg, self.rms_window_size)
90+
91+
# Update curves
92+
self.emg_curve.setData(self.time_data, filtered_emg) # Plot filtered EMG in blue
93+
self.envelope_curve.setData(self.time_data, rms_envelope) # Plot EMG envelope in red
94+
95+
if __name__ == "__main__":
96+
app = QApplication(sys.argv)
97+
window = EMGMonitor()
98+
window.show()
99+
sys.exit(app.exec_())

applications/ffteeg.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import numpy as np
2+
from numpy import hamming
3+
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QHBoxLayout, QMainWindow, QWidget
4+
from PyQt5.QtCore import Qt
5+
from pyqtgraph import PlotWidget
6+
import pyqtgraph as pg
7+
import pylsl
8+
import sys
9+
from scipy.signal import butter, filtfilt
10+
from scipy.fft import fft
11+
12+
class EEGMonitor(QMainWindow):
13+
def __init__(self):
14+
super().__init__()
15+
16+
self.setWindowTitle("Real-Time EEG Monitor with FFT and Brainwave Power")
17+
self.setGeometry(100, 100, 1200, 800)
18+
19+
# Main layout split into two halves: top for EEG, bottom for FFT and Brainwaves
20+
self.central_widget = QWidget()
21+
self.main_layout = QVBoxLayout(self.central_widget)
22+
23+
# First half for EEG signal plot
24+
self.eeg_plot_widget = PlotWidget(self)
25+
self.eeg_plot_widget.setBackground('w')
26+
self.eeg_plot_widget.showGrid(x=True, y=True)
27+
self.eeg_plot_widget.setLabel('bottom', 'EEG Plot')
28+
self.eeg_plot_widget.setYRange(0, 15000, padding=0)
29+
self.eeg_plot_widget.setXRange(0, 10, padding=0)
30+
self.eeg_plot_widget.setMouseEnabled(x=False, y=False) # Disable zoom
31+
self.main_layout.addWidget(self.eeg_plot_widget)
32+
33+
# Second half for FFT and Brainwave Power, aligned horizontally
34+
self.bottom_layout = QHBoxLayout()
35+
36+
# FFT Plot (left side of the second half)
37+
self.fft_plot = PlotWidget(self)
38+
self.fft_plot.setBackground('w')
39+
self.fft_plot.showGrid(x=True, y=True)
40+
self.fft_plot.setLabel('bottom', 'FFT')
41+
self.fft_plot.setYRange(0, 25000, padding=0)
42+
self.fft_plot.setXRange(0, 50, padding=0) # Set x-axis to 0 to 50 Hz
43+
self.fft_plot.setMouseEnabled(x=False, y=False) # Disable zoom
44+
self.bottom_layout.addWidget(self.fft_plot)
45+
46+
# Bar graph for brainwave power bands (right side of the second half)
47+
self.bar_chart_widget = pg.PlotWidget(self)
48+
self.bar_chart_widget.setBackground('w')
49+
self.bar_chart_widget.setLabel('bottom', 'Brainpower Bands')
50+
self.bar_chart_widget.setXRange(-0.5, 4.5)
51+
self.bar_chart_widget.setMouseEnabled(x=False, y=False) # Disable zoom
52+
# Add brainwave power bars
53+
self.brainwave_bars = pg.BarGraphItem(x=[0, 1, 2, 3, 4], height=[0, 0, 0, 0, 0], width=0.5, brush='g')
54+
self.bar_chart_widget.addItem(self.brainwave_bars)
55+
# Set x-ticks for brainwave types
56+
self.bar_chart_widget.getAxis('bottom').setTicks([[(0, 'Delta'), (1, 'Theta'), (2, 'Alpha'), (3, 'Beta'), (4, 'Gamma')]])
57+
self.bottom_layout.addWidget(self.bar_chart_widget)
58+
59+
# Add the bottom layout to the main layout
60+
self.main_layout.addLayout(self.bottom_layout)
61+
self.setCentralWidget(self.central_widget)
62+
63+
# Set up LSL stream inlet
64+
streams = pylsl.resolve_stream('name', 'BioAmpDataStream')
65+
if not streams:
66+
print("No LSL stream found!")
67+
sys.exit(0)
68+
self.inlet = pylsl.StreamInlet(streams[0])
69+
70+
# Sampling rate
71+
self.sampling_rate = int(self.inlet.info().nominal_srate())
72+
print(f"Sampling rate: {self.sampling_rate} Hz")
73+
74+
# Data and buffers
75+
self.buffer_size = self.sampling_rate * 10 # Fixed-size buffer for 10 seconds
76+
self.eeg_data = np.zeros(self.buffer_size) # Fixed-size array for circular buffer
77+
self.time_data = np.linspace(0, 10, self.buffer_size) # Fixed time array for plotting
78+
self.current_index = 0 # Index for overwriting data
79+
80+
# Low-pass filter (4th order, cutoff at 45 Hz)
81+
self.b_lowpass, self.a_lowpass = butter(4, 45 / (0.5 * self.sampling_rate), btype='low')
82+
83+
# Timer for updating the plot
84+
self.timer = pg.QtCore.QTimer()
85+
self.timer.timeout.connect(self.update_plot)
86+
self.timer.start(20)
87+
88+
self.eeg_curve = self.eeg_plot_widget.plot(self.time_data, self.eeg_data, pen=pg.mkPen('b', width=1)) #EEG Colour is blue
89+
self.fft_curve = self.fft_plot.plot(pen=pg.mkPen('r', width=1)) # FFT Colour is red
90+
91+
def update_plot(self):
92+
samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=30)
93+
if samples:
94+
for sample in samples:
95+
# Overwrite the oldest data point in the buffer
96+
self.eeg_data[self.current_index] = sample[0]
97+
self.current_index = (self.current_index + 1) % self.buffer_size # Circular increment
98+
99+
# Apply low-pass filter
100+
filtered_eeg = filtfilt(self.b_lowpass, self.a_lowpass, self.eeg_data)
101+
102+
# Update the EEG plot with the filtered data
103+
self.eeg_curve.setData(self.time_data, filtered_eeg)
104+
105+
# Perform FFT with windowing (Hamming window)
106+
window = hamming(len(filtered_eeg)) # Apply Hamming window to reduce spectral leakage
107+
filtered_eeg_windowed = filtered_eeg * window # Element-wise multiply
108+
109+
eeg_fft = np.abs(fft(filtered_eeg_windowed))[:len(filtered_eeg_windowed) // 2] # Positive frequencies only
110+
freqs = np.fft.fftfreq(len(filtered_eeg_windowed), 1 / self.sampling_rate)[:len(filtered_eeg_windowed) // 2]
111+
112+
# Update FFT plot
113+
self.fft_curve.setData(freqs, eeg_fft)
114+
115+
# Calculate brainwave power
116+
brainwave_power = self.calculate_brainwave_power(eeg_fft, freqs)
117+
self.brainwave_bars.setOpts(height=brainwave_power)
118+
119+
def calculate_brainwave_power(self, fft_data, freqs):
120+
delta_power = np.sum(fft_data[(freqs >= 0.5) & (freqs <= 4)])
121+
theta_power = np.sum(fft_data[(freqs >= 4) & (freqs <= 8)])
122+
alpha_power = np.sum(fft_data[(freqs >= 8) & (freqs <= 13)])
123+
beta_power = np.sum(fft_data[(freqs >= 13) & (freqs <= 30)])
124+
gamma_power = np.sum(fft_data[(freqs >= 30) & (freqs <= 45)])
125+
126+
return [delta_power, theta_power, alpha_power, beta_power, gamma_power]
127+
128+
if __name__ == "__main__":
129+
app = QApplication(sys.argv)
130+
window = EEGMonitor()
131+
window.show()
132+
sys.exit(app.exec_())

applications/heartbeat_ecg.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,34 @@ def __init__(self):
5959

6060
# Set y-axis limits based on sampling rate
6161
if self.sampling_rate == 250:
62-
self.plot_widget.setYRange(0, 2**10) # for R3
62+
self.plot_widget.setYRange(0, 2**10,padding=0) # for R3 & ensuring no extra spaces at end
6363
elif self.sampling_rate == 500:
64-
self.plot_widget.setYRange(0, 2**14) # for R4
64+
self.plot_widget.setYRange(0, 2**14,padding=0) # for R4 & ensuring no extra spaces at end
6565

6666
# Set fixed x-axis range
67-
self.plot_widget.setXRange(0, 10) # 10 seconds
67+
self.plot_widget.setXRange(0, 10,padding=0) # ensure no extra spaces
6868

6969
self.ecg_curve = self.plot_widget.plot(self.time_data, self.ecg_data, pen=pg.mkPen('k', width=1))
7070
self.r_peak_curve = self.plot_widget.plot([], [], pen=None, symbol='o', symbolBrush='r', symbolSize=10) # R-peaks in red
7171

7272
self.moving_average_window_size = 5 # Initialize moving average buffer
7373
self.heart_rate_history = [] # Buffer to store heart rates for moving average
7474

75+
# Connect double-click event
76+
self.plot_widget.scene().sigMouseClicked.connect(self.on_double_click)
77+
78+
def on_double_click(self, event):
79+
if event.double():
80+
self.reset_zoom()
81+
82+
def reset_zoom(self):
83+
# Reset to default y-axis limits based on the sampling rate
84+
if self.sampling_rate == 250:
85+
self.plot_widget.setYRange(0, 2**10, padding=0)
86+
elif self.sampling_rate == 500:
87+
self.plot_widget.setYRange(0, 2**14, padding=0)
88+
self.plot_widget.setXRange(0, 10, padding=0)
89+
7590
def update_plot(self):
7691
samples, _ = self.inlet.pull_chunk(timeout=0.0, max_samples=30)
7792
if samples:

0 commit comments

Comments
 (0)