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

Stack vertices in plot_volume_source_estimates #12025

Merged
merged 10 commits into from
Oct 2, 2023
1 change: 1 addition & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Bugs
- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` by `Paul Roujansky`_)
- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() <mne.transforms.Transform.save>` (:gh:`12004` by `Marijn van Vliet`_)
- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_)
- Fix :func:`~mne.viz.plot_volume_source_estimates` with :class:`~mne.VolSourceEstimate` which include a list of vertices (:gh:`12025` by `Mathieu Scheltienne`_)
- Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_)

API changes
Expand Down
10 changes: 10 additions & 0 deletions mne/minimum_norm/inverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ def _repr_html_(self):
)
return html

@property
def ch_names(self):
"""Name of channels attached to the inverse operator."""
return self["info"].ch_names

@property
def info(self):
""":class:`~mne.Info` attached to the inverse operator."""
return self["info"]


def _pick_channels_inverse_operator(ch_names, inv):
"""Return data channel indices to be used knowing an inverse operator.
Expand Down
29 changes: 14 additions & 15 deletions mne/viz/_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2650,10 +2650,9 @@ def plot_volume_source_estimates(
%(subject_none)s
If ``None``, ``stc.subject`` will be used.
%(subjects_dir)s
mode : str
The plotting mode to use. Either 'stat_map' (default) or 'glass_brain'.
For "glass_brain", activation absolute values are displayed
after being transformed to a standard MNI brain.
mode : ``'stat_map'`` | ``'glass_brain'``
The plotting mode to use. For ``'glass_brain'``, activation absolute values are
displayed after being transformed to a standard MNI brain.
bg_img : instance of SpatialImage | str
The background image used in the nilearn plotting function.
Can also be a string to use the ``bg_img`` file in the subject's
Expand Down Expand Up @@ -2714,10 +2713,11 @@ def plot_volume_source_estimates(
>>> morph = mne.compute_source_morph(src_sample, subject_to='fsaverage') # doctest: +SKIP
>>> fig = stc_vol_sample.plot(morph) # doctest: +SKIP
""" # noqa: E501
from matplotlib import pyplot as plt, colors
import nibabel as nib
from ..source_estimate import VolSourceEstimate
from matplotlib import pyplot as plt, colors

from ..morph import SourceMorph
from ..source_estimate import VolSourceEstimate
from ..source_space._source_space import _ensure_src

if not check_version("nilearn", "0.4"):
Expand Down Expand Up @@ -2745,8 +2745,9 @@ def plot_volume_source_estimates(
level="debug",
)
subject = _check_subject(src_subject, subject, first_kind=kind)
stc_ijk = np.array(np.unravel_index(stc.vertices[0], img.shape[:3], order="F")).T
assert stc_ijk.shape == (len(stc.vertices[0]), 3)
vertices = np.hstack(stc.vertices)
stc_ijk = np.array(np.unravel_index(vertices, img.shape[:3], order="F")).T
assert stc_ijk.shape == (vertices.size, 3)
del kind

# XXX this assumes zooms are uniform, should probably mult by zooms...
Expand All @@ -2756,12 +2757,11 @@ def _cut_coords_to_idx(cut_coords, img):
"""Convert voxel coordinates to index in stc.data."""
ijk = _cut_coords_to_ijk(cut_coords, img)
del cut_coords
logger.debug(" Affine remapped cut coords to [%d, %d, %d] idx" % tuple(ijk))
logger.debug(" Affine remapped cut coords to [%d, %d, %d] idx", tuple(ijk))
dist, loc_idx = dist_to_verts.query(ijk[np.newaxis])
dist, loc_idx = dist[0], loc_idx[0]
logger.debug(
" Using vertex %d at a distance of %d voxels"
% (stc.vertices[0][loc_idx], dist)
" Using vertex %d at a distance of %d voxels", (vertices[loc_idx], dist)
)
return loc_idx

Expand Down Expand Up @@ -2848,7 +2848,7 @@ def _update_timeslice(idx, params):
plot_map_callback(params["img_idx"], title="", cut_coords=cut_coords)

def _update_vertlabel(loc_idx):
vert_legend.get_texts()[0].set_text(f"{stc.vertices[0][loc_idx]}")
vert_legend.get_texts()[0].set_text(f"{vertices[loc_idx]}")

@verbose_dec
def _onclick(event, params, verbose=None):
Expand Down Expand Up @@ -2932,7 +2932,7 @@ def _onclick(event, params, verbose=None):
(stc.times[time_idx],)
+ tuple(cut_coords)
+ tuple(ijk)
+ (stc.vertices[0][loc_idx],)
+ (vertices[loc_idx],)
)
)
del ijk
Expand Down Expand Up @@ -3046,8 +3046,7 @@ def plot_and_correct(*args, **kwargs):

plot_and_correct(stat_map_img=params["img_idx"], title="", cut_coords=cut_coords)

if show:
plt.show()
plt_show(show)
fig.canvas.mpl_connect(
"button_press_event", partial(_onclick, params=params, verbose=verbose)
)
Expand Down
67 changes: 65 additions & 2 deletions mne/viz/tests/test_3d_mpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@
import pytest

from mne import (
compute_covariance,
compute_source_morph,
make_fixed_length_epochs,
make_forward_solution,
read_bem_solution,
read_forward_solution,
VolSourceEstimate,
read_trans,
setup_volume_source_space,
SourceEstimate,
VolSourceEstimate,
VolVectorSourceEstimate,
compute_source_morph,
)
from mne.datasets import testing
from mne.io import read_raw_fif
from mne.minimum_norm import apply_inverse, make_inverse_operator
from mne.utils import catch_logging, _record_warnings
from mne.viz import plot_volume_source_estimates
from mne.viz.utils import _fake_click, _fake_keypress
Expand Down Expand Up @@ -148,3 +156,58 @@ def test_plot_volume_source_estimates_morph():
stc.plot(
sample_src, "sample", subjects_dir, clim=dict(lims=[-1, 2, 3], kind="value")
)


@testing.requires_testing_data
def test_plot_volume_source_estimates_on_vol_labels():
"""Test plot of source estimate on srcs setup on 2 labels."""
pytest.importorskip("nibabel")
pytest.importorskip("dipy")
pytest.importorskip("nilearn")
raw = read_raw_fif(
data_dir / "MEG" / "sample" / "sample_audvis_trunc_raw.fif", preload=False
)
raw.pick("meg").crop(0, 10)
raw.pick(raw.ch_names[::2]).del_proj().load_data()
epochs = make_fixed_length_epochs(raw, preload=True).apply_baseline((None, None))
evoked = epochs.average()
subject = "sample"
bem = read_bem_solution(
subjects_dir / f"{subject}" / "bem" / "sample-320-bem-sol.fif"
)
pos = 25.0 # spacing in mm
volume_label = [
"Right-Cerebral-Cortex",
"Left-Cerebral-Cortex",
]
src = setup_volume_source_space(
subject,
subjects_dir=subjects_dir,
pos=pos,
mri=subjects_dir / subject / "mri" / "aseg.mgz",
bem=bem,
volume_label=volume_label,
add_interpolator=False,
)
trans = read_trans(data_dir / "MEG" / "sample" / "sample_audvis_trunc-trans.fif")
fwd = make_forward_solution(
evoked.info,
trans,
src,
bem,
meg=True,
eeg=False,
mindist=0,
n_jobs=1,
)
cov = compute_covariance(
epochs,
tmin=None,
tmax=None,
method="empirical",
)
inverse_operator = make_inverse_operator(evoked.info, fwd, cov, loose=1, depth=0.8)
stc = apply_inverse(
evoked, inverse_operator, 1.0 / 3**2, method="sLORETA", pick_ori=None
)
stc.plot(src, subject, subjects_dir, initial_time=0.03)
Loading