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, MRG] Add volume/tumor segmentation #8

Merged
merged 50 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
1205494
minimal working version for tumor segmentation
alexrockhill Apr 13, 2023
541cb72
style
alexrockhill Apr 13, 2023
a7a1be0
add smoothing, a few more touchups, worked well for a hard patient
alexrockhill Apr 13, 2023
f36f2d9
add export button
alexrockhill Apr 21, 2023
8eb4e7e
Merge branch 'main' into seg
alexrockhill Apr 21, 2023
fe6f14a
add brain alpha slider
alexrockhill Apr 21, 2023
0e736b5
fix style
alexrockhill Apr 21, 2023
69c022f
add max n voxels
alexrockhill Apr 24, 2023
95c56a9
black
alexrockhill Apr 24, 2023
0aac17d
add mark global function, add brainmask (slow because does not have t…
alexrockhill Apr 25, 2023
55a0b30
black
alexrockhill Apr 25, 2023
643c47d
resolve conflicts
alexrockhill Dec 1, 2023
b839bb0
Merge branch 'main' into seg
alexrockhill Dec 1, 2023
ae46a97
style
alexrockhill Dec 1, 2023
6d8ed5e
fix transforms
alexrockhill Dec 1, 2023
bbc4572
style
alexrockhill Dec 1, 2023
7487724
[BUG] Fix marching cubes not in the right space when CT is not aligned
alexrockhill Dec 4, 2023
3ee1c8d
try restricting pyqt version
alexrockhill Dec 4, 2023
59e9200
Merge branch 'bug' of https://github.com/alexrockhill/mne-gui-addons …
alexrockhill Dec 4, 2023
76ce34d
cruft
alexrockhill Dec 4, 2023
a627217
update index, don't plot brain if not aligned
alexrockhill Dec 4, 2023
a0f0c42
don't show brain if not aligned
alexrockhill Dec 4, 2023
739afa7
fix broken link
alexrockhill Dec 4, 2023
3992f91
add tests start
alexrockhill Dec 4, 2023
49a253e
fix all tests are skipped
alexrockhill Dec 4, 2023
a560949
style
alexrockhill Dec 4, 2023
37c66f5
fix dep, tests
alexrockhill Dec 4, 2023
b44c4d7
try again ignore warning
alexrockhill Dec 4, 2023
77d8853
ignore in wrong place
alexrockhill Dec 4, 2023
4c2db14
wrong still
alexrockhill Dec 4, 2023
aef8607
cruft
alexrockhill Dec 4, 2023
290298f
space
alexrockhill Dec 4, 2023
ea08203
finish adding tests
alexrockhill Dec 4, 2023
b8e2c81
fix re
alexrockhill Dec 4, 2023
2632c63
mne-stable errors
alexrockhill Dec 5, 2023
eca9614
working
alexrockhill Dec 6, 2023
1ab565c
1.6 release
alexrockhill Dec 6, 2023
dfdc492
still fails on stable
alexrockhill Dec 6, 2023
5e3804d
fix tests after bug fixes in scan RAS conversion
alexrockhill Dec 6, 2023
df9dd7d
Merge branch 'bug' of https://github.com/alexrockhill/mne-gui-addons …
alexrockhill Dec 6, 2023
bd63afc
fix new changes
alexrockhill Dec 6, 2023
db48a70
fix test
alexrockhill Dec 6, 2023
0937828
style
alexrockhill Dec 6, 2023
a3ef6d5
spelling
alexrockhill Dec 6, 2023
6fd4845
close gui, add qdarkstyle
alexrockhill Dec 6, 2023
f3fea04
allow unclosed
alexrockhill Dec 6, 2023
83ec391
rerun cis
alexrockhill Dec 6, 2023
3bc8b15
Merge branch 'main' into seg
alexrockhill Dec 6, 2023
b1140cd
doubled line
alexrockhill Dec 6, 2023
c3f5609
increase timeout
alexrockhill Dec 6, 2023
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
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
Loading