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

[BUG] Fix marching cubes not in the right space when CT is not aligned #25

Merged
merged 21 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 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 "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
pip install -ve ./mne-python .
- run:
name: Check Qt
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ jobs:
include:
- os: macos-latest
python-version: "3.10"
mne-version: mne-stable
mne-version: mne-main # pin to main for import functions until 1.7 release
qt: PyQt6
# Old (and PyQt5)
- os: ubuntu-latest
python-version: "3.9"
mne-version: mne-stable
mne-version: mne-main # pin to main for import functions until 1.7 release
qt: PyQt5
# PySide2
- os: ubuntu-latest
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
if: "matrix.mne-version == 'mne-main'"
run: git clone --single-branch --branch main https://github.com/mne-tools/mne-python.git
- run: pip install -ve ./mne-python
- run: pip install -v ${{ matrix.qt }} "PyQt6-Qt6!=6.6.1"
- run: pip install -v ${{ matrix.qt }}!=6.6.1 "PyQt6-Qt6!=6.6.1"
- run: pip install -ve .[tests]
- run: mne sys_info
- run: |
Expand Down
101 changes: 100 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,103 @@
.. -*- mode: rst -*-

MNE-GUI-Addons
--------------

MNE-Python GUI addons.
This project contains graphical user interface (GUI) addons that complement the
scripting functionality in ``mne-python``, ``mne-bids`` and other ``mne`` projects
for tasks that require a user to interact with data, usually that is 3D, in order
for them to be accomplished. For example:

- Locating intracranial electrode contacts from a computed tomography (CT) or
magnetic resonance (MR) scan
- Viewing time-frequency source estimate data

Documentation
^^^^^^^^^^^^^

Please see https://mne.tools/mne-gui-addons/ for installation instructions, tutorials,
and examples for a wide variety of topics, contributing guidelines, and an API
reference.


Reporting
^^^^^^^^^

If you find a bug or have an idea for a new feature that should be added,
please use the
`issue tracker <https://github.com/mne-tools/mne-gui-addons/issues/new/choose>`__ of
our GitHub repository.


Installation
^^^^^^^^^^^^

To install the latest stable version of MNE-GUI-Addons with minimal dependencies
only, use pip in a terminal:

.. code-block:: console

$ pip install --upgrade mne-gui-addons


Get the development version
^^^^^^^^^^^^^^^^^^^^^^^^^^^

To install the latest development version of MNE-Python using pip, open a
terminal and type:

.. code-block:: console

$ pip install --upgrade git+https://github.com/mne-tools/mne-gui-addons@main

To clone the repository with `git <https://git-scm.com/>`__, open a terminal
and type:

.. code-block:: console

$ git clone https://github.com/mne-tools/mne-gui-addons.git


Contributing
^^^^^^^^^^^^

Please see the `contributing guidelines <https://mne.tools/dev/development/contributing.html>`__ on our documentation website.


License
^^^^^^^

MNE-GUI-Addons is **BSD-licensed** (BSD-3-Clause):

This software is OSI Certified Open Source Software.
OSI Certified is a certification mark of the Open Source Initiative.

Copyright (c) 2011-2023, authors of MNE-Python.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the names of MNE-Python authors nor the names of any
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

**This software is provided by the copyright holders and contributors
"as is" and any express or implied warranties, including, but not
limited to, the implied warranties of merchantability and fitness for
a particular purpose are disclaimed. In no event shall the copyright
owner or contributors be liable for any direct, indirect, incidental,
special, exemplary, or consequential damages (including, but not
limited to, procurement of substitute goods or services; loss of use,
data, or profits; or business interruption) however caused and on any
theory of liability, whether in contract, strict liability, or tort
(including negligence or otherwise) arising in any way out of the use
of this software, even if advised of the possibility of such
damage.**
9 changes: 8 additions & 1 deletion doc/index.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
MNE-GUI-Addons
--------------

MNE-Python GUI addons.
This project contains graphical user interface (GUI) addons that complement the
scripting functionality in ``mne-python``, ``mne-bids`` and other ``mne`` projects
for tasks that require a user to interact with data, usually that is 3D, in order
for them to be accomplished. For example:

- Locating intracranial electrode contacts from a computed tomography (CT) or
magnetic resonance (MR) scan
- Viewing time-frequency source estimate data

.. toctree::
:maxdepth: 1
Expand Down
6 changes: 6 additions & 0 deletions examples/ieeg_locate.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,9 @@ def plot_overlay(image, compare, title, thresh=None):
# :ref:`mne watershed_bem` or :ref:`mne flash_bem`.
# First, let's plot the localized sensor positions without modification.

# reload original found positions
raw_ecog = mne.io.read_raw(misc_path / "ecog" / "sample_ecog_ieeg.fif")

# plot projected sensors
brain_kwargs = dict(cortex="low_contrast", alpha=0.2, background="white")
brain = mne.viz.Brain(
Expand Down Expand Up @@ -537,6 +540,9 @@ def plot_overlay(image, compare, title, thresh=None):
# identify their location. The estimated head->mri ``trans`` was used
# when the electrode contacts were localized so we need to use it again here.

# reload original found positions
raw = mne.io.read_raw(misc_path / "seeg" / "sample_seeg_ieeg.fif")

# plot the alignment
brain = mne.viz.Brain("sample_seeg", subjects_dir=misc_path / "seeg", **brain_kwargs)
brain.add_sensors(raw.info, trans=subj_trans)
Expand Down
18 changes: 10 additions & 8 deletions examples/locate_ieeg_micro.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,20 @@
# launch the viewer with only the CT (note, we won't be able to use
# the MR in this case to help determine which brain area the contact is
# in), and use the user interface to find the locations of the contacts
gui = mne_gui.locate_ieeg(raw.info, head_ct_t, CT_orig)
gui = mne_gui.locate_ieeg(
raw.info, head_ct_t, CT_orig, subject="sample_seeg", subjects_dir=subjects_dir
)

# we'll programmatically mark all the contacts on one electrode shaft
for i, pos in enumerate(
[
(-52.66, -40.84, -26.99),
(-55.47, -38.03, -27.92),
(-57.68, -36.27, -28.85),
(-59.89, -33.81, -29.32),
(-62.57, -31.35, -30.37),
(-65.13, -29.07, -31.30),
(-67.57, -26.26, -31.88),
(-158.90, -78.84, -119.97),
(-161.71, -77.91, -117.16),
(-163.92, -76.98, -115.40),
(-166.13, -76.51, -112.94),
(-168.81, -75.46, -110.49),
(-171.37, -74.53, -108.20),
(-173.81, -73.95, -105.40),
]
):
gui.set_RAS(pos)
Expand Down
79 changes: 43 additions & 36 deletions mne_gui_addons/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def _load_image_data(self, base_image=None):
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._mri_data = None
self._mr_data = None
self._head = None
self._lh = self._rh = None
else:
Expand All @@ -170,45 +170,41 @@ def _load_image_data(self, base_image=None):
if op.isfile(op.join(self._subject_dir, "mri", "brain.mgz"))
else "T1"
)
self._mri_data, vox_mri_t, vox_scan_ras_t, ras_vox_scan_ras_t = _load_image(
op.join(self._subject_dir, "mri", f"{mri_img}.mgz")
)
(
self._mr_data,
mr_vox_mri_t,
mr_vox_scan_ras_t,
mr_ras_vox_scan_ras_t,
) = _load_image(op.join(self._subject_dir, "mri", f"{mri_img}.mgz"))

# ready alternate base image if provided, otherwise use brain/T1
self._base_mr_aligned = True
if base_image is None:
assert self._mri_data is not None
self._base_data = self._mri_data
self._vox_mri_t = vox_mri_t
self._vox_scan_ras_t = vox_scan_ras_t
self._ras_vox_scan_ras_t = ras_vox_scan_ras_t
assert self._mr_data is not None
self._base_data = self._mr_data
self._vox_mri_t = mr_vox_mri_t
self._vox_scan_ras_t = mr_vox_scan_ras_t
self._ras_vox_scan_ras_t = mr_ras_vox_scan_ras_t
else:
(
self._base_data,
self._vox_mri_t,
self._vox_scan_ras_t,
self._ras_vox_scan_ras_t,
) = _load_image(base_image)
if self._mri_data is not None:
if self._mri_data.shape != self._base_data.shape or not np.allclose(
self._vox_scan_ras_t, vox_scan_ras_t, rtol=1e-6
if self._mr_data is not None:
if self._mr_data.shape != self._base_data.shape or not np.allclose(
self._vox_scan_ras_t, mr_vox_scan_ras_t, rtol=1e-6
):
raise ValueError(
"Base image is not aligned to MRI, got "
f"Base shape={self._base_data.shape}, "
f"MRI shape={self._mri_data.shape}, "
f"Base affine={vox_scan_ras_t} and "
f"MRI affine={self._vox_scan_ras_t}, "
"please provide an aligned image or do not use the "
"``subject`` and ``subjects_dir`` arguments"
)
self._base_mr_aligned = False

self._mri_vox_t = np.linalg.inv(self._vox_mri_t)
self._scan_ras_vox_t = np.linalg.inv(self._vox_scan_ras_t)
self._scan_ras_ras_vox_t = np.linalg.inv(
self._ras_vox_scan_ras_t
) # to RAS voxels
self._scan_ras_ras_vox_t = np.linalg.inv(self._ras_vox_scan_ras_t)

self._scan_ras_mri_t = np.dot(self._vox_mri_t, self._scan_ras_vox_t)
self._mri_scan_ras_t = np.dot(self._vox_scan_ras_t, self._mri_vox_t)

self._voxel_sizes = np.array(self._base_data.shape)
self._voxel_ratios = self._voxel_sizes / self._voxel_sizes.min()

Expand All @@ -231,6 +227,10 @@ def _load_image_data(self, base_image=None):
op.join(self._subject_dir, "surf", "lh.seghead")
)
assert _frame_to_str[self._head["coord_frame"]] == "mri"
# transform to scanner RAS
self._head["rr"] = apply_trans(
self._mri_scan_ras_t, self._head["rr"] * 1000
)
else:
warn(
"`seghead` not found, using marching cubes on base image "
Expand All @@ -251,8 +251,16 @@ def _load_image_data(self, base_image=None):
if op.exists(surf_fname.format(hemi="lh")):
self._lh = _read_mri_surface(surf_fname.format(hemi="lh"))
assert _frame_to_str[self._lh["coord_frame"]] == "mri"
# convert to scanner RAS
self._lh["rr"] = apply_trans(
self._mri_scan_ras_t, self._lh["rr"] * 1000
)
self._rh = _read_mri_surface(surf_fname.format(hemi="rh"))
assert _frame_to_str[self._rh["coord_frame"]] == "mri"
# convert to scanner RAS
self._rh["rr"] = apply_trans(
self._mri_scan_ras_t, self._rh["rr"] * 1000
)
else:
warn(
"`pial` surface not found, skipping adding to 3D "
Expand Down Expand Up @@ -331,17 +339,20 @@ def _plot_images(self):
"button_release_event", partial(self._on_click, axis=axis)
)
# add head and brain in mm (convert from m)
if self._head is None:
if self._head is None or not self._base_mr_aligned:
logger.debug(
"Using marching cubes on the base image for the "
"3D visualization panel"
)
# in this case, leave in voxel coordinates
thresh = np.quantile(self._base_data, 0.95)
if not (self._base_data < thresh).any():
thresh = self._base_data.min()
rr, tris = _marching_cubes(
np.where(self._base_data < np.quantile(self._base_data, 0.95), 0, 1),
np.where(self._base_data <= thresh, 0, 1),
[1],
)[0]
rr = apply_trans(self._vox_mri_t, rr)
rr = apply_trans(self._ras_vox_scan_ras_t, rr) # base image vox -> RAS
self._renderer.mesh(
*rr.T,
triangles=tris,
Expand All @@ -350,27 +361,26 @@ def _plot_images(self):
reset_camera=False,
render=False,
)
self._renderer.set_camera(focalpoint=rr.mean(axis=0))
else:
self._renderer.mesh(
*self._head["rr"].T * 1000,
*self._head["rr"].T,
triangles=self._head["tris"],
color="gray",
opacity=0.2,
reset_camera=False,
render=False,
)
if self._lh is not None and self._rh is not None:
if self._lh is not None and self._rh is not None and self._base_mr_aligned:
self._renderer.mesh(
*self._lh["rr"].T * 1000,
*self._lh["rr"].T,
triangles=self._lh["tris"],
color="white",
opacity=0.2,
reset_camera=False,
render=False,
)
self._renderer.mesh(
*self._rh["rr"].T * 1000,
*self._rh["rr"].T,
triangles=self._rh["tris"],
color="white",
opacity=0.2,
Expand Down Expand Up @@ -411,10 +421,7 @@ def _configure_status_bar(self, hbox=None):

def _update_camera(self, render=False):
"""Update the camera position."""
self._renderer.set_camera(
focalpoint=tuple(apply_trans(self._scan_ras_mri_t, self._ras)),
distance="auto",
)
self._renderer.set_camera(focalpoint=tuple(self._ras), distance="auto")
if render:
self._renderer._update()

Expand Down
Loading
Loading