diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index b9ec5e5777f..f6255f7cf90 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -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() ` (: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 diff --git a/mne/minimum_norm/inverse.py b/mne/minimum_norm/inverse.py index e505b155490..7b23d137858 100644 --- a/mne/minimum_norm/inverse.py +++ b/mne/minimum_norm/inverse.py @@ -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. diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 2b994f5f9bb..f4aa2b1999c 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -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 @@ -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"): @@ -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... @@ -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 @@ -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): @@ -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 @@ -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) ) diff --git a/mne/viz/tests/test_3d_mpl.py b/mne/viz/tests/test_3d_mpl.py index 9fdd1d02f85..2060de1ebbe 100644 --- a/mne/viz/tests/test_3d_mpl.py +++ b/mne/viz/tests/test_3d_mpl.py @@ -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 @@ -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)