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 all 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
mscheltienne 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
mscheltienne Jul 21, 2022
7abba70
fix doc style
mscheltienne 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
mscheltienne 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
12 changes: 10 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
name: Set BASH_ENV
command: |
set -e
./scripts/setup_circleci.sh
./scripts/setup_xvfb.sh
sudo apt install -qq graphviz optipng python3.8-venv python3-venv libxft2 ffmpeg
python3.8 -m venv ~/python_env
echo "set -e" >> $BASH_ENV
Expand Down Expand Up @@ -92,7 +92,11 @@ jobs:
pip install --progress-bar off .
pip install --upgrade --progress-bar off -r requirements_testing.txt
pip install --upgrade --progress-bar off -r requirements_doc.txt

pip install --upgrade --progress-bar off PyQt5
python -m pip uninstall -yq sphinx-gallery mne-qt-browser
# TODO: Revert to upstream/main once https://github.com/mne-tools/mne-qt-browser/pull/105 is merged
python -m pip install --upgrade --progress-bar off https://github.com/mne-tools/mne-qt-browser/zipball/main https://github.com/sphinx-gallery/sphinx-gallery/zipball/master

- save_cache:
key: pip-cache
paths:
Expand All @@ -104,6 +108,10 @@ jobs:
- ~/.local/lib/python3.8/site-packages
- ~/.local/bin

# - run:
# name: Check Qt
# command: LD_DEBUG=libs python -c "from PySide6.QtWidgets import QApplication, QWidget; app = QApplication([])"

# Look at what we have and fail early if there is some library conflict
- run:
name: Check installation
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade --progress-bar off pip setuptools wheel
pip install $STD_ARGS --only-binary ":all:" PyQt6 PyQt6-sip PyQt6-Qt6

# build with sdist directly
- uses: actions/checkout@v3
Expand Down Expand Up @@ -118,6 +119,10 @@ jobs:

- uses: actions/checkout@v3

- name: 'Setup xvfb'
if: "matrix.os == 'ubuntu-latest'"
run: ./scripts/setup_xvfb.sh

- name: Install mne-icalabel
run: |
pip install --upgrade --progress-bar off pip setuptools wheel
Expand Down Expand Up @@ -149,10 +154,15 @@ jobs:
shell: bash -el {0}
run: mne sys_info

- shell: bash -el {0}
run: |
QT_QPA_PLATFORM=xcb LIBGL_DEBUG=verbose LD_DEBUG=libs
name: 'Check Qt GL'

- name: Run pytest
shell: bash
run: |
python -m pytest ./mne_icalabel --cov=mne_icalabel --cov-report=xml --cov-config=setup.cfg --verbose --ignore mne-python
python -m pytest ./mne_icalabel --cov=mne_icalabel --cov-report=xml --cov-config=setup.cfg --verbose --ignore mne-python -vv mne_icalabel/gui

- name: Upload coverage stats to codecov
if: "matrix.os == 'ubuntu-latest'"
Expand Down
3 changes: 2 additions & 1 deletion doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Version 0.3 (Unreleased)
Enhancements
~~~~~~~~~~~~

-
- Adding a GUI to facilitate the labeling of ICA components, by `Adam Li`_ and `Mathieu Scheltienne`_ (:gh:`66`)

Bug
~~~
Expand All @@ -41,6 +41,7 @@ Authors

* `Mathieu Scheltienne`_
* `Anand Saini`_
* `Adam Li`_

:doc:`Find out what was new in previous releases <whats_new_previous_releases>`

Expand Down
70 changes: 70 additions & 0 deletions examples/label_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- 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

from mne_icalabel.gui import label_ica_components

# %%
# Load in some sample data

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()

# %%
# Preprocess and run ICA on the data
# ----------------------------------
# Before labeling components with the GUI, one needs to filter the data
# and then fit the ICA instance. Afterwards, one can run the GUI using the
# ``Raw`` data object and the fitted ``ICA`` instance. The GUI will modify
# the ICA instance in place, and add the labels of each component to
# the ``labels_`` attribute.

# 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 the components using a GUI
mne.set_log_level("DEBUG")
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.
print(ica.labels_)

# %%
# Save the labeled components
# ---------------------------
# After the GUI labels, save the components using the ``write_components_tsv``
# function. This will save the ICA annotations to disc in BIDS-Derivative for
# EEG data format.
#
# Note: BIDS-EEG-Derivatives is not fully specified, so this functionality
# may change in the future without notice.

# fname = '<some path to save the components>'
# write_components_tsv(ica, fname)
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.3.dev0"

from . import gui
from .label_components import label_components # noqa: F401
21 changes: 18 additions & 3 deletions mne_icalabel/annotation/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from mne.preprocessing import ICA
from mne.utils import _check_pandas_installed

from ..config import ICLABEL_LABELS_TO_MNE
from ..iclabel.config import ICLABEL_STRING_TO_NUMERICAL


Expand Down Expand Up @@ -41,17 +42,31 @@ def write_components_tsv(ica: ICA, fname):
if not isinstance(fname, BIDSPath):
fname = get_bids_path_from_fname(fname)

# initialize status, description and IC type
status = ["good"] * ica.n_components_
status_description = ["n/a"] * ica.n_components_
ic_type = ["n/a"] * ica.n_components_

# extract the component labels if they are present in the ICA instance
if ica.labels_:
for label, comps in ica.labels_.items():
this_status = "good" if label == "brain" else "bad"
if label in ICLABEL_LABELS_TO_MNE.values():
for comp in comps:
status[comp] = this_status
ic_type[comp] = label

# Create TSV.
tsv_data = pd.DataFrame(
dict(
component=list(range(ica.n_components_)),
type=["ica"] * ica.n_components_,
description=["Independent Component"] * ica.n_components_,
status=["good"] * ica.n_components_,
status_description=["n/a"] * ica.n_components_,
status=status,
status_description=status_description,
annotate_method=["n/a"] * ica.n_components_,
annotate_author=["n/a"] * ica.n_components_,
ic_type=["n/a"] * ica.n_components_,
ic_type=ic_type,
)
)
# make sure parent directories exist
Expand Down
7 changes: 6 additions & 1 deletion mne_icalabel/annotation/tests/test_bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,19 @@ def test_write_channels_tsv(_ica, _tmp_bids_path):
suffix="channels",
extension=".tsv",
)
_ica = _ica.copy()
_ica.labels_["ecg"] = [0]

write_components_tsv(_ica, deriv_fname)

assert deriv_fname.fpath.exists()
expected_json = deriv_fname.copy().update(extension=".json")
assert expected_json.fpath.exists()

ch_tsv = pd.read_csv(deriv_fname, sep="\t")
assert all(status == "good" for status in ch_tsv["status"])
assert all(status == "good" for status in ch_tsv["status"][1:])
assert ch_tsv["status"][0] == "bad"
assert ch_tsv["ic_type"].values[0] == "ecg"


def test_mark_components(_ica, _tmp_bids_path):
Expand Down
1 change: 1 addition & 0 deletions mne_icalabel/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Entry-points for mne-icalabel commands."""
36 changes: 36 additions & 0 deletions mne_icalabel/commands/mne_gui_ic_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import argparse

from qtpy.QtWidgets import QApplication

from mne_icalabel.gui._label_components import ICAComponentLabeler


def main():
"""Entry point for mne_gui_ic_annotation."""
parser = argparse.ArgumentParser(prog="mne-icalabel", description="IC annotation GUI")
parser.add_argument("--dev", help="loads a sample dataset.", action="store_true")
args = parser.parse_args()

if not args.dev:
raise NotImplementedError
else:
from mne.datasets import sample
from mne.io import read_raw
from mne.preprocessing import ICA

directory = sample.data_path() / "MEG" / "sample"
raw = read_raw(directory / "sample_audvis_raw.fif", preload=False)
raw.crop(0, 10).pick_types(eeg=True, exclude="bads")
raw.load_data()
# preprocess
raw.filter(l_freq=1.0, h_freq=100.0)
raw.set_eeg_reference("average")

n_components = 15
ica = ICA(n_components=n_components, method="picard")
ica.fit(raw)

app = QApplication([])
window = ICAComponentLabeler(inst=raw, ica=ica)
window.show()
app.exec()
11 changes: 11 additions & 0 deletions mne_icalabel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@
"iclabel": iclabel_label_components,
"manual": None,
}

# map ICLabel labels to MNE str format
ICLABEL_LABELS_TO_MNE = {
"Brain": "brain",
"Muscle": "muscle",
"Eye": "eog",
"Heart": "ecg",
"Line Noise": "line_noise",
"Channel Noise": "ch_noise",
"Other": "other",
}
1 change: 0 additions & 1 deletion mne_icalabel/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
# Author: Eric Larson <[email protected]>
#
# License: BSD-3-Clause

import warnings

import pytest
Expand Down
32 changes: 32 additions & 0 deletions mne_icalabel/gui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from mne.preprocessing import ICA


def label_ica_components(inst, ica: ICA, show: bool = True, block: bool = False):
"""Launch the IC labelling GUI.

Parameters
----------
inst : : Raw | Epochs
`~mne.io.Raw` or `~mne.Epochs` instance used to fit the `~mne.preprocessing.ICA` decomposition.
ica : ICA
The ICA object fitted on `inst`.
show : bool
Show the GUI if True.
block : bool
Whether to halt program execution until the figure is closed.

Returns
-------
gui : instance of ICAComponentLabeler
The graphical user interface (GUI) window.
"""
from mne.viz.backends._utils import _init_mne_qtapp, _qt_app_exec

from ._label_components import ICAComponentLabeler

# get application
app = _init_mne_qtapp()
gui = ICAComponentLabeler(inst=inst, ica=ica, show=show)
if block:
_qt_app_exec(app)
return gui
Loading