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_ ())
0 commit comments