Skip to content

Commit

Permalink
[ENH, MRG] Add volume/tumor segmentation (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrockhill authored Dec 7, 2023
1 parent 7e90930 commit b5d4917
Show file tree
Hide file tree
Showing 8 changed files with 664 additions and 36 deletions.
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- run:
name: Get Python running
command: |
pip install --upgrade PyQt6!=6.6.1 "PyQt6-Qt6!=6.6.1" sphinx-gallery pydata-sphinx-theme numpydoc scikit-learn nilearn mne-bids autoreject git+https://github.com/pyvista/pyvista@main memory_profiler sphinxcontrib.bibtex sphinxcontrib.youtube
pip install --upgrade PyQt6!=6.6.1 "PyQt6-Qt6!=6.6.1" sphinx-gallery pydata-sphinx-theme numpydoc scikit-learn nilearn mne-bids autoreject git+https://github.com/pyvista/pyvista@main memory_profiler sphinxcontrib.bibtex sphinxcontrib.youtube darkdetect qdarkstyle
pip install -ve ./mne-python .
- run:
name: Check Qt
Expand Down Expand Up @@ -70,6 +70,7 @@ jobs:
name: make html
command: |
make -C doc html
no_output_timeout: 30m
- store_test_results:
path: doc/_build/test-results
- store_artifacts:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
tmp.*

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
# not documented
"IntracranialElectrodeLocator",
"VolSourceEstimateViewer",
"VolumeSegmenter",
}
numpydoc_validate = True
numpydoc_validation_checks = {
Expand Down
58 changes: 57 additions & 1 deletion mne_gui_addons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,54 @@ def itc(data):
return gui


@_verbose
@_fill_doc
def segment_volume(
base_image=None,
subject=None,
subjects_dir=None,
show=True,
block=False,
verbose=None,
):
"""Locate intracranial electrode contacts.
Parameters
----------
base_image : path-like | nibabel.spatialimages.SpatialImage
The image on which to segment the volume. Defaults to the
freesurfer T1. Path-like inputs and nibabel image
objects are supported.
%(subject)s
%(subjects_dir)s
show : bool
Show the GUI if True.
block : bool
Whether to halt program execution until the figure is closed.
%(verbose)s
Returns
-------
gui : instance of VolumeSegmenter
The graphical user interface (GUI) window.
"""
from mne.viz.backends._utils import _init_mne_qtapp, _qt_app_exec
from ._segment import VolumeSegmenter

app = _init_mne_qtapp()

gui = VolumeSegmenter(
base_image=base_image,
subject=subject,
subjects_dir=subjects_dir,
show=show,
verbose=verbose,
)
if block:
_qt_app_exec(app)
return gui


class _GUIScraper(object):
"""Scrape GUI outputs."""

Expand All @@ -289,12 +337,20 @@ def __repr__(self):
def __call__(self, block, block_vars, gallery_conf):
from ._ieeg_locate import IntracranialElectrodeLocator
from ._vol_stc import VolSourceEstimateViewer
from ._segment import VolumeSegmenter
from sphinx_gallery.scrapers import figure_rst
from qtpy import QtGui

for gui in block_vars["example_globals"].values():
if (
isinstance(gui, (IntracranialElectrodeLocator, VolSourceEstimateViewer))
isinstance(
gui,
(
IntracranialElectrodeLocator,
VolSourceEstimateViewer,
VolumeSegmenter,
),
)
and not getattr(gui, "_scraped", False)
and gallery_conf["builder_name"] == "html"
):
Expand Down
74 changes: 70 additions & 4 deletions mne_gui_addons/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
from matplotlib.patches import Rectangle
from matplotlib.colors import LinearSegmentedColormap

from mne.viz.backends.renderer import _get_renderer
from mne.viz.utils import safe_event
Expand All @@ -46,6 +47,48 @@
_ZOOM_STEP_SIZE = 5
_ZOOM_BORDER = 1 / 5

# 20 colors generated to be evenly spaced in a cube, worked better than
# matplotlib color cycle
_UNIQUE_COLORS = [
(0.1, 0.42, 0.43),
(0.9, 0.34, 0.62),
(0.47, 0.51, 0.3),
(0.47, 0.55, 0.99),
(0.79, 0.68, 0.06),
(0.34, 0.74, 0.05),
(0.58, 0.87, 0.13),
(0.86, 0.98, 0.4),
(0.92, 0.91, 0.66),
(0.77, 0.38, 0.34),
(0.9, 0.37, 0.1),
(0.2, 0.62, 0.9),
(0.22, 0.65, 0.64),
(0.14, 0.94, 0.8),
(0.34, 0.31, 0.68),
(0.59, 0.28, 0.74),
(0.46, 0.19, 0.94),
(0.37, 0.93, 0.7),
(0.56, 0.86, 0.55),
(0.67, 0.69, 0.44),
]
_N_COLORS = len(_UNIQUE_COLORS)
_CMAP = LinearSegmentedColormap.from_list("colors", _UNIQUE_COLORS, N=_N_COLORS)


def _get_volume_info(img):
header = img.header
version = header["version"]
vol_info = dict(head=[20])
if version == 1:
version = f"{version} # volume info valid"
vol_info["valid"] = version
vol_info["filename"] = img.get_filename()
vol_info["volume"] = header["dims"][:3]
vol_info["voxelsize"] = header["delta"]
vol_info["xras"], vol_info["yras"], vol_info["zras"] = header["Mdc"]
vol_info["cras"] = header["Pxyz_c"]
return vol_info


@verbose
def _load_image(img, verbose=None):
Expand All @@ -69,7 +112,13 @@ def _load_image(img, verbose=None):
vox_mri_t = orig_mgh.header.get_vox2ras_tkr()
aff_trans = nib.orientations.inv_ornt_aff(ornt_trans, img.shape)
ras_vox_scan_ras_t = np.dot(vox_scan_ras_t, aff_trans)
return img_data, vox_mri_t, vox_scan_ras_t, ras_vox_scan_ras_t
return (
img_data,
vox_mri_t,
vox_scan_ras_t,
ras_vox_scan_ras_t,
_get_volume_info(orig_mgh),
)


def _make_mpl_plot(
Expand All @@ -96,6 +145,12 @@ def _make_mpl_plot(
return canvas, fig


def make_label(name):
label = QLabel(name)
label.setAlignment(QtCore.Qt.AlignCenter)
return label


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

Expand All @@ -106,7 +161,13 @@ class SliceBrowser(QMainWindow):
)

@_qt_safe_window(splash="_renderer.figure.splash", window="")
def __init__(self, base_image=None, subject=None, subjects_dir=None, verbose=None):
def __init__(
self,
base_image=None,
subject=None,
subjects_dir=None,
verbose=None,
):
"""GUI for browsing slices of anatomical images."""
# initialize QMainWindow class
super(SliceBrowser, self).__init__()
Expand Down Expand Up @@ -175,6 +236,7 @@ def _load_image_data(self, base_image=None):
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"))

# ready alternate base image if provided, otherwise use brain/T1
Expand All @@ -191,6 +253,7 @@ def _load_image_data(self, base_image=None):
self._vox_mri_t,
self._vox_scan_ras_t,
self._ras_vox_scan_ras_t,
self._vol_info,
) = _load_image(base_image)
if self._mr_data is not None:
if self._mr_data.shape != self._base_data.shape or not np.allclose(
Expand Down Expand Up @@ -371,22 +434,24 @@ def _plot_images(self):
render=False,
)
if self._lh is not None and self._rh is not None and self._base_mr_aligned:
self._renderer.mesh(
self._lh_actor, _ = self._renderer.mesh(
*self._lh["rr"].T,
triangles=self._lh["tris"],
color="white",
opacity=0.2,
reset_camera=False,
render=False,
)
self._renderer.mesh(
self._rh_actor, _ = self._renderer.mesh(
*self._rh["rr"].T,
triangles=self._rh["tris"],
color="white",
opacity=0.2,
reset_camera=False,
render=False,
)
else:
self._lh_actor = self._rh_actor = None
self._renderer.set_camera(
azimuth=90, elevation=90, distance=300, focalpoint=tuple(self._ras)
)
Expand Down Expand Up @@ -530,6 +595,7 @@ def _set_ras(self, ras, update_plots=True):
logger.debug(f"Setting RAS: ({msg}) mm")
if update_plots:
self._move_cursors_to_pos()
self.setFocus() # focus back to main

def set_vox(self, vox):
"""Set the crosshairs to a given voxel coordinate.
Expand Down
31 changes: 1 addition & 30 deletions mne_gui_addons/_ieeg_locate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@
QComboBox,
)

from matplotlib.colors import LinearSegmentedColormap

from ._core import SliceBrowser
from ._core import SliceBrowser, _CMAP, _N_COLORS
from mne.channels import make_dig_montage
from mne.surface import _voxel_neighbors
from mne.transforms import apply_trans, _get_trans, invert_transform
Expand All @@ -44,33 +42,6 @@
_SEARCH_ANGLE_THRESH = np.deg2rad(30)
_MISSING_PROP_OKAY = 0.25

# 20 colors generated to be evenly spaced in a cube, worked better than
# matplotlib color cycle
_UNIQUE_COLORS = [
(0.1, 0.42, 0.43),
(0.9, 0.34, 0.62),
(0.47, 0.51, 0.3),
(0.47, 0.55, 0.99),
(0.79, 0.68, 0.06),
(0.34, 0.74, 0.05),
(0.58, 0.87, 0.13),
(0.86, 0.98, 0.4),
(0.92, 0.91, 0.66),
(0.77, 0.38, 0.34),
(0.9, 0.37, 0.1),
(0.2, 0.62, 0.9),
(0.22, 0.65, 0.64),
(0.14, 0.94, 0.8),
(0.34, 0.31, 0.68),
(0.59, 0.28, 0.74),
(0.46, 0.19, 0.94),
(0.37, 0.93, 0.7),
(0.56, 0.86, 0.55),
(0.67, 0.69, 0.44),
]
_N_COLORS = len(_UNIQUE_COLORS)
_CMAP = LinearSegmentedColormap.from_list("ch_colors", _UNIQUE_COLORS, N=_N_COLORS)


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

0 comments on commit b5d4917

Please sign in to comment.