diff --git a/NeuroShortcut.py b/NeuroShortcut.py index b8c7095..57f79c1 100644 --- a/NeuroShortcut.py +++ b/NeuroShortcut.py @@ -9,7 +9,7 @@ from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds from brainflow.data_filter import DataFilter, WindowOperations, DetrendOperations -shape_colors = ["ffb480"]*8 +shape_colors = ["ffb480"] * 8 svg_str = """ @@ -17,16 +17,24 @@ - - - - - - - - - - + + + + + + + + + + @@ -35,7 +43,7 @@ - + @@ -92,44 +100,56 @@ stream_running = False stop_stream = False current_device = 0 -freq_bands = {'delta': [0,4],'theta': [4,7],'alpha': [8,12],'beta': [12,30],'gamma': [30,100]} +freq_bands = {'delta': [0, 4], 'theta': [4, 7], 'alpha': [8, 12], 'beta': [12, 30], 'gamma': [30, 100]} current_freq_band = 2 current_electrodes = [] -electrode_locations = [1,3,5,7,0,2,4,6] +electrode_locations = [1, 3, 5, 7, 0, 2, 4, 6] electrode_names = ['Fp2', 'Fp1', 'F4', 'F3', 'P4', 'P3', 'O2', 'O1'] datapoints = 30 -X = list(range(0,datapoints)) -y = {0: [0]*datapoints, 1: [0]*datapoints, 2: [0]*datapoints, 3: [0]*datapoints, 4: [0]*datapoints, 5: [0]*datapoints, 6: [0]*datapoints, 7: [0]*datapoints} +X = list(range(0, datapoints)) +y = {0: [0] * datapoints, 1: [0] * datapoints, 2: [0] * datapoints, 3: [0] * datapoints, 4: [0] * datapoints, + 5: [0] * datapoints, 6: [0] * datapoints, 7: [0] * datapoints} color_count = {'delta': 100, 'theta': 30, 'alpha': 30, 'beta': 30, 'gamma': 100} freq_band_colors = [np.array( - [np.rint(np.linspace(255, 255, color_count['delta'])), np.rint(np.linspace(136, 161, color_count['delta'])), np.rint(np.linspace(136, 0, color_count['delta']))]).T, - np.array( - [np.rint(np.linspace(255, 177, color_count['theta'])), np.rint(np.linspace(246, 255, color_count['theta'])), np.rint(np.linspace(136, 0, color_count['theta']))]).T, - np.array( - [np.rint(np.linspace(137, 0, color_count['alpha'])), np.rint(np.linspace(255, 223, color_count['alpha'])), np.rint(np.linspace(173, 255, color_count['alpha']))]).T, - np.array( - [np.rint(np.linspace(135, 57, color_count['beta'])), np.rint(np.linspace(208, 0, color_count['beta'])), np.rint(np.linspace(255, 255, color_count['beta']))]).T, - np.array( - [np.rint(np.linspace(232, 255, color_count['gamma'])), np.rint(np.linspace(144, 0, color_count['gamma'])), np.rint(np.linspace(255, 138, color_count['gamma']))]).T] + [np.rint(np.linspace(255, 255, color_count['delta'])), np.rint(np.linspace(136, 161, color_count['delta'])), + np.rint(np.linspace(136, 0, color_count['delta']))]).T, + np.array( + [np.rint(np.linspace(255, 177, color_count['theta'])), + np.rint(np.linspace(246, 255, color_count['theta'])), + np.rint(np.linspace(136, 0, color_count['theta']))]).T, + np.array( + [np.rint(np.linspace(137, 0, color_count['alpha'])), + np.rint(np.linspace(255, 223, color_count['alpha'])), + np.rint(np.linspace(173, 255, color_count['alpha']))]).T, + np.array( + [np.rint(np.linspace(135, 57, color_count['beta'])), + np.rint(np.linspace(208, 0, color_count['beta'])), + np.rint(np.linspace(255, 255, color_count['beta']))]).T, + np.array( + [np.rint(np.linspace(232, 255, color_count['gamma'])), + np.rint(np.linspace(144, 0, color_count['gamma'])), + np.rint(np.linspace(255, 138, color_count['gamma']))]).T] marker_offset = 0 threshold_value = 0 smoothing = 1 current_trigger = 0 trigger_widgets = [] trigger_layouts = [] -band_power_log = {'delta': [[0]*datapoints]*9, - 'theta': [[0]*datapoints]*9, - 'alpha': [[0]*datapoints]*9, - 'beta': [[0]*datapoints]*9, - 'gamma': [[0]*datapoints]*9} +band_power_log = {'delta': [[0] * datapoints] * 9, + 'theta': [[0] * datapoints] * 9, + 'alpha': [[0] * datapoints] * 9, + 'beta': [[0] * datapoints] * 9, + 'gamma': [[0] * datapoints] * 9} log_updated = False all_triggers = pd.read_csv("triggers.csv") + def clearLayout(layout): - while layout.count(): - child = layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() + while layout.count(): + child = layout.takeAt(0) + if child.widget(): + child.widget().deleteLater() + class signalListener(QtCore.QThread): band_power = QtCore.pyqtSignal(list) @@ -197,14 +217,9 @@ def run(self): freq_band = freq_bands[name] power = round(DataFilter.get_band_power(psd, freq_band[0], freq_band[1]), 2) - # smoothing - if channels_copy != {}: - for index_modifier in range(0, smoothing-1): - power = round((power + channels_copy[eeg_channel][current_index-index_modifier]) / 2, 2) - if name == 'delta' or name == 'gamma': - power = power/100 - power = (color_count[name]-1 if power >= color_count[name] else power) + power = power / 100 + power = (color_count[name] - 1 if power >= color_count[name] else power) channels[eeg_channel].append(power) current_index += 1 @@ -220,7 +235,6 @@ def run(self): band_power_log['gamma'][key] = band_power_log['gamma'][key][1:] + [channels[key][4]] log_updated = True - channel_bands.append(channels[eeg_channel][current_freq_band]) self.band_power.emit(channel_bands) @@ -238,6 +252,7 @@ def run(self): # except Exception as e: # print("ERR:", e) + class triggerListener(QtCore.QThread): def run(self): global band_power_log @@ -258,33 +273,41 @@ def run(self): electrodes = all_triggers["electrodes"][index].split(", ") for electrode in electrodes: if len(signal) == 0: - signal = band_power_log[frequency_band][electrode_names.index(electrode)+1] + signal = band_power_log[frequency_band][electrode_names.index(electrode) + 1] else: - signal = np.mean(np.vstack([signal, band_power_log[frequency_band][electrode_names.index(electrode)+1]]), axis=0).tolist() + signal = np.mean( + np.vstack([signal, band_power_log[frequency_band][electrode_names.index(electrode) + 1]]), + axis=0).tolist() # apply smoothing for a in range(all_triggers["smoothing"][index], len(signal)): - signal[a-1] = np.mean(signal[a - all_triggers["smoothing"][index]:a]) + signal[a - 1] = np.mean(signal[a - all_triggers["smoothing"][index]:a]) # check for a threshold pass - val = signal[len(signal)-1:] + val = signal[len(signal) - 1:] if len(val) > 0 and \ - ((all_triggers["threshold_direction"][index] == "below" and val[0] < all_triggers["threshold"][index]) or - (all_triggers["threshold_direction"][index] == "above" and val[0] > all_triggers["threshold"][index])): + ((all_triggers["threshold_direction"][index] == "below" and val[0] < all_triggers["threshold"][ + index]) or + (all_triggers["threshold_direction"][index] == "above" and val[0] > all_triggers["threshold"][ + index])): # check that activation delay and cool down are satisfied activation_delay_passed = True if int(float(all_triggers["activation_delay"][index])) > 0: for sample in signal[-int(float(all_triggers["activation_delay"][index])):]: - if ((all_triggers["threshold_direction"][index] and sample >= all_triggers["threshold"][index]) or - (not all_triggers["threshold_direction"][index] and sample <= all_triggers["threshold"][index])): + if ((all_triggers["threshold_direction"][index] and sample >= all_triggers["threshold"][ + index]) or + (not all_triggers["threshold_direction"][index] and sample <= + all_triggers["threshold"][index])): activation_delay_passed = False cool_down_passed = False if int(float(all_triggers["cool_down"][index])) > 0: for sample in signal[-int(float(all_triggers["cool_down"][index])):]: - if ((all_triggers["threshold_direction"][index] and sample < all_triggers["threshold"][index]) or - (not all_triggers["threshold_direction"][index] and sample > all_triggers["threshold"][index])): + if ((all_triggers["threshold_direction"][index] and sample < all_triggers["threshold"][ + index]) or + (not all_triggers["threshold_direction"][index] and sample > + all_triggers["threshold"][index])): cool_down_passed = True else: cool_down_passed = True @@ -297,7 +320,10 @@ def run(self): log_updated = False + uiclass, baseclass = pg.Qt.loadUiType("interface.ui") + + class MainWindow(uiclass, baseclass): def __init__(self): super().__init__() @@ -320,7 +346,7 @@ def __init__(self): svg_bytes = bytearray(svg_str, encoding='utf-8') self.svgWidget = QtSvg.QSvgWidget(self) self.svgWidget.renderer().load(svg_bytes) - self.svgWidget.move(self.svgWidget.x()+10, self.svgWidget.y()+50) + self.svgWidget.move(self.svgWidget.x() + 10, self.svgWidget.y() + 50) self.svgWidget.setHidden(True) # Trigger list layout init @@ -364,12 +390,12 @@ def tileClick(self, outline_id): global current_electrodes global electrode_names - svg_str_list = svg_str.split('id="outline'+str(outline_id)+'" stroke="') + svg_str_list = svg_str.split('id="outline' + str(outline_id) + '" stroke="') if svg_str_list[1].split('"')[0] == "none": - svg_str_list[0] = svg_str_list[0] + 'id="outline'+str(outline_id)+'" stroke="#FFFFFF" fill="#888888"' + svg_str_list[0] = svg_str_list[0] + 'id="outline' + str(outline_id) + '" stroke="#FFFFFF" fill="#888888"' current_electrodes.remove(electrode_names[electrode_locations[outline_id]]) else: - svg_str_list[0] = svg_str_list[0] + 'id="outline'+str(outline_id)+'" stroke="none" fill="none"' + svg_str_list[0] = svg_str_list[0] + 'id="outline' + str(outline_id) + '" stroke="none" fill="none"' current_electrodes.append(electrode_names[electrode_locations[outline_id]]) svg_str_list[1] = "\"".join(svg_str_list[1].split('"')[3:]) @@ -422,15 +448,20 @@ def bandSelectChanged(self, band_id): self.thresholdSlider.setValue(0) if current_freq_band == 0: # Delta - self.descriptionLabel.setText('

Delta waves are usually associated with the deep stage 3 of NREM sleep, also known as slow-wave sleep (SWS), and aid in characterizing the depth of sleep. (Learn More)

') + self.descriptionLabel.setText( + '

Delta waves are usually associated with the deep stage 3 of NREM sleep, also known as slow-wave sleep (SWS), and aid in characterizing the depth of sleep. (Learn More)

') elif current_freq_band == 1: # Theta - self.descriptionLabel.setText('

Humans exhibit predominantly cortical theta wave activity during REM sleep. Increased sleepiness is associated with decreased alpha wave power and increased theta wave power. Meditation has been shown to increase theta power. (Learn More)

') + self.descriptionLabel.setText( + '

Humans exhibit predominantly cortical theta wave activity during REM sleep. Increased sleepiness is associated with decreased alpha wave power and increased theta wave power. Meditation has been shown to increase theta power. (Learn More)

') elif current_freq_band == 2: # Alpha - self.descriptionLabel.setText('

Alpha waves are reduced with open eyes and sleep, while they are enhanced during drowsiness. Occipital alpha waves during periods of eyes closed are the strongest EEG brain signals. (Learn More)

') + self.descriptionLabel.setText( + '

Alpha waves are reduced with open eyes and sleep, while they are enhanced during drowsiness. Occipital alpha waves during periods of eyes closed are the strongest EEG brain signals. (Learn More)

') elif current_freq_band == 3: # Beta - self.descriptionLabel.setText('

Low-amplitude beta waves with multiple and varying frequencies are often associated with active, busy or anxious thinking and active concentration. Over the motor cortex, beta waves are associated with the muscle contractions that happen in isotonic movements and are suppressed prior to and during movement changes, with similar observations across fine and gross motor skills. (Learn More)

') + self.descriptionLabel.setText( + '

Low-amplitude beta waves with multiple and varying frequencies are often associated with active, busy or anxious thinking and active concentration. Over the motor cortex, beta waves are associated with the muscle contractions that happen in isotonic movements and are suppressed prior to and during movement changes, with similar observations across fine and gross motor skills. (Learn More)

') elif current_freq_band == 4: # Gamma - self.descriptionLabel.setText('

High-amplitude gamma wave synchrony can be self-induced via meditation. Long-term practitioners of meditation such as Tibetan Buddhist monks exhibit both increased gamma-band activity at baseline as well as significant increases in gamma synchrony during meditation, as determined by scalp EEG. (Learn More)

') + self.descriptionLabel.setText( + '

High-amplitude gamma wave synchrony can be self-induced via meditation. Long-term practitioners of meditation such as Tibetan Buddhist monks exhibit both increased gamma-band activity at baseline as well as significant increases in gamma synchrony during meditation, as determined by scalp EEG. (Learn More)

') def deviceSelected(self, device_id): global current_device @@ -469,7 +500,7 @@ def updateTiles(self, band_power): try: self.powerTimeSeries.clear() y_mean = [] - for a in range(0,8): + for a in range(0, 8): # Band power plotting if electrode_names[electrode_locations[a]] in current_electrodes: y[a] = y[a][1:] + [band_power[electrode_locations[a]]] @@ -477,29 +508,39 @@ def updateTiles(self, band_power): y_mean = y[a] else: y_mean = np.mean(np.vstack([y_mean, y[a]]), axis=0).tolist() + self.powerTimeSeries.plot(X, y[a], pen={'color': '#BBBBBB', 'width': 0.5}) # Vector tile colors - color = "rgb(" + ",".join([str(int(i)) for i in freq_band_colors[current_freq_band][int(band_power[electrode_locations[a]])]]) + ")" + color = "rgb(" + ",".join([str(int(i)) for i in freq_band_colors[current_freq_band][ + int(band_power[electrode_locations[a]])]]) + ")" svg_str_split = svg_str.split('id="shape' + str(electrode_locations[a]) + '" fill="') svg_start = svg_str_split[0] + 'id="shape' + str(electrode_locations[a]) + '" fill="' + color + '"' svg_end = '"'.join(svg_str_split[1].split('"')[1:]) svg_str = svg_start + svg_end + # apply smoothing + if len(y_mean) > 0: + y_mean[len(y_mean) - 1] = np.mean(y_mean[len(y_mean) - smoothing:len(y_mean)]) + # Threshold triggered indicator - val = y_mean[len(y_mean)-1:] + val = y_mean[len(y_mean) - 1:] if len(val) > 0: val = val[0] if self.invertTriggerArea.isChecked(): if val < threshold_value: - self.indicator.setStyleSheet("border: 2px solid black; border-radius: 10px; background-color: red;") + self.indicator.setStyleSheet( + "border: 2px solid black; border-radius: 10px; background-color: red;") else: - self.indicator.setStyleSheet("border: 2px solid black; border-radius: 10px; background-color: #222222;") + self.indicator.setStyleSheet( + "border: 2px solid black; border-radius: 10px; background-color: #222222;") else: if val > threshold_value: - self.indicator.setStyleSheet("border: 2px solid black; border-radius: 10px; background-color: red;") + self.indicator.setStyleSheet( + "border: 2px solid black; border-radius: 10px; background-color: red;") else: - self.indicator.setStyleSheet("border: 2px solid black; border-radius: 10px; background-color: #222222;") + self.indicator.setStyleSheet( + "border: 2px solid black; border-radius: 10px; background-color: #222222;") # Plot layout self.powerTimeSeries.plot(X, y_mean, pen={'color': '#000000', 'width': 1.5}) @@ -508,7 +549,7 @@ def updateTiles(self, band_power): svg_bytes = bytearray(svg_str, encoding='utf-8') self.svgWidget.renderer().load(svg_bytes) except Exception as e: - print("ERR: ",end="") + print("ERR: ", end="") input(e) else: @@ -523,7 +564,7 @@ def updateThreshold(self): marker_position = self.powerTimeSeries.height() - self.thresholdSlider.value() threshold_value = ((((marker_position - 0) * (30 - 0)) / (self.thresholdSlider.maximum() - 0)) - 30) * -1 self.thresholdMarker.move(self.thresholdMarker.x(), marker_position - marker_offset) - self.thresholdValueLabel.setText("Threshold: "+str(round(threshold_value, 2))) + self.thresholdValueLabel.setText("Threshold: " + str(round(threshold_value, 2))) def invertTriggerAreaClicked(self): global marker_offset @@ -537,7 +578,7 @@ def invertTriggerAreaClicked(self): def smoothingChanged(self, smoothing_level): global smoothing - smoothing = smoothing_level+1 + smoothing = smoothing_level + 1 def saveTriggerButtonClicked(self): global freq_bands @@ -568,7 +609,9 @@ def saveTriggerButtonClicked(self): system_command = "" active = True - pd.DataFrame([[last_edit, name, frequency_band, electrodes, smoothing, threshold, threshold_direction, activation_delay, cool_down, system_command, active]]).to_csv("triggers.csv",mode='a',header=False,index=False) + pd.DataFrame([[last_edit, name, frequency_band, electrodes, smoothing, threshold, threshold_direction, + activation_delay, cool_down, system_command, active]]).to_csv("triggers.csv", mode='a', + header=False, index=False) self.triggerName.setText("") all_triggers = pd.read_csv("triggers.csv") print("Saved") @@ -590,19 +633,20 @@ def loadTriggers(self): self.triggerListLayout.addWidget(trigger_widgets[trigger_id]) trigger_widgets[trigger_id].setObjectName("trigger") trigger_widgets[trigger_id].setFixedHeight(101) - trigger_widgets[trigger_id].setStyleSheet("QWidget#trigger { border: 2px solid #BBBBBB; } QLabel { font-size: 10pt; }") + trigger_widgets[trigger_id].setStyleSheet( + "QWidget#trigger { border: 2px solid #BBBBBB; } QLabel { font-size: 10pt; }") trigger_widgets[trigger_id].mousePressEvent = self.triggerSelect trigger_layouts.append(QtWidgets.QGridLayout()) trigger_widgets[trigger_id].setLayout(trigger_layouts[trigger_id]) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Last Edit
"+trigger[0]), 1, 1) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Name
"+trigger[1]), 1, 2) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Freq. Band
"+trigger[2]), 1, 3) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Electrodes
"+trigger[3]), 1, 4) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Smoothing
"+str(trigger[4])), 2, 1) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Threshold
"+str(trigger[5])), 2, 2) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Threshold Direction
"+trigger[6]), 2, 3) - trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Active
"+str(trigger[10])), 2, 4) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Last Edit
" + trigger[0]), 1, 1) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Name
" + trigger[1]), 1, 2) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Freq. Band
" + trigger[2]), 1, 3) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Electrodes
" + trigger[3]), 1, 4) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Smoothing
" + str(trigger[4])), 2, 1) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Threshold
" + str(trigger[5])), 2, 2) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Threshold Direction
" + trigger[6]), 2, 3) + trigger_layouts[trigger_id].addWidget(QtWidgets.QLabel("Active
" + str(trigger[10])), 2, 4) trigger_id += 1 @@ -620,14 +664,17 @@ def triggerSelect(self, event): scroll_height = self.triggerListArea.verticalScrollBar().value() for tridder_id in range(1, 10000): - if event.windowPos().y() > (tridder_id-1)*106+50-scroll_height and event.windowPos().y() < tridder_id*106+50-scroll_height: + if event.windowPos().y() > ( + tridder_id - 1) * 106 + 50 - scroll_height and event.windowPos().y() < tridder_id * 106 + 50 - scroll_height: # Set current trigger id - current_trigger = tridder_id-1 + current_trigger = tridder_id - 1 break # Update trigger widget, number selection boxes, and line edit for trigger_widget in trigger_widgets: - trigger_widget.setStyleSheet("QWidget#trigger { border: 2px solid #BBBBBB; background-color: none; } QLabel { font-size: 10pt; }") - trigger_widgets[current_trigger].setStyleSheet("QWidget#trigger { border: 2px solid #BBBBBB; background-color: #CCCCCC; } QLabel { font-size: 10pt; }") + trigger_widget.setStyleSheet( + "QWidget#trigger { border: 2px solid #BBBBBB; background-color: none; } QLabel { font-size: 10pt; }") + trigger_widgets[current_trigger].setStyleSheet( + "QWidget#trigger { border: 2px solid #BBBBBB; background-color: #CCCCCC; } QLabel { font-size: 10pt; }") triggers = pd.read_csv("triggers.csv").to_numpy() self.activeCheckBox.setChecked(int(triggers[current_trigger][10])) @@ -706,6 +753,7 @@ def deleteTriggerButtonClicked(self): print("Deleted") + app = QtWidgets.QApplication(sys.argv) window = MainWindow() window.show() diff --git a/interface.ui b/interface.ui index b8e405d..0380205 100644 --- a/interface.ui +++ b/interface.ui @@ -117,7 +117,7 @@ - background-image: url(cytonpins.png); + background-image: url(assets/cytonpins.png); diff --git a/triggers.csv b/triggers.csv index 7b90f53..427234b 100644 --- a/triggers.csv +++ b/triggers.csv @@ -1,5 +1 @@ last_edit,name,frequency_band,electrodes,smoothing,threshold,threshold_direction,activation_delay,cool_down,system_command,active -12/11/2023 20:51:52,Eyes Closed,alpha,"O2, O1",1,13.7,above,0.0,0.0,dir,1 -12/11/2023 20:52:17,test 1,beta,Fp1,1,7.72,below,0.0,0.0,,True -12/11/2023 20:52:21,test 2,beta,"Fp1, Fp2",1,7.72,below,0.0,0.0,,True -12/11/2023 20:52:39,test 3,delta,"Fp1, Fp2, F4, F3",1,-0.0,below,0.0,0.0,,True