Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature bipolar references #173

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed
- PySide2 installed by default if wrapped Qt bindings (PyQt5, PySide2) are not found ([#187](https://github.com/cbrnr/mnelab/pull/187) by [Guillaume Dollé](https://github.com/gdolle))
- Add support for bipolar reference [#173](https://github.com/cbrnr/mnelab/pull/173) by [Guillaume Dollé](https://github.com/gdolle))

## [0.6.2] - 2020-10-30
### Fixed
Expand Down
13 changes: 8 additions & 5 deletions mnelab/dialogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
from .metainfodialog import MetaInfoDialog
from .montagedialog import MontageDialog
from .pickchannelsdialog import PickChannelsDialog
from .referencebipolardialog import ReferenceBipolarDialog
from .referencedialog import ReferenceDialog
from .runicadialog import RunICADialog
from .xdfstreamsdialog import XDFStreamsDialog


__all__ = [AnnotationsDialog, AppendDialog, CalcDialog,
ChannelPropertiesDialog, CropDialog, EpochDialog, ERDSDialog,
ErrorMessageBox, EventsDialog, FilterDialog, FindEventsDialog,
HistoryDialog, InterpolateBadsDialog, MetaInfoDialog, MontageDialog,
PickChannelsDialog, ReferenceDialog, RunICADialog, XDFStreamsDialog]
__all__ = [AnnotationsDialog, CalcDialog, AppendDialog, ERDSDialog,
ChannelPropertiesDialog, CropDialog,
EpochDialog, ErrorMessageBox, EventsDialog, FilterDialog,
FindEventsDialog, HistoryDialog, InterpolateBadsDialog,
MetaInfoDialog, MontageDialog, PickChannelsDialog,
ReferenceBipolarDialog, ReferenceDialog, RunICADialog,
XDFStreamsDialog]
127 changes: 127 additions & 0 deletions mnelab/dialogs/referencebipolardialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@

# Authors: Clemens Brunner <[email protected]>
#
# License: BSD (3-clause)

from qtpy.QtWidgets import (QDialog, QVBoxLayout, QListWidget,
QDialogButtonBox, QPushButton, QLabel,
QGridLayout, QGroupBox, QFileDialog)
from qtpy.QtCore import Slot
import json


class ReferenceBipolarDialog(QDialog):
def __init__(self, parent, channels, selected=None, title="Pick channels"):
super().__init__(parent)
self.setWindowTitle(title)
if(selected is None):
selected = []
self.initial_selection = selected
vbox = QVBoxLayout(self)
vbox2 = QVBoxLayout(self)
gbox = QGroupBox()
grid = QGridLayout()

grid.addWidget(QLabel("Channels (select by pair)"), 0, 0)
self.channels = QListWidget()
self.channels.insertItems(0, channels)
self.channels.setSelectionMode(QListWidget.ExtendedSelection)
grid.addWidget(self.channels, 1, 0)

grid.addWidget(QLabel("Bipolar channels: "), 0, 2)
self.selected = QListWidget()
if(len(selected)):
self.selected.insertItem(0, str(selected))
grid.addWidget(self.selected, 1, 2)

self.pushbutton_add = QPushButton("Add")
vbox2.addWidget(self.pushbutton_add)
self.pushbutton_add.pressed.connect(self.add_buttons)
self.pushbutton_add.pressed.connect(self.toggle_buttons)

self.pushbutton_rm = QPushButton("Remove")
vbox2.addWidget(self.pushbutton_rm)
self.pushbutton_rm.pressed.connect(self.rm_buttons)
self.pushbutton_rm.pressed.connect(self.toggle_buttons)

self.pushbutton_open = QPushButton("Open")
vbox2.addWidget(self.pushbutton_open)
self.pushbutton_open.pressed.connect(self.open_buttons)
self.pushbutton_open.pressed.connect(self.toggle_buttons)

self.pushbutton_save = QPushButton("Save")
vbox2.addWidget(self.pushbutton_save)
self.pushbutton_save.pressed.connect(self.save_buttons)
self.pushbutton_save.pressed.connect(self.toggle_buttons)

vbox2.addStretch(1)
gbox.setLayout(vbox2)
grid.addWidget(gbox, 1, 1)
vbox.addLayout(grid)

self.buttonbox = QDialogButtonBox(QDialogButtonBox.Ok |
QDialogButtonBox.Cancel)
vbox.addWidget(self.buttonbox)
self.buttonbox.accepted.connect(self.accept)
self.buttonbox.rejected.connect(self.reject)
self.selected.itemSelectionChanged.connect(self.toggle_buttons)
self.toggle_buttons() # initialize OK button state

@Slot()
def toggle_buttons(self):
"""slot TOGGLE buttons.
"""
if(self.selected.count()):
self.buttonbox.button(QDialogButtonBox.Ok).setEnabled(True)
self.pushbutton_save.setEnabled(True)
else:
self.buttonbox.button(QDialogButtonBox.Ok).setEnabled(False)
self.pushbutton_save.setEnabled(False)

@Slot()
def add_buttons(self):
"""slot ADD button.
"""
if(len(self.channels.selectedItems()) == 2):
pair = [item.data(0) for item in self.channels.selectedItems()]
self.selected.insertItem(0, str(pair))

@Slot()
def rm_buttons(self):
"""slot RM button.
"""
selected = self.selected.selectedItems()
if(not selected):
return
for item in selected:
self.selected.takeItem(self.selected.row(item))

@Slot()
def open_buttons(self):
"""open saved bipolar channels
"""
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.AnyFile)
if dialog.exec_():
fnames = dialog.selectedFiles()
fname = fnames.pop(0)
with open(fname) as file:
bichan = json.loads(file.read())
for i in bichan:
self.selected.insertItem(0, str(i))

@Slot()
def save_buttons(self):
"""saved bipolar channels
"""
dialog = QFileDialog()
fd = dialog.getSaveFileName()
fname = fd[0]
print("Saving bipolar reference profile: ", fname)
with open(fname, 'w') as file:
sel = [self.selected.item(i).text()
for i in range(self.selected.count())]
sell = [[i.strip("'").strip('"')
for i in j[1:-1].split(", ")] for j in sel]
file.write(json.dumps(sell))
print("Profile saved!")
3 changes: 3 additions & 0 deletions mnelab/dialogs/referencedialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ def __init__(self, parent):
vbox = QVBoxLayout(self)
grid = QGridLayout()
self.average = QRadioButton("Average")
self.bipolar = QRadioButton("Bipolar")
self.channels = QRadioButton("Channel(s):")
self.average.toggled.connect(self.toggle)
self.bipolar.toggled.connect(self.toggle)
self.channellist = QLineEdit()
self.channellist.setEnabled(False)
self.average.setChecked(True)
grid.addWidget(self.average, 0, 0)
grid.addWidget(self.channels, 1, 0)
grid.addWidget(self.channellist, 1, 1)
grid.addWidget(self.bipolar, 2, 0)
vbox.addLayout(grid)
buttonbox = QDialogButtonBox(QDialogButtonBox.Ok |
QDialogButtonBox.Cancel)
Expand Down
37 changes: 30 additions & 7 deletions mnelab/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
EpochDialog, ErrorMessageBox, EventsDialog, FilterDialog,
FindEventsDialog, HistoryDialog, InterpolateBadsDialog,
MetaInfoDialog, MontageDialog, PickChannelsDialog,
ReferenceDialog, RunICADialog, XDFStreamsDialog)
ReferenceBipolarDialog, ReferenceDialog, RunICADialog,
XDFStreamsDialog)
from .widgets.infowidget import InfoWidget
from .model import LabelsNotFoundError, InvalidAnnotationsError
from .utils import have, has_locations, image_path, interface_style
from .io import writers
from .io.xdf import get_xml, get_streams
from .viz import plot_erds


__version__ = "0.7.0.dev0"

MAX_RECENT = 6 # maximum number of recent files
Expand Down Expand Up @@ -136,15 +136,16 @@ def __init__(self, model):
self.actions["export_bads"] = file_menu.addAction(
"Export &bad channels...",
lambda: self.export_file(model.export_bads, "Export bad channels",
"*.csv"))
"*.csv", "bad_channels"))
self.actions["export_events"] = file_menu.addAction(
"Export &events...",
lambda: self.export_file(model.export_events, "Export events",
"*.csv"))
"*.csv", "events"))
self.actions["export_annotations"] = file_menu.addAction(
"Export &annotations...",
lambda: self.export_file(model.export_annotations,
"Export annotations", "*.csv"))
"Export annotations", "*.csv",
"annotations"))
self.actions["export_ica"] = file_menu.addAction(
"Export ICA...",
lambda: self.export_file(model.export_ica,
Expand Down Expand Up @@ -440,9 +441,22 @@ def open_file(self, f, text, ffilter="*"):
if fname:
f(fname)

def export_file(self, f, text, ffilter="*"):
def export_file(self, f, text, ffilter="*", suffix=''):
"""Export to file."""
fname = QFileDialog.getSaveFileName(self, text, filter=ffilter)[0]

if suffix != '':
suffix = "_"+suffix

curent_file_path = Path(self.model.current["data"].filenames[0]).parent
curent_file_name = self.model.current["name"]

Dialog = QFileDialog.getSaveFileName(self, text,
str(curent_file_path.joinpath(
curent_file_name+suffix)),
filter=ffilter)

fname = Dialog[0]

if fname:
if ffilter != "*":
exts = [ext.replace("*", "") for ext in ffilter.split()]
Expand Down Expand Up @@ -812,6 +826,15 @@ def set_reference(self):
self.auto_duplicate()
if dialog.average.isChecked():
self.model.set_reference("average")
elif dialog.bipolar.isChecked():
channels = self.model.current["data"].info["ch_names"]
selected = None
dialog = ReferenceBipolarDialog(self, channels,
selected=selected)
if dialog.exec_():
selected = [dialog.selected.item(i).text()
for i in range(dialog.selected.count())]
self.model.set_reference("bipolar", selected)
else:
ref = [c.strip() for c in dialog.channellist.text().split(",")]
self.model.set_reference(ref)
Expand Down
34 changes: 33 additions & 1 deletion mnelab/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def export_bads(self, fname):
fname = join(split(fname)[0], name + ext)
with open(fname, "w") as f:
f.write(",".join(self.current["data"].info["bads"]))
print("Bad channels exported: ", fname)

def export_events(self, fname):
"""Export events to a CSV file."""
Expand All @@ -178,6 +179,7 @@ def export_events(self, fname):
fname = join(split(fname)[0], name + ext)
np.savetxt(fname, self.current["events"][:, [0, 2]], fmt="%d",
delimiter=",", header="pos,type", comments="")
print("Events exported: ", fname)

def export_annotations(self, fname):
"""Export annotations to a CSV file."""
Expand All @@ -190,6 +192,7 @@ def export_annotations(self, fname):
for a in zip(anns.description, anns.onset, anns.duration):
f.write(",".join([a[0], str(a[1]), str(a[2])]))
f.write("\n")
print("Annotations exported: ", fname)

def export_ica(self, fname):
name, ext = splitext(split(fname)[-1])
Expand Down Expand Up @@ -478,12 +481,41 @@ def convert_beer_lambert(self):
"data = mne.preprocessing.nirs.beer_lambert_law(data)")

@data_changed
def set_reference(self, ref):
def set_reference(self, ref, bichan=None):
self.current["reference"] = ref
if ref == "average":
self.current["name"] += " (average ref)"
self.current["data"].set_eeg_reference(ref)
self.history.append('data.set_eeg_reference("average")')
if ref == "bipolar":
self.current["name"] += " (original + bipolar ref)"

anodes = [i[1:-1].split(', ').pop(0)
.rstrip("'\"").lstrip("\"'").strip() for i in bichan]
cathodes = [i[1:-1].split(', ').pop(1)
.rstrip("'\"").lstrip("\"'").strip() for i in bichan]
anodes_ch_names = []
cathodes_ch_names = []

for anode in anodes:
for ch_name in self.current["data"].info["ch_names"]:
if anode in ch_name:
anodes_ch_names.append(ch_name)
for cathode in cathodes:
for ch_name in self.current["data"].info["ch_names"]:
if cathode in ch_name:
cathodes_ch_names.append(ch_name)

self.current["data"] = mne.set_bipolar_reference(
self.current["data"],
anode=anodes_ch_names,
cathode=cathodes_ch_names,
drop_refs=False)
d = {x: x.strip() for x in
self.current["data"].info["ch_names"] if "EEG" in x}
mne.rename_channels(self.current["data"].info, d)
self.history.append(f'data.set_bipolar_reference({ref})')

else:
self.current["name"] += " (" + ",".join(ref) + ")"
if set(ref) - set(self.current["data"].info["ch_names"]):
Expand Down