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: Add show hide menu with freesurfer atlas #27

Merged
merged 13 commits into from
Apr 30, 2024
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ jobs:
- run:
name: Get Python running
command: |
# TO DO: sphinx-gallery main -> stable on 0.17 release
pip install --upgrade PyQt6!=6.6.0 "PyQt6-Qt6!=6.6.0,!=6.7.0" git+https://github.com/sphinx-gallery/sphinx-gallery.git pydata-sphinx-theme numpydoc scikit-learn nilearn mne-bids autoreject pyvista memory_profiler sphinxcontrib.bibtex sphinxcontrib.youtube darkdetect qdarkstyle
pip install --upgrade PyQt6!=6.6.0 "PyQt6-Qt6!=6.6.0,!=6.7.0" sphinx-gallery pydata-sphinx-theme numpydoc scikit-learn nilearn mne-bids autoreject pyvista memory_profiler sphinxcontrib.bibtex sphinxcontrib.youtube darkdetect qdarkstyle
pip install -ve ./mne-python .
- run:
name: Check Qt
Expand Down
149 changes: 135 additions & 14 deletions mne_gui_addons/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import numpy as np
from functools import partial

from qtpy import QtCore
from qtpy.QtCore import Slot, Qt
from qtpy import QtCore, QtGui
from qtpy.QtCore import Slot, Signal, Qt
from qtpy.QtWidgets import (
QMainWindow,
QGridLayout,
Expand All @@ -21,6 +21,8 @@
QMessageBox,
QWidget,
QLineEdit,
QComboBox,
QPushButton,
)

from matplotlib import patheffects
Expand All @@ -29,6 +31,7 @@
from matplotlib.patches import Rectangle
from matplotlib.colors import LinearSegmentedColormap

from mne import read_freesurfer_lut
from mne.viz.backends.renderer import _get_renderer
from mne.viz.utils import safe_event
from mne.surface import _read_mri_surface, _marching_cubes
Expand Down Expand Up @@ -151,6 +154,17 @@ def make_label(name):
return label


class ComboBox(QComboBox):
"""Dropdown menu that emits a click when popped up."""

clicked = Signal()

def showPopup(self):
"""Override show popup method to emit click."""
self.clicked.emit()
super(ComboBox, self).showPopup()


class SliceBrowser(QMainWindow):
"""Navigate between slices of an MRI, CT, etc. image."""

Expand All @@ -173,6 +187,10 @@ def __init__(
super(SliceBrowser, self).__init__()
self.setAttribute(Qt.WA_DeleteOnClose, True)

atlas_ids, colors = read_freesurfer_lut()
self._fs_lut = {atlas_id: colors[name] for name, atlas_id in atlas_ids.items()}
self._atlas_ids = {val: key for key, val in atlas_ids.items()}

self._verbose = verbose
# if bad/None subject, will raise an informative error when loading MRI
subject = os.environ.get("SUBJECT") if subject is None else subject
Expand Down Expand Up @@ -203,13 +221,15 @@ def __init__(
self._configure_ui()

def _configure_ui(self):
toolbar = self._configure_toolbar()
bottom_hbox = self._configure_status_bar()

# Put everything together
plot_ch_hbox = QHBoxLayout()
plot_ch_hbox.addLayout(self._plt_grid)

main_vbox = QVBoxLayout()
main_vbox.addLayout(toolbar)
main_vbox.addLayout(plot_ch_hbox)
main_vbox.addLayout(bottom_hbox)

Expand All @@ -219,25 +239,27 @@ def _configure_ui(self):

def _load_image_data(self, base_image=None):
"""Get image data to display and transforms to/from vox/RAS."""
self._using_atlas = False
if self._subject_dir is None:
# if the recon-all is not finished or the CT is not
# downsampled to the MRI, the MRI can not be used
self._mr_data = None
self._head = None
self._lh = self._rh = None
self._mr_data = self._head = self._lh = self._rh = None
self._mr_scan_ras_ras_vox_t = None
else:
mri_img = (
"brain"
if op.isfile(op.join(self._subject_dir, "mri", "brain.mgz"))
else "T1"
mr_base_fname = op.join(self._subject_dir, "mri", "{}.mgz")
mr_fname = (
mr_base_fname.format("brain")
if op.isfile(mr_base_fname.format("brain"))
else mr_base_fname.format("T1")
)
(
self._mr_data,
mr_vox_mri_t,
mr_vox_scan_ras_t,
mr_ras_vox_scan_ras_t,
self._mr_vol_info,
) = _load_image(op.join(self._subject_dir, "mri", f"{mri_img}.mgz"))
) = _load_image(mr_fname)
self._mr_scan_ras_ras_vox_t = np.linalg.inv(mr_ras_vox_scan_ras_t)

# ready alternate base image if provided, otherwise use brain/T1
self._base_mr_aligned = True
Expand Down Expand Up @@ -417,23 +439,25 @@ def _plot_images(self):
[1],
)[0]
rr = apply_trans(self._ras_vox_scan_ras_t, rr) # base image vox -> RAS
self._renderer.mesh(
self._mc_actor, _ = self._renderer.mesh(
*rr.T,
triangles=tris,
color="gray",
opacity=0.2,
reset_camera=False,
render=False,
)
self._head_actor = None
else:
self._renderer.mesh(
self._head_actor, _ = self._renderer.mesh(
*self._head["rr"].T,
triangles=self._head["tris"],
color="gray",
opacity=0.2,
reset_camera=False,
render=False,
)
self._mc_actor = None
if self._lh is not None and self._rh is not None and self._base_mr_aligned:
self._lh_actor, _ = self._renderer.mesh(
*self._lh["rr"].T,
Expand All @@ -460,6 +484,51 @@ def _plot_images(self):
self._draw()
self._renderer._update()

def _configure_toolbar(self, hbox=None):
"""Make a bar at the top with tools on it."""
hbox = QHBoxLayout() if hbox is None else hbox

help_button = QPushButton("Help")
help_button.released.connect(self._show_help)
hbox.addWidget(help_button)

hbox.addStretch(6)

self._toggle_show_selector = ComboBox()

# add title, not selectable
self._toggle_show_selector.addItem("Show/Hide")
model = self._toggle_show_selector.model()
model.itemFromIndex(model.index(0, 0)).setSelectable(False)
# color differently
color = QtGui.QColor("gray")
brush = QtGui.QBrush(color)
brush.setStyle(QtCore.Qt.SolidPattern)
model.setData(model.index(0, 0), brush, QtCore.Qt.BackgroundRole)

if self._base_mr_aligned and hasattr(self, "_toggle_show_brain"):
self._toggle_show_selector.addItem("Show brain slices")
self._toggle_show_selector.addItem("Show atlas slices")

if hasattr(self, "_toggle_show_mip"):
self._toggle_show_selector.addItem("Show max intensity proj")

if hasattr(self, "_toggle_show_max"):
self._toggle_show_selector.addItem("Show local maxima")

if self._head_actor is not None:
self._toggle_show_selector.addItem("Hide 3D head")

if self._lh_actor is not None and self._rh_actor is not None:
self._toggle_show_selector.addItem("Hide 3D brain")

if self._mc_actor is not None:
self._toggle_show_selector.addItem("Hide 3D rendering")

self._toggle_show_selector.currentIndexChanged.connect(self._toggle_show)
hbox.addWidget(self._toggle_show_selector)
return hbox

def _configure_status_bar(self, hbox=None):
"""Make a bar at the bottom with information in it."""
hbox = QHBoxLayout() if hbox is None else hbox
Expand Down Expand Up @@ -539,6 +608,51 @@ def _update_VOX(self, event):
if ras is not None:
self._set_ras(ras)

def _toggle_show(self):
"""Show or hide objects in the 3D rendering."""
text = self._toggle_show_selector.currentText()
if text == "Show/Hide":
return
idx = self._toggle_show_selector.currentIndex()
show_hide, item = text.split(" ")[0], " ".join(text.split(" ")[1:])
show_hide_opp = "Show" if show_hide == "Hide" else "Hide"
if "slices" in item:
# atlas shown and brain already on or brain already on and atlas shown
if show_hide == "Show" and "mri" in self._images:
idx2, item2 = (2, "atlas") if self._using_atlas else (1, "brain")
self._toggle_show_selector.setItemText(idx2, f"Show {item2} slices")
self._toggle_show_brain()
mr_base_fname = op.join(self._subject_dir, "mri", "{}.mgz")
if show_hide == "Show" and "atlas" in item and not self._using_atlas:
if op.isfile(mr_base_fname.format("wmparc")):
self._mr_data = _load_image(mr_base_fname.format("wmparc"))[0]
else:
self._mr_data = _load_image(mr_base_fname.format("aparc+aseg"))[0]
self._using_atlas = True
if show_hide == "Show" and "brain" in item and self._using_atlas:
if op.isfile(mr_base_fname.format("brain")):
self._mr_data = _load_image(mr_base_fname.format("brain"))[0]
else:
self._mr_data = _load_image(mr_base_fname.format("T1"))[0]
self._using_atlas = False
self._toggle_show_brain()
self._update_moved()
elif item == "max intensity proj":
self._toggle_show_mip()
elif item == "local maxima":
self._toggle_show_max()
else:
actors = {
"3D head": [self._head_actor],
"3D brain": [self._lh_actor, self._rh_actor],
"3D rendering": [self._mc_actor],
}[item]
for actor in actors:
actor.SetVisibility(show_hide == "Show")
self._renderer._update()
self._toggle_show_selector.setItemText(idx, f"{show_hide_opp} {item}")
self._toggle_show_selector.setCurrentIndex(0) # back to title

def _convert_text(self, text, text_kind):
text = text.replace("\n", "")
vals = text.split(",")
Expand Down Expand Up @@ -720,9 +834,16 @@ def _update_moved(self):
self._VOX_textbox.setText(
"{:3d}, {:3d}, {:3d}".format(*self._vox.round().astype(int))
)
self._intensity_label.setText(
"intensity = {:.2f}".format(self._base_data[tuple(self._current_slice)])
intensity_text = "intensity = {:.2f}".format(
self._base_data[tuple(self._current_slice)]
)
if self._using_atlas:
vox = (
apply_trans(self._mr_scan_ras_ras_vox_t, self._ras).round().astype(int)
)
label = self._atlas_ids[int(self._mr_data[tuple(vox)])]
intensity_text += f" ({label})"
self._intensity_label.setText(intensity_text)

@safe_event
def closeEvent(self, event):
Expand Down
Loading
Loading