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

[ENH] Draft GUI for labeling components #66

Merged
merged 60 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
306b53e
Initial commit to show skeleton
adam2392 May 31, 2022
3ae5dcd
Some more components
adam2392 Jun 6, 2022
7fed4cd
Add draft gui
adam2392 Jun 15, 2022
1a6df47
Adding the GUI with basic Python API
adam2392 Jun 15, 2022
cd0e947
Adding updated widget
adam2392 Jun 15, 2022
77426c1
draft_2
mscheltienne Jun 15, 2022
eac7cc6
update logic
mscheltienne Jun 15, 2022
88ab828
some typos
mscheltienne Jun 15, 2022
cb05faa
Adding testing script
adam2392 Jun 27, 2022
6a10148
Adding updates to make things compile
adam2392 Jun 27, 2022
0b4a6ed
remove old version
mscheltienne Jul 6, 2022
a3af1d8
plot psd and topomap
mscheltienne Jul 6, 2022
d5bd516
change dpi
mscheltienne Jul 6, 2022
d9d912a
resize interface
mscheltienne Jul 6, 2022
65a5beb
fix style
mscheltienne Jul 6, 2022
8dfdb8e
add entry-point to run the GUI and app.exec() to start the Qt event loop
mscheltienne Jul 6, 2022
630a49d
fix style
mscheltienne Jul 6, 2022
59d23a5
add saving of the selected labels to self.saved_labels and add a rese…
mscheltienne Jul 10, 2022
cf37d9a
Adding updated api TO SAVE components to ICA isntance.
adam2392 Jul 18, 2022
fb6fb47
Adding test file for TODO
adam2392 Jul 18, 2022
100c8f2
Merge branch 'main' into gui
adam2392 Jul 19, 2022
172bb9d
Merge branch 'main' into gui
adam2392 Jul 19, 2022
c151acc
Export to BIDS should work and then try to get time-serires plot
adam2392 Jul 19, 2022
08af219
Try again
adam2392 Jul 19, 2022
a5f53a7
adad time-series widget
Jul 19, 2022
6dfdc80
fix timeSeries opening in new window
mscheltienne Jul 20, 2022
2517f3a
refactor and tests
mscheltienne Jul 20, 2022
612dd41
Fix style
adam2392 Jul 20, 2022
0a68ae9
simpler handling of the central widget and its layout
mscheltienne Jul 20, 2022
e13048b
add type hint for labels2save
mscheltienne Jul 20, 2022
037d2a8
better parent widgets
Jul 21, 2022
7abba70
fix doc style
Jul 21, 2022
9451c81
Fix error in unit test
adam2392 Jul 21, 2022
5594346
fix reset button
mscheltienne Jul 21, 2022
3512a8a
Add reqs
adam2392 Jul 23, 2022
0bd75b3
Merge branch 'main' into gui
adam2392 Jul 24, 2022
1cb507c
Fix example by closing figure
adam2392 Jul 24, 2022
d25e0f6
Adding updated example
adam2392 Jul 24, 2022
1e379c2
Apply suggestions from code review
adam2392 Jul 24, 2022
928274b
Update mne_icalabel/commands/__init__.py
adam2392 Jul 24, 2022
f47c836
Fix style
adam2392 Jul 24, 2022
d4d1044
Trya gain
adam2392 Jul 25, 2022
766aa22
Fix flake
adam2392 Jul 25, 2022
ebb939b
Fix example again
adam2392 Jul 25, 2022
46a116e
Get pyqt working
adam2392 Jul 25, 2022
23bfa96
Try GH actions again
adam2392 Jul 25, 2022
d8b22f9
Try again
adam2392 Jul 25, 2022
9405cd9
Fix this
adam2392 Jul 25, 2022
ffc3be2
Fix unit test
adam2392 Jul 25, 2022
8459966
Add version restriction
adam2392 Jul 25, 2022
0917b64
Try again
adam2392 Jul 26, 2022
f6f8794
delete old browsers
Jul 28, 2022
7a470f0
Merge
adam2392 Jul 28, 2022
487ee31
Merge branch 'gui' of https://github.com/adam2392/mne-icalabel into gui
adam2392 Jul 28, 2022
5928195
Not block
adam2392 Jul 28, 2022
a421081
Try again
adam2392 Jul 28, 2022
3b9e020
Try again
adam2392 Jul 28, 2022
e9a7c5b
Try again
adam2392 Jul 28, 2022
5c86ae0
Try again
adam2392 Jul 29, 2022
b3e2f1f
Try again
adam2392 Jul 29, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions examples/label_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
.. _tut-label-ica-components:

Labeling ICA components with a GUI
==================================

This tutorial covers how to label ICA components with a GUI.
"""

# %%

import os

import mne
from mne.preprocessing import ICA

import mne_icalabel

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(
sample_data_folder, "MEG", "sample", "sample_audvis_filt-0-40_raw.fif"
)
raw = mne.io.read_raw_fif(sample_data_raw_file)

# Here we'll crop to 60 seconds and drop gradiometer channels for speed
raw.crop(tmax=60.0).pick_types(meg="mag", eeg=True, stim=True, eog=True)
raw.load_data()

# high-pass filter the data and then perform ICA
filt_raw = raw.copy().filter(l_freq=1.0, h_freq=None)
ica = ICA(n_components=15, max_iter="auto", random_state=97)
ica.fit(filt_raw)

# now label
gui = mne_icalabel.gui.label_ica_components(raw, ica)

# The `ica` object is modified to contain the component labels
# after closing the GUI and can now be saved
# gui.close() # typically you close when done

# Now, we can take a look at the components, which can be
# saved into the BIDs directory.
1 change: 1 addition & 0 deletions mne_icalabel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

__version__ = "0.2dev0"

from . import gui
from .label_components import label_components # noqa: F401
31 changes: 31 additions & 0 deletions mne_icalabel/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from mne.utils import verbose


@verbose
def label_ica_components(inst, ica, verbose=None):
"""Label ICA components.

Parameters
----------
inst : Raw | Epochs
The raw data instance that was used for ICA.
ica : ICA
The fitted ICA instance.
%(verbose)s

Returns
-------
gui : instance of ICAComponentLabeler
The graphical user interface (GUI) window.
"""
from qtpy.QtWidgets import QApplication

from ._label_components import ICAComponentLabeler

# get application
app = QApplication.instance()
if app is None:
app = QApplication(["ICA Component Labeler"])
gui = ICAComponentLabeler(inst=inst, ica=ica, verbose=verbose)
gui.show()
mscheltienne marked this conversation as resolved.
Show resolved Hide resolved
return gui
216 changes: 216 additions & 0 deletions mne_icalabel/gui/_label_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
"""ICA GUI for labeling components."""

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


import platform

from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
from mne.preprocessing import ICA
from mne.viz.backends.renderer import _get_renderer
from qtpy import QtGui
from qtpy.QtCore import Signal, Slot
from qtpy.QtWidgets import (
QAbstractItemView,
QComboBox,
QGridLayout,
QHBoxLayout,
QLabel,
QListView,
QMainWindow,
QMessageBox,
QPlainTextEdit,
QPushButton,
QSlider,
QVBoxLayout,
QWidget,
)

# _IMG_LABELS = [['I', 'P'], ['I', 'L'], ['P', 'L']]
# _CH_PLOT_SIZE = 1024
# _ZOOM_STEP_SIZE = 5
# _RADIUS_SCALAR = 0.4
# _TUBE_SCALAR = 0.1
# _BOLT_SCALAR = 30 # mm
_CH_MENU_WIDTH = 30 if platform.system() == "Windows" else 10


def _make_topo_plot(width=4, height=4, dpi=300):
"""Make subplot for the topomap."""
fig = Figure(figsize=(width, height), dpi=dpi)
canvas = FigureCanvas(fig)
ax = fig.subplots()
fig.subplots_adjust(bottom=0, left=0, right=1, top=1, wspace=0, hspace=0)
ax.set_facecolor("k")
# clean up excess plot text, invert
ax.invert_yaxis()
adam2392 marked this conversation as resolved.
Show resolved Hide resolved
ax.set_xticks([])
ax.set_yticks([])
return canvas, fig


def _make_ts_plot(width=4, height=4, dpi=300):
"""Make subplot for the component time-series."""
fig = Figure(figsize=(width, height), dpi=dpi)
canvas = FigureCanvas(fig)
ax = fig.subplots()
fig.subplots_adjust(bottom=0, left=0, right=1, top=1, wspace=0, hspace=0)
ax.set_facecolor("k")
# clean up excess plot text, invert
ax.invert_yaxis()
adam2392 marked this conversation as resolved.
Show resolved Hide resolved
ax.set_xticks([])
ax.set_yticks([])
return canvas, fig
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time-series plot, we need to be able to scroll in time, and probably also zoom on both the X and Y-axis.
How about maybe looking into the mne-qt-browser, maybe we can just load the widgets it has?



def _make_spectrum_plot(width=4, height=4, dpi=300):
"""Make subplot for the spectrum."""
fig = Figure(figsize=(width, height), dpi=dpi)
canvas = FigureCanvas(fig)
ax = fig.subplots()
fig.subplots_adjust(bottom=0, left=0, right=1, top=1, wspace=0, hspace=0)
ax.set_facecolor("k")
# clean up excess plot text, invert
ax.invert_yaxis()
adam2392 marked this conversation as resolved.
Show resolved Hide resolved
ax.set_xticks([])
ax.set_yticks([])
return canvas, fig
mscheltienne marked this conversation as resolved.
Show resolved Hide resolved


# TODO:
# ? - plot_properties plot - topoplot, ICA time-series
# ? - update ICA components
# ? - menu with save, load
class ICAComponentLabeler(QMainWindow):
def __init__(self, inst, ica: ICA) -> None:
# initialize QMainWindow class
super().__init__()

# keep an internal pointer to the ICA and Raw
self._ica = ica
self._inst = inst

# GUI design to add widgets into a Layout
# Main plots: make one plot for each view: topographic, time-series, power-spectrum
plt_grid = QGridLayout()
plts = [_make_topo_plot(), _make_ts_plot(), _make_spectrum_plot()]
self._figs = [plts[0][1], plts[1][1], plts[2][1]]
plt_grid.addWidget(plts[0][0], 0, 0)
plt_grid.addWidget(plts[1][0], 0, 1)
plt_grid.addWidget(plts[2][0], 1, 0)

# TODO: is this the correct function to use to render? or nah... since we don't have 3D?
self._renderer = _get_renderer(name="ICA Component Labeler", size=(400, 400), bgcolor="w")
plt_grid.addWidget(self._renderer.plotter)
adam2392 marked this conversation as resolved.
Show resolved Hide resolved

# initialize channel data
self._component_index = 0

# component names are just a list of numbers from 0 to n_components
self._component_names = list(range(ica.n_components_))

# Component selector in a clickable selection list
self._component_list = QListView()
self._component_list.setSelectionMode(QAbstractItemView.SingleSelection)
max_comp_name_len = max([len(name) for name in self._component_list])
self._component_list.setMinimumWidth(max_comp_name_len * _CH_MENU_WIDTH)
self._component_list.setMaximumWidth(max_comp_name_len * _CH_MENU_WIDTH)
self._set_component_names()

# Plots
self._plot_images()

# TODO: Menus for user interface
# button_hbox = self._get_button_bar()
# slider_hbox = self._get_slider_bar()
# bottom_hbox = self._get_bottom_bar()

# Add lines
self._lines = dict()
self._lines_2D = dict()
for group in set(self._groups.values()):
self._update_lines(group)

# Put everything together
plot_component_hbox = QHBoxLayout()
plot_component_hbox.addLayout(plt_grid)
plot_component_hbox.addWidget(self._component_list)

# TODO: add the rest of the button and other widgets/menus
main_vbox = QVBoxLayout()
main_vbox.addLayout(plot_component_hbox)
# main_vbox.addLayout(button_hbox)
# main_vbox.addLayout(slider_hbox)
# main_vbox.addLayout(bottom_hbox)

central_widget = QWidget()
central_widget.setLayout(main_vbox)
self.setCentralWidget(central_widget)

# ready for user
self._component_list.setFocus() # always focus on list

def _set_component_names(self):
"""Add the component names to the selector."""
self._component_list_model = QtGui.QStandardItemModel(self._component_list)
for name in self._component_names:
self._component_list_model.appendRow(QtGui.QStandardItem(name))
# TODO: can add a method to color code the list of items
# self._color_list_item(name=name)
self._component_list.setModel(self._component_list_model)
self._component_list.clicked.connect(self._go_to_component)
self._component_list.setCurrentIndex(
self._component_list_model.index(self._component_index, 0)
)
self._component_list.keyPressEvent = self._key_press_event

def _go_to_component(self, index):
"""Change current channel to the item selected."""
self._component_index = index.row()
self._update_component_selection()

def _update_component_selection(self):
"""Update which channel is selected."""
name = self._component_names[self._component_index]
self._component_list.setCurrentIndex(
self._component_list_model.index(self._component_index, 0)
)
# self._group_selector.setCurrentIndex(self._groups[name])
# self._update_group()
# if not np.isnan(self._chs[name]).any():
# self._set_ras(self._chs[name])
# self._update_camera(render=True)
# self._draw()

def _plot_images(self):
# TODO: embed the matplotlib figure in each FigureCanvas
pass

def _save_component_labels(self):
pass

@Slot()
def _mark_component(self):
pass

@safe_event
def closeEvent(self, event):
"""Clean up upon closing the window."""
self._renderer.plotter.close()
self.close()

def _key_press_event(self, event):
pass

def _show_help(self):
"""Show the help menu."""
QMessageBox.information(
self,
"Help",
"Help:\n'g': mark component as good (brain)\n"
"up/down arrow: move up/down the list of components\n",
)
1 change: 1 addition & 0 deletions requirements_testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ python-picard
joblib
scikit-learn
pandas
qtpy