├── README ├── feedback.py ├── parser.py ├── pyeeg.py ├── sdl_viewer.py └── sdl_viewer_background.png /README: -------------------------------------------------------------------------------- 1 | FORK: This is just a small change of the original at https://github.com/akloster/python-mindwave for the Mindwave Mobile. The change is using Bluetooth to communicate directly with the mindwave mobile headset instead of using a serial port like for the Mindwave. 2 | 3 | This is a set of simple scripts that interface with the Neurosky Mindwave. 4 | 5 | The Mindwave is a kind of Headset that can record EEG brain waves. 6 | 7 | These scripts have been tested under Linux(Ubuntu), so there might be some adaptation 8 | to do for other platforms. The USB device is hardcoded but is trivial to change in parser.py. 9 | 10 | You need to install pygame, pyserial, numpy and scipy. You might not have the neccessary 11 | permissions to open the serial connection. If that is the case, please drop me a note what you 12 | did to make it work. For example it might be enough to add your user account to the group 13 | "dialout". 14 | 15 | 16 | But, for what might you use an EEG? 17 | 18 | * Neurofeedback training 19 | * Better concentration 20 | * Help treat Depression and ADHD 21 | * Potentially other protocols 22 | * Help with meditation and measure progress and endurance 23 | * Use Brainwaves to control games 24 | 25 | 26 | Please message me, if you have suggestions for improvements/bugs etc. or if 27 | you are interested to develop more features/games for the Mindwave. 28 | 29 | If you want to use the Mindwave with some kind of physical computing platform (arduino, BeagleBoard, 30 | BeagleBone, Rasperry Pi), the manufacturer recommends that you solder some wires into the headset for 31 | direct communication. That means that you risk completely ruining the device and having to attach the Arduino 32 | (or similar) to the headset itself. Instead it's probably cheaper and easier to use the USB Host capabilities 33 | of an Arduino shield or the native USB Ports of the other platform for the dongle, retaining full 34 | functionality and "wirelessness". -------------------------------------------------------------------------------- /feedback.py: -------------------------------------------------------------------------------- 1 | import pygame, sys 2 | from numpy import * 3 | from pygame.locals import * 4 | import scipy 5 | from pyeeg import bin_power 6 | from time import time 7 | fpsClock= pygame.time.Clock() 8 | from random import random, choice 9 | class FeedbackTask: 10 | def __init__(self): 11 | font = pygame.font.Font("freesansbold.ttf",20) 12 | self.title_img = font.render(self.name,False, pygame.Color(255,0,0)) 13 | def process_baseline_recording(raw_values): 14 | pass 15 | def frame(self,p,surface): 16 | surface.blit(self.title_img,(300,50)) 17 | 18 | class FeedbackGraph: 19 | def __init__(self): 20 | self.values = [] 21 | self.times = [] 22 | 23 | def insert_value(self,t, value): 24 | 25 | self.values.append(value) 26 | self.times.append(time()) 27 | 28 | 29 | def draw_graph(self,surface, scale): 30 | x = 600 31 | i = len(self.values) 32 | if len(self.values)>3: 33 | while x>0: 34 | i-=1 35 | v = self.values[i] 36 | t = self.times[i] 37 | x = 500-(time()-t)*10 38 | y = 400-v*scale 39 | if i0 and value<=100: 64 | if len(self.graph.times)==0 or time()>=self.graph.times[-1]+1: 65 | self.graph.insert_value(time(), value) 66 | 67 | for i in range(6): 68 | pygame.draw.line(window, pygame.Color(0,0,200),(0,400-i*20*3),(600,400-i*20*3), 2) 69 | self.graph.draw_graph(window,3.0) 70 | 71 | 72 | class Meditation(FeedbackTask): 73 | name = "Meditation" 74 | def __init__(self): 75 | FeedbackTask.__init__(self) 76 | self.values = [] 77 | self.times = [] 78 | self.graph = FeedbackGraph() 79 | 80 | def process_baseline_recording(raw_values): 81 | pass 82 | def frame(self,p, window): 83 | FeedbackTask.frame(self, p, window) 84 | value = p.current_meditation 85 | if value>0 and value<=100: 86 | if len(self.graph.times)==0 or time()>=self.graph.times[-1]+1: 87 | self.graph.insert_value(time(), value) 88 | 89 | for i in range(6): 90 | pygame.draw.line(window, pygame.Color(0,0,200),(0,400-i*20*3),(600,400-i*20*3), 2) 91 | self.graph.draw_graph(window,3.0) 92 | 93 | 94 | 95 | class ThetaLowerTask(FeedbackTask): 96 | name = "Lower Theta" 97 | def __init__(self): 98 | FeedbackTask.__init__(self) 99 | self.spectra = [] 100 | self.graph = FeedbackGraph() 101 | def process_baseline_recording(raw_values): 102 | pass 103 | 104 | def frame(self, p,window): 105 | flen = 50 106 | spectrum, relative_spectrum = bin_power(p.raw_values[-p.buffer_len:], range(flen),512) 107 | self.spectra.append(array(relative_spectrum)) 108 | if len(self.spectra)>30: 109 | self.spectra.pop(0) 110 | spectrum = mean(array(self.spectra),axis=0) 111 | value = (1-sum(spectrum[3:8]))*100 112 | self.graph.insert_value(time(), value) 113 | for i in range(6): 114 | pygame.draw.line(window, pygame.Color(0,0,200),(0,400-i*20*3),(600,400-i*20*3), 2) 115 | self.graph.draw_graph(window,3.0) 116 | 117 | 118 | 119 | class ThetaIncreaseTask(FeedbackTask): 120 | name = "Increase Theta" 121 | def __init__(self): 122 | FeedbackTask.__init__(self) 123 | self.spectra = [] 124 | self.graph = FeedbackGraph() 125 | def process_baseline_recording(raw_values): 126 | pass 127 | def frame(self, p,window): 128 | FeedbackTask.frame(self,p,window) 129 | flen = 50 130 | spectrum, relative_spectrum = bin_power(p.raw_values[-p.buffer_len:], range(flen),512) 131 | self.spectra.append(array(relative_spectrum)) 132 | if len(self.spectra)>10: 133 | self.spectra.pop(0) 134 | spectrum = mean(array(self.spectra),axis=0) 135 | value = (sum(spectrum[3:8] / sum(spectrum[8:40])))*200 136 | self.graph.insert_value(time(), value) 137 | for i in range(6): 138 | pygame.draw.line(window, pygame.Color(0,0,200),(0,400-i*20*3),(600,400-i*20*3), 2) 139 | self.graph.draw_graph(window,3.0) 140 | 141 | 142 | tasks = [Attention, Meditation, ThetaLowerTask, ThetaIncreaseTask] 143 | 144 | task_keys ={ 145 | K_1: Attention, 146 | K_2: Meditation, 147 | K_3: ThetaLowerTask, 148 | K_4: ThetaIncreaseTask 149 | 150 | } 151 | 152 | def feedback_menu(window,p): 153 | quit = False 154 | font = pygame.font.Font("freesansbold.ttf",20) 155 | task_images = [font.render("%i: %s" % (i+1, cls.name), False, pygame.Color(255,0,0)) for i,cls in enumerate(tasks)] 156 | while not quit: 157 | window.fill(pygame.Color(0,0,0)) 158 | y= 300 159 | for img in task_images: 160 | window.blit(img, (400,y)) 161 | y+= img.get_height() 162 | p.update() 163 | pygame.display.update() 164 | for event in pygame.event.get(): 165 | if event.type==QUIT: 166 | pygame.quit() 167 | sys.exit() 168 | if event.type==KEYDOWN: 169 | if event.key== K_F5: 170 | pass 171 | elif event.key == K_ESCAPE: 172 | quit = True 173 | elif event.key in task_keys: 174 | start_session(task_keys[event.key]) 175 | 176 | def start_session(Task): 177 | quit = False 178 | font = pygame.font.Font("freesansbold.ttf",20) 179 | task = Task() 180 | while not quit: 181 | window.fill(pygame.Color(0,0,0)) 182 | p.update() 183 | for event in pygame.event.get(): 184 | if event.type==QUIT: 185 | pygame.quit() 186 | sys.exit() 187 | if event.type==KEYDOWN: 188 | if event.key== K_F5: 189 | pass 190 | elif event.key == K_ESCAPE: 191 | quit = True 192 | task.frame(p,window) 193 | pygame.display.update() 194 | fpsClock.tick(20) 195 | 196 | if __name__=="__main__": 197 | pygame.init() 198 | 199 | fpsClock= pygame.time.Clock() 200 | 201 | window = pygame.display.set_mode((1280,720)) 202 | pygame.display.set_caption("PyGame Neurofeedback Trainer") 203 | 204 | from parser import Parser 205 | p = Parser() 206 | feedback_menu(window, p) 207 | 208 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from time import time 3 | from numpy import mean 4 | import serial 5 | import bluetooth 6 | """ 7 | 8 | This is a Driver Class for the Neurosky Mindwave. The Mindwave consists of a headset and an usb dongle. 9 | The dongle communicates with the mindwave headset wirelessly and can relay the data to a program that 10 | opens its usb serial port. 11 | 12 | Some clarification on the Neurosky docs: The Neurosky Chip/Board is used in several devices, for example the Neurosky Mindset, 13 | the Neurosky Mindwave, Mattel MindFlex and several others. These chips all use the same protocol over a serial connection, but 14 | depending on the device, some kind of middleware is used. The Mindset uses bluetooth to communicate with the computer, the 15 | Mindwave has its own proprietary dongle, and the MindFlex uses a dumbed down RF protocol to communicate with the "main" 16 | board of the game. 17 | 18 | However, all of these devices speak essentially the same protocol. I also had the impression, before reading the docs, that only 19 | the Mindset provides raw values, which is obviously not the case. 20 | 21 | The Mindwave ships with a TCP/IP server to provide apps a relatively easy way to access the data. Maybe I will write a substitute 22 | in Python in the future, but for now I am satisfied with using Python only. 23 | """ 24 | class Parser: 25 | def __init__(self): 26 | self.parser = self.run() 27 | self.parser.next() 28 | self.current_vector =[] 29 | self.raw_values = [] 30 | self.current_meditation = 0 31 | self.current_attention= 0 32 | self.current_spectrum = [] 33 | self.sending_data = False 34 | self.state ="initializing" 35 | self.raw_file = None 36 | self.esense_file = None 37 | self.mindwaveMobileSocket = bluetooth.BluetoothSocket(bluetooth.RFCOMM) 38 | mindwaveMobileAddress = '9C:B7:0D:72:CD:02'; 39 | try: 40 | self.mindwaveMobileSocket.connect((mindwaveMobileAddress, 1)) 41 | except bluetooth.btcommon.BluetoothError as error: 42 | print "Could not connect: ", error, "; Retrying in 5s..." 43 | 44 | def update(self): 45 | bytes = self.mindwaveMobileSocket.recv(1000) 46 | for b in bytes: 47 | self.parser.send(ord(b)) # Send each byte to the generator 48 | def write_serial(self, string): 49 | self.mindwaveMobileSocket.send(string) 50 | 51 | def start_raw_recording(self, file_name): 52 | self.raw_file = file(file_name, "wt") 53 | self.raw_start_time = time() 54 | def start_esense_recording(self, file_name): 55 | self.esense_file = file(file_name, "wt") 56 | self.esense_start_time = time() 57 | def stop_raw_recording(self): 58 | if self.raw_file: 59 | self.raw_file.close() 60 | self.raw_file = None 61 | 62 | def stop_esense_recording(self): 63 | if self.esense_file: 64 | self.esense_file.close() 65 | self.esense_file = None 66 | def run(self): 67 | """ 68 | This generator parses one byte at a time. 69 | """ 70 | last = time() 71 | i = 1 72 | self.buffer_len = 512*3 73 | times = [] 74 | while 1: 75 | byte = yield 76 | if byte== 0xaa: 77 | byte = yield # This byte should be "\aa" too 78 | if byte== 0xaa: 79 | # packet synced by 0xaa 0xaa 80 | packet_length = yield 81 | packet_code = yield 82 | if packet_code == 0xd4: 83 | # standing by 84 | self.dongle_state= "standby" 85 | elif packet_code == 0xd0: 86 | self.dongle_state = "connected" 87 | else: 88 | self.sending_data = True 89 | left = packet_length-2 90 | while left>0: 91 | if packet_code ==0x80: # raw value 92 | row_length = yield 93 | a = yield 94 | b = yield 95 | value = struct.unpack("self.buffer_len: 98 | self.raw_values = self.raw_values[-self.buffer_len:] 99 | left-=2 100 | 101 | if self.raw_file: 102 | t = time()-self.raw_start_time 103 | self.raw_file.write("%.4f,%i\n" %(t, value)) 104 | elif packet_code == 0x02: # Poor signal 105 | a = yield 106 | self.poor_signal = a 107 | if a>0: 108 | pass 109 | left-=1 110 | elif packet_code == 0x04: # Attention (eSense) 111 | a = yield 112 | if a>0: 113 | v = struct.unpack("b",chr(a))[0] 114 | if v>0: 115 | self.current_attention = v 116 | if self.esense_file: 117 | self.esense_file.write("%.2f,,%i\n" % (time()-self.esense_start_time, v)) 118 | left-=1 119 | elif packet_code == 0x05: # Meditation (eSense) 120 | a = yield 121 | if a>0: 122 | v = struct.unpack("b",chr(a))[0] 123 | if v>0: 124 | self.current_meditation = v 125 | if self.esense_file: 126 | self.esense_file.write("%.2f,%i,\n" % (time()-self.esense_start_time, v)) 127 | 128 | left-=1 129 | elif packet_code == 0x83: 130 | vlength = yield 131 | self.current_vector = [] 132 | for row in range(8): 133 | a = yield 134 | b = yield 135 | c = yield 136 | value = a*255*255+b*255+c 137 | self.current_vector.append(value) 138 | left-=vlength 139 | packet_code = yield 140 | else: 141 | pass # sync failed 142 | else: 143 | pass # sync failed 144 | dongle_state = None 145 | DONGLE_STANDBY= "Standby" 146 | -------------------------------------------------------------------------------- /pyeeg.py: -------------------------------------------------------------------------------- 1 | """Copyleft 2010 Forrest Sheng Bao http://fsbao.net 2 | 3 | PyEEG, a Python module to extract EEG features, v 0.02_r2 4 | 5 | Project homepage: http://pyeeg.org 6 | 7 | **Data structure** 8 | 9 | PyEEG only uses standard Python and numpy data structures, 10 | so you need to import numpy before using it. 11 | For numpy, please visit http://numpy.scipy.org 12 | 13 | **Naming convention** 14 | 15 | I follow "Style Guide for Python Code" to code my program 16 | http://www.python.org/dev/peps/pep-0008/ 17 | 18 | Constants: UPPER_CASE_WITH_UNDERSCORES, e.g., SAMPLING_RATE, LENGTH_SIGNAL. 19 | 20 | Function names: lower_case_with_underscores, e.g., spectrum_entropy. 21 | 22 | Variables (global and local): CapitalizedWords or CapWords, e.g., Power. 23 | 24 | If a variable name consists of one letter, I may use lower case, e.g., x, y. 25 | 26 | Functions listed alphabetically 27 | -------------------------------------------------- 28 | 29 | """ 30 | 31 | from numpy.fft import fft 32 | from numpy import zeros, floor, log10, log, mean, array, sqrt, vstack, cumsum, \ 33 | ones, log2, std 34 | from numpy.linalg import svd, lstsq 35 | import time 36 | 37 | ######################## Functions contributed by Xin Liu ################# 38 | 39 | def hurst(X): 40 | """ Compute the Hurst exponent of X. If the output H=0.5,the behavior 41 | of the time-series is similar to random walk. If H<0.5, the time-series 42 | cover less "distance" than a random walk, vice verse. 43 | 44 | Parameters 45 | ---------- 46 | 47 | X 48 | 49 | list 50 | 51 | a time series 52 | 53 | Returns 54 | ------- 55 | H 56 | 57 | float 58 | 59 | Hurst exponent 60 | 61 | Examples 62 | -------- 63 | 64 | >>> import pyeeg 65 | >>> from numpy.random import randn 66 | >>> a = randn(4096) 67 | >>> pyeeg.hurst(a) 68 | >>> 0.5057444 69 | 70 | """ 71 | 72 | N = len(X) 73 | 74 | T = array([float(i) for i in xrange(1,N+1)]) 75 | Y = cumsum(X) 76 | Ave_T = Y/T 77 | 78 | S_T = zeros((N)) 79 | R_T = zeros((N)) 80 | for i in xrange(N): 81 | S_T[i] = std(X[:i+1]) 82 | X_T = Y - T * Ave_T[i] 83 | R_T[i] = max(X_T[:i + 1]) - min(X_T[:i + 1]) 84 | 85 | R_S = R_T / S_T 86 | R_S = log(R_S) 87 | n = log(T).reshape(N, 1) 88 | H = lstsq(n[1:], R_S[1:])[0] 89 | return H[0] 90 | 91 | 92 | ######################## Begin function definitions ####################### 93 | 94 | def embed_seq(X,Tau,D): 95 | """Build a set of embedding sequences from given time series X with lag Tau 96 | and embedding dimension DE. Let X = [x(1), x(2), ... , x(N)], then for each 97 | i such that 1 < i < N - (D - 1) * Tau, we build an embedding sequence, 98 | Y(i) = [x(i), x(i + Tau), ... , x(i + (D - 1) * Tau)]. All embedding 99 | sequence are placed in a matrix Y. 100 | 101 | Parameters 102 | ---------- 103 | 104 | X 105 | list 106 | 107 | a time series 108 | 109 | Tau 110 | integer 111 | 112 | the lag or delay when building embedding sequence 113 | 114 | D 115 | integer 116 | 117 | the embedding dimension 118 | 119 | Returns 120 | ------- 121 | 122 | Y 123 | 2-D list 124 | 125 | embedding matrix built 126 | 127 | Examples 128 | --------------- 129 | >>> import pyeeg 130 | >>> a=range(0,9) 131 | >>> pyeeg.embed_seq(a,1,4) 132 | array([[ 0., 1., 2., 3.], 133 | [ 1., 2., 3., 4.], 134 | [ 2., 3., 4., 5.], 135 | [ 3., 4., 5., 6.], 136 | [ 4., 5., 6., 7.], 137 | [ 5., 6., 7., 8.]]) 138 | >>> pyeeg.embed_seq(a,2,3) 139 | array([[ 0., 2., 4.], 140 | [ 1., 3., 5.], 141 | [ 2., 4., 6.], 142 | [ 3., 5., 7.], 143 | [ 4., 6., 8.]]) 144 | >>> pyeeg.embed_seq(a,4,1) 145 | array([[ 0.], 146 | [ 1.], 147 | [ 2.], 148 | [ 3.], 149 | [ 4.], 150 | [ 5.], 151 | [ 6.], 152 | [ 7.], 153 | [ 8.]]) 154 | 155 | 156 | 157 | """ 158 | N =len(X) 159 | 160 | if D * Tau > N: 161 | print "Cannot build such a matrix, because D * Tau > N" 162 | exit() 163 | 164 | if Tau<1: 165 | print "Tau has to be at least 1" 166 | exit() 167 | 168 | Y=zeros((N - (D - 1) * Tau, D)) 169 | for i in xrange(0, N - (D - 1) * Tau): 170 | for j in xrange(0, D): 171 | Y[i][j] = X[i + j * Tau] 172 | return Y 173 | 174 | def in_range(Template, Scroll, Distance): 175 | """Determines whether one vector is the the range of another vector. 176 | 177 | The two vectors should have equal length. 178 | 179 | Parameters 180 | ----------------- 181 | Template 182 | list 183 | The template vector, one of two vectors being compared 184 | 185 | Scroll 186 | list 187 | The scroll vector, one of the two vectors being compared 188 | 189 | D 190 | float 191 | Two vectors match if their distance is less than D 192 | 193 | Bit 194 | 195 | 196 | Notes 197 | ------- 198 | The distance between two vectors can be defined as Euclidean distance 199 | according to some publications. 200 | 201 | The two vector should of equal length 202 | 203 | """ 204 | 205 | for i in range(0, len(Template)): 206 | if abs(Template[i] - Scroll[i]) > Distance: 207 | return False 208 | return True 209 | """ Desperate code, but do not delete 210 | def bit_in_range(Index): 211 | if abs(Scroll[Index] - Template[Bit]) <= Distance : 212 | print "Bit=", Bit, "Scroll[Index]", Scroll[Index], "Template[Bit]",\ 213 | Template[Bit], "abs(Scroll[Index] - Template[Bit])",\ 214 | abs(Scroll[Index] - Template[Bit]) 215 | return Index + 1 # move 216 | 217 | Match_No_Tail = range(0, len(Scroll) - 1) # except the last one 218 | # print Match_No_Tail 219 | 220 | # first compare Template[:-2] and Scroll[:-2] 221 | 222 | for Bit in xrange(0, len(Template) - 1): # every bit of Template is in range of Scroll 223 | Match_No_Tail = filter(bit_in_range, Match_No_Tail) 224 | print Match_No_Tail 225 | 226 | # second and last, check whether Template[-1] is in range of Scroll and 227 | # Scroll[-1] in range of Template 228 | 229 | # 2.1 Check whether Template[-1] is in the range of Scroll 230 | Bit = - 1 231 | Match_All = filter(bit_in_range, Match_No_Tail) 232 | 233 | # 2.2 Check whether Scroll[-1] is in the range of Template 234 | # I just write a loop for this. 235 | for i in Match_All: 236 | if abs(Scroll[-1] - Template[i] ) <= Distance: 237 | Match_All.remove(i) 238 | 239 | 240 | return len(Match_All), len(Match_No_Tail) 241 | """ 242 | 243 | def bin_power(X,Band,Fs): 244 | """Compute power in each frequency bin specified by Band from FFT result of 245 | X. By default, X is a real signal. 246 | 247 | Note 248 | ----- 249 | A real signal can be synthesized, thus not real. 250 | 251 | Parameters 252 | ----------- 253 | 254 | Band 255 | list 256 | 257 | boundary frequencies (in Hz) of bins. They can be unequal bins, e.g. 258 | [0.5,4,7,12,30] which are delta, theta, alpha and beta respectively. 259 | You can also use range() function of Python to generate equal bins and 260 | pass the generated list to this function. 261 | 262 | Each element of Band is a physical frequency and shall not exceed the 263 | Nyquist frequency, i.e., half of sampling frequency. 264 | 265 | X 266 | list 267 | 268 | a 1-D real time series. 269 | 270 | Fs 271 | integer 272 | 273 | the sampling rate in physical frequency 274 | 275 | Returns 276 | ------- 277 | 278 | Power 279 | list 280 | 281 | spectral power in each frequency bin. 282 | 283 | Power_ratio 284 | list 285 | 286 | spectral power in each frequency bin normalized by total power in ALL 287 | frequency bins. 288 | 289 | """ 290 | 291 | C = fft(X) 292 | C = abs(C) 293 | Power =zeros(len(Band)-1); 294 | for Freq_Index in xrange(0,len(Band)-1): 295 | Freq = float(Band[Freq_Index]) ## Xin Liu 296 | Next_Freq = float(Band[Freq_Index+1]) 297 | Power[Freq_Index] = sum(C[floor(Freq/Fs*len(X)):floor(Next_Freq/Fs*len(X))]) 298 | Power_Ratio = Power/sum(Power) 299 | return Power, Power_Ratio 300 | 301 | def first_order_diff(X): 302 | """ Compute the first order difference of a time series. 303 | 304 | For a time series X = [x(1), x(2), ... , x(N)], its first order 305 | difference is: 306 | Y = [x(2) - x(1) , x(3) - x(2), ..., x(N) - x(N-1)] 307 | 308 | """ 309 | D=[] 310 | 311 | for i in xrange(1,len(X)): 312 | D.append(X[i]-X[i-1]) 313 | 314 | return D 315 | 316 | def pfd(X, D=None): 317 | """Compute Petrosian Fractal Dimension of a time series from either two 318 | cases below: 319 | 1. X, the time series of type list (default) 320 | 2. D, the first order differential sequence of X (if D is provided, 321 | recommended to speed up) 322 | 323 | In case 1, D is computed by first_order_diff(X) function of pyeeg 324 | 325 | To speed up, it is recommended to compute D before calling this function 326 | because D may also be used by other functions whereas computing it here 327 | again will slow down. 328 | """ 329 | if D is None: ## Xin Liu 330 | D = first_order_diff(X) 331 | N_delta= 0; #number of sign changes in derivative of the signal 332 | for i in xrange(1,len(D)): 333 | if D[i]*D[i-1]<0: 334 | N_delta += 1 335 | n = len(X) 336 | return log10(n)/(log10(n)+log10(n/n+0.4*N_delta)) 337 | 338 | 339 | def hfd(X, Kmax): 340 | """ Compute Hjorth Fractal Dimension of a time series X, kmax 341 | is an HFD parameter 342 | """ 343 | L = []; 344 | x = [] 345 | N = len(X) 346 | for k in xrange(1,Kmax): 347 | Lk = [] 348 | for m in xrange(0,k): 349 | Lmk = 0 350 | for i in xrange(1,int(floor((N-m)/k))): 351 | Lmk += abs(X[m+i*k] - X[m+i*k-k]) 352 | Lmk = Lmk*(N - 1)/floor((N - m) / float(k)) / k 353 | Lk.append(Lmk) 354 | L.append(log(mean(Lk))) 355 | x.append([log(float(1) / k), 1]) 356 | 357 | (p, r1, r2, s)=lstsq(x, L) 358 | return p[0] 359 | 360 | def hjorth(X, D = None): 361 | """ Compute Hjorth mobility and complexity of a time series from either two 362 | cases below: 363 | 1. X, the time series of type list (default) 364 | 2. D, a first order differential sequence of X (if D is provided, 365 | recommended to speed up) 366 | 367 | In case 1, D is computed by first_order_diff(X) function of pyeeg 368 | 369 | Notes 370 | ----- 371 | To speed up, it is recommended to compute D before calling this function 372 | because D may also be used by other functions whereas computing it here 373 | again will slow down. 374 | 375 | Parameters 376 | ---------- 377 | 378 | X 379 | list 380 | 381 | a time series 382 | 383 | D 384 | list 385 | 386 | first order differential sequence of a time series 387 | 388 | Returns 389 | ------- 390 | 391 | As indicated in return line 392 | 393 | Hjorth mobility and complexity 394 | 395 | """ 396 | 397 | if D is None: 398 | D = first_order_diff(X) 399 | 400 | D.insert(0, X[0]) # pad the first difference 401 | D = array(D) 402 | 403 | n = len(X) 404 | 405 | M2 = float(sum(D ** 2)) / n 406 | TP = sum(array(X) ** 2) 407 | M4 = 0; 408 | for i in xrange(1, len(D)): 409 | M4 += (D[i] - D[i - 1]) ** 2 410 | M4 = M4 / n 411 | 412 | return sqrt(M2 / TP), sqrt(float(M4) * TP / M2 / M2) # Hjorth Mobility and Complexity 413 | 414 | def spectral_entropy(X, Band, Fs, Power_Ratio = None): 415 | """Compute spectral entropy of a time series from either two cases below: 416 | 1. X, the time series (default) 417 | 2. Power_Ratio, a list of normalized signal power in a set of frequency 418 | bins defined in Band (if Power_Ratio is provided, recommended to speed up) 419 | 420 | In case 1, Power_Ratio is computed by bin_power() function. 421 | 422 | Notes 423 | ----- 424 | To speed up, it is recommended to compute Power_Ratio before calling this 425 | function because it may also be used by other functions whereas computing 426 | it here again will slow down. 427 | 428 | Parameters 429 | ---------- 430 | 431 | Band 432 | list 433 | 434 | boundary frequencies (in Hz) of bins. They can be unequal bins, e.g. 435 | [0.5,4,7,12,30] which are delta, theta, alpha and beta respectively. 436 | You can also use range() function of Python to generate equal bins and 437 | pass the generated list to this function. 438 | 439 | Each element of Band is a physical frequency and shall not exceed the 440 | Nyquist frequency, i.e., half of sampling frequency. 441 | 442 | X 443 | list 444 | 445 | a 1-D real time series. 446 | 447 | Fs 448 | integer 449 | 450 | the sampling rate in physical frequency 451 | 452 | Returns 453 | ------- 454 | 455 | As indicated in return line 456 | 457 | See Also 458 | -------- 459 | bin_power: pyeeg function that computes spectral power in frequency bins 460 | 461 | """ 462 | 463 | if Power_Ratio is None: 464 | Power, Power_Ratio = bin_power(X, Band, Fs) 465 | 466 | Spectral_Entropy = 0 467 | for i in xrange(0, len(Power_Ratio) - 1): 468 | Spectral_Entropy += Power_Ratio[i] * log(Power_Ratio[i]) 469 | Spectral_Entropy /= log(len(Power_Ratio)) # to save time, minus one is omitted 470 | return -1 * Spectral_Entropy 471 | 472 | def svd_entropy(X, Tau, DE, W = None): 473 | """Compute SVD Entropy from either two cases below: 474 | 1. a time series X, with lag tau and embedding dimension dE (default) 475 | 2. a list, W, of normalized singular values of a matrix (if W is provided, 476 | recommend to speed up.) 477 | 478 | If W is None, the function will do as follows to prepare singular spectrum: 479 | 480 | First, computer an embedding matrix from X, Tau and DE using pyeeg 481 | function embed_seq(): 482 | M = embed_seq(X, Tau, DE) 483 | 484 | Second, use scipy.linalg function svd to decompose the embedding matrix 485 | M and obtain a list of singular values: 486 | W = svd(M, compute_uv=0) 487 | 488 | At last, normalize W: 489 | W /= sum(W) 490 | 491 | Notes 492 | ------------- 493 | 494 | To speed up, it is recommended to compute W before calling this function 495 | because W may also be used by other functions whereas computing it here 496 | again will slow down. 497 | """ 498 | 499 | if W is None: 500 | Y = EmbedSeq(X, tau, dE) 501 | W = svd(Y, compute_uv = 0) 502 | W /= sum(W) # normalize singular values 503 | 504 | return -1*sum(W * log(W)) 505 | 506 | def fisher_info(X, Tau, DE, W = None): 507 | """ Compute Fisher information of a time series from either two cases below: 508 | 1. X, a time series, with lag Tau and embedding dimension DE (default) 509 | 2. W, a list of normalized singular values, i.e., singular spectrum (if W is 510 | provided, recommended to speed up.) 511 | 512 | If W is None, the function will do as follows to prepare singular spectrum: 513 | 514 | First, computer an embedding matrix from X, Tau and DE using pyeeg 515 | function embed_seq(): 516 | M = embed_seq(X, Tau, DE) 517 | 518 | Second, use scipy.linalg function svd to decompose the embedding matrix 519 | M and obtain a list of singular values: 520 | W = svd(M, compute_uv=0) 521 | 522 | At last, normalize W: 523 | W /= sum(W) 524 | 525 | Parameters 526 | ---------- 527 | 528 | X 529 | list 530 | 531 | a time series. X will be used to build embedding matrix and compute 532 | singular values if W or M is not provided. 533 | Tau 534 | integer 535 | 536 | the lag or delay when building a embedding sequence. Tau will be used 537 | to build embedding matrix and compute singular values if W or M is not 538 | provided. 539 | DE 540 | integer 541 | 542 | the embedding dimension to build an embedding matrix from a given 543 | series. DE will be used to build embedding matrix and compute 544 | singular values if W or M is not provided. 545 | W 546 | list or array 547 | 548 | the set of singular values, i.e., the singular spectrum 549 | 550 | Returns 551 | ------- 552 | 553 | FI 554 | integer 555 | 556 | Fisher information 557 | 558 | Notes 559 | ----- 560 | To speed up, it is recommended to compute W before calling this function 561 | because W may also be used by other functions whereas computing it here 562 | again will slow down. 563 | 564 | See Also 565 | -------- 566 | embed_seq : embed a time series into a matrix 567 | """ 568 | 569 | if W is None: 570 | M = embed_seq(X, Tau, DE) 571 | W = svd(M, compute_uv = 0) 572 | W /= sum(W) 573 | 574 | FI = 0 575 | for i in xrange(0, len(W) - 1): # from 1 to M 576 | FI += ((W[i +1] - W[i]) ** 2) / (W[i]) 577 | 578 | return FI 579 | 580 | def ap_entropy(X, M, R): 581 | """Computer approximate entropy (ApEN) of series X, specified by M and R. 582 | 583 | Suppose given time series is X = [x(1), x(2), ... , x(N)]. We first build 584 | embedding matrix Em, of dimension (N-M+1)-by-M, such that the i-th row of Em 585 | is x(i),x(i+1), ... , x(i+M-1). Hence, the embedding lag and dimension are 586 | 1 and M-1 respectively. Such a matrix can be built by calling pyeeg function 587 | as Em = embed_seq(X, 1, M). Then we build matrix Emp, whose only 588 | difference with Em is that the length of each embedding sequence is M + 1 589 | 590 | Denote the i-th and j-th row of Em as Em[i] and Em[j]. Their k-th elments 591 | are Em[i][k] and Em[j][k] respectively. The distance between Em[i] and Em[j] 592 | is defined as 1) the maximum difference of their corresponding scalar 593 | components, thus, max(Em[i]-Em[j]), or 2) Euclidean distance. We say two 1-D 594 | vectors Em[i] and Em[j] *match* in *tolerance* R, if the distance between them 595 | is no greater than R, thus, max(Em[i]-Em[j]) <= R. Mostly, the value of R is 596 | defined as 20% - 30% of standard deviation of X. 597 | 598 | Pick Em[i] as a template, for all j such that 0 < j < N - M + 1, we can 599 | check whether Em[j] matches with Em[i]. Denote the number of Em[j], 600 | which is in the range of Em[i], as k[i], which is the i-th element of the 601 | vector k. The probability that a random row in Em matches Em[i] is 602 | \simga_1^{N-M+1} k[i] / (N - M + 1), thus sum(k)/ (N - M + 1), 603 | denoted as Cm[i]. 604 | 605 | We repeat the same process on Emp and obtained Cmp[i], but here 0>> import pyeeg 805 | >>> from numpy.random import randn 806 | >>> print pyeeg.dfa(randn(4096)) 807 | 0.490035110345 808 | 809 | Reference 810 | --------- 811 | Peng C-K, Havlin S, Stanley HE, Goldberger AL. Quantification of scaling 812 | exponents and crossover phenomena in nonstationary heartbeat time series. 813 | _Chaos_ 1995;5:82-87 814 | 815 | Notes 816 | ----- 817 | 818 | This value depends on the box sizes very much. When the input is a white 819 | noise, this value should be 0.5. But, some choices on box sizes can lead to 820 | the value lower or higher than 0.5, e.g. 0.38 or 0.58. 821 | 822 | Based on many test, I set the box sizes from 1/5 of signal length to one 823 | (x-5)-th of the signal length, where x is the nearest power of 2 from the 824 | length of the signal, i.e., 1/16, 1/32, 1/64, 1/128, ... 825 | 826 | You may generate a list of box sizes and pass in such a list as a parameter. 827 | 828 | """ 829 | 830 | X = array(X) 831 | 832 | if Ave is None: 833 | Ave = mean(X) 834 | 835 | Y = cumsum(X) 836 | Y -= Ave 837 | 838 | if L is None: 839 | L = floor(len(X)*1/(2**array(range(4,int(log2(len(X)))-4)))) 840 | 841 | F = zeros(len(L)) # F(n) of different given box length n 842 | 843 | for i in xrange(0,len(L)): 844 | n = int(L[i]) # for each box length L[i] 845 | if n==0: 846 | print "time series is too short while the box length is too big" 847 | print "abort" 848 | exit() 849 | for j in xrange(0,len(X),n): # for each box 850 | if j+n < len(X): 851 | c = range(j,j+n) 852 | c = vstack([c, ones(n)]).T # coordinates of time in the box 853 | y = Y[j:j+n] # the value of data in the box 854 | F[i] += lstsq(c,y)[1] # add residue in this box 855 | F[i] /= ((len(X)/n)*n) 856 | F = sqrt(F) 857 | 858 | Alpha = lstsq(vstack([log(L), ones(len(L))]).T,log(F))[0][0] 859 | 860 | return Alpha 861 | -------------------------------------------------------------------------------- /sdl_viewer.py: -------------------------------------------------------------------------------- 1 | import pygame, sys 2 | from numpy import * 3 | from pygame.locals import * 4 | import scipy 5 | from pyeeg import bin_power 6 | pygame.init() 7 | 8 | fpsClock= pygame.time.Clock() 9 | 10 | window = pygame.display.set_mode((1280,720)) 11 | pygame.display.set_caption("Mindwave Viewer") 12 | 13 | from parser import Parser 14 | 15 | p = Parser() 16 | 17 | blackColor = pygame.Color(0,0,0) 18 | redColor = pygame.Color(255,0,0) 19 | greenColor = pygame.Color(0,255,0) 20 | deltaColor = pygame.Color(100,0,0) 21 | thetaColor = pygame.Color(0,0,255) 22 | alphaColor = pygame.Color(255,0,0) 23 | betaColor = pygame.Color(0,255,00) 24 | gammaColor = pygame.Color(0,255,255) 25 | 26 | 27 | background_img = pygame.image.load("sdl_viewer_background.png") 28 | 29 | 30 | font = pygame.font.Font("freesansbold.ttf",20) 31 | raw_eeg = True 32 | spectra = [] 33 | iteration = 0 34 | 35 | meditation_img = font.render("Meditation", False, redColor) 36 | attention_img = font.render("Attention", False, redColor) 37 | 38 | record_baseline = False 39 | 40 | while True: 41 | p.update() 42 | window.blit(background_img,(0,0)) 43 | if p.sending_data: 44 | iteration+=1 45 | 46 | flen = 50 47 | 48 | if len(p.raw_values)>=500: 49 | spectrum, relative_spectrum = bin_power(p.raw_values[-p.buffer_len:], range(flen),512) 50 | spectra.append(array(relative_spectrum)) 51 | if len(spectra)>30: 52 | spectra.pop(0) 53 | 54 | spectrum = mean(array(spectra),axis=0) 55 | for i in range (flen-1): 56 | value = float(spectrum[i]*1000) 57 | if i<3: 58 | color = deltaColor 59 | elif i<8: 60 | color = thetaColor 61 | elif i<13: 62 | color = alphaColor 63 | elif i<30: 64 | color = betaColor 65 | else: 66 | color = gammaColor 67 | pygame.draw.rect(window, color, (25+i*10,400-value, 5,value)) 68 | else: 69 | pass 70 | pygame.draw.circle(window,redColor, (800,200),p.current_attention/2) 71 | pygame.draw.circle(window,greenColor, (800,200),60/2,1) 72 | pygame.draw.circle(window,greenColor, (800,200),100/2,1) 73 | window.blit(attention_img, (760,260)) 74 | pygame.draw.circle(window,redColor, (700,200),p.current_meditation/2) 75 | pygame.draw.circle(window,greenColor, (700,200),60/2,1) 76 | pygame.draw.circle(window,greenColor, (700,200),100/2,1) 77 | 78 | window.blit(meditation_img, (600,260)) 79 | if len(p.current_vector)>7: 80 | m = max(p.current_vector) 81 | for i in range(7): 82 | value = p.current_vector[i] *100.0/m 83 | pygame.draw.rect(window, redColor, (600+i*30,450-value, 6,value)) 84 | 85 | if raw_eeg: 86 | lv = 0 87 | for i,value in enumerate(p.raw_values[-1000:]): 88 | v = value/ 255.0/ 5 89 | pygame.draw.line(window, redColor, (i+25,500-lv),(i+25, 500-v)) 90 | lv = v 91 | else: 92 | img = font.render("Mindwave Headset is not sending data... Press F5 to autoconnect or F6 to disconnect.", False, redColor) 93 | window.blit(img,(100,100)) 94 | 95 | for event in pygame.event.get(): 96 | if event.type==QUIT: 97 | pygame.quit() 98 | sys.exit() 99 | if event.type==KEYDOWN: 100 | if event.key== K_F5: 101 | p.write_serial("\xc2") 102 | elif event.key== K_F6: 103 | p.write_serial("\xc1") 104 | elif event.key==K_ESCAPE: 105 | pygame.quit() 106 | sys.exit() 107 | elif event.key == K_F7: 108 | record_baseline = True 109 | p.start_raw_recording("baseline_raw.csv") 110 | p.start_esense_recording("baseline_esense.csv") 111 | elif event.key == K_F8: 112 | record_baseline = False 113 | p.stop_esense_recording() 114 | p.stop_raw_recording() 115 | pygame.display.update() 116 | fpsClock.tick(30) -------------------------------------------------------------------------------- /sdl_viewer_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robintibor/python-mindwave/7fa0ebb078ed618bffc3b12837654be9ea26921e/sdl_viewer_background.png --------------------------------------------------------------------------------