diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index e35a5be0a50..96bde9ed148 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -33,6 +33,10 @@ Changelog - Add support for mixed source spaces to :func:`mne.compute_source_morph` by `Eric Larson`_ +- Add support for omitting the SDR step in volumetric morphing by passing ``n_iter_sdr=()`` to `mne.compute_source_morph` by `Eric Larson`_ + +- Add support for passing a string argument to ``bg_img`` in `mne.viz.plot_volume_source_estimates` by `Eric Larson`_ + - Add support for providing the destination surface source space in the ``src_to`` argument of :func:`mne.compute_source_morph` by `Eric Larson`_ - Add ``tol_kind`` option to :func:`mne.compute_rank` by `Eric Larson`_ @@ -116,6 +120,10 @@ Bug - Fix bug with :func:`mne.compute_source_morph` when more than one volume source space was present (e.g., when using labels) where only the first label would be interpolated when ``mri_resolution=True`` by `Eric Larson`_ +- Fix bug with :func:`mne.compute_source_morph` when morphing to a volume source space when ``src_to`` is used and the destination subject is not ``fsaverage`` by `Eric Larson`_ + +- Fix bug with :func:`mne.compute_source_morph` where outermost voxels in the destination source space could be errantly omitted by `Eric Larson`_ + - Fix bug with :func:`mne.minimum_norm.compute_source_psd_epochs` and :func:`mne.minimum_norm.source_band_induced_power` raised errors when ``method='eLORETA'`` by `Eric Larson`_ - Fix bug with :func:`mne.minimum_norm.apply_inverse` where the explained variance did not work for complex data by `Eric Larson`_ diff --git a/examples/inverse/plot_morph_volume_stc.py b/examples/inverse/plot_morph_volume_stc.py index a6935de3fc9..4b7581ab007 100644 --- a/examples/inverse/plot_morph_volume_stc.py +++ b/examples/inverse/plot_morph_volume_stc.py @@ -9,7 +9,8 @@ :class:`mne.VolSourceEstimate` to a common reference space. We achieve this using :class:`mne.SourceMorph`. Pre-computed data will be morphed based on an affine transformation and a nonlinear registration method -known as Symmetric Diffeomorphic Registration (SDR) by Avants et al. [1]_. +known as Symmetric Diffeomorphic Registration (SDR) by +:footcite:`AvantsEtAl2008`. Transformation is estimated from the subject's anatomical T1 weighted MRI (brain) to `FreeSurfer's 'fsaverage' T1 weighted MRI (brain) @@ -18,13 +19,6 @@ Afterwards the transformation will be applied to the volumetric source estimate. The result will be plotted, showing the fsaverage T1 weighted anatomical MRI, overlaid with the morphed volumetric source estimate. - -References ----------- -.. [1] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. """ # Author: Tommy Clausner # @@ -153,3 +147,6 @@ # # >>> morph.apply(stc) # +# References +# ---------- +# .. footbibliography:: diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index cfe5f58dd28..ebdb25300cb 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -239,7 +239,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, path = _get_path(path, key, name) # To update the testing or misc dataset, push commits, then make a new # release on GitHub. Then update the "releases" variable: - releases = dict(testing='0.92', misc='0.6') + releases = dict(testing='0.93', misc='0.6') # And also update the "md5_hashes['testing']" variable below. # To update any other dataset, update the data archive itself (upload @@ -326,7 +326,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, sample='12b75d1cb7df9dfb4ad73ed82f61094f', somato='ea825966c0a1e9b2f84e3826c5500161', spm='9f43f67150e3b694b523a21eb929ea75', - testing='42daafd1b882da2ef041de860ca6e771', + testing='2eb998a0893a28faedd583973c70b517', multimodal='26ec847ae9ab80f58f204d09e2c08367', fnirs_motor='c4935d19ddab35422a69f3326a01fef8', opm='370ad1dcfd5c47e029e692c85358a374', diff --git a/mne/morph.py b/mne/morph.py index f5629978ae7..9dea5f94fc7 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -17,6 +17,7 @@ _BaseVolSourceEstimate, _BaseSourceEstimate, _get_ico_tris) from .source_space import SourceSpaces, _ensure_src from .surface import read_morph_map, mesh_edges, read_surface, _compute_nearest +from .transforms import _angle_between_quats, rot_to_quat from .utils import (logger, verbose, check_version, get_subjects_dir, warn as warn_, fill_doc, _check_option, _validate_type, BunchConst, wrapped_stdout, _check_fname, warn, @@ -33,8 +34,9 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', """Create a SourceMorph from one subject to another. Method is based on spherical morphing by FreeSurfer for surface - cortical estimates [1]_ and Symmetric Diffeomorphic Registration - for volumic data [2]_. + cortical estimates :footcite:`GreveEtAl2013` and + Symmetric Diffeomorphic Registration for volumic data + :footcite:`AvantsEtAl2008`. Parameters ---------- @@ -134,7 +136,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', obtained as described `here `_. For statistical comparisons between hemispheres, use of the symmetric ``fsaverage_sym`` - model is recommended to minimize bias [1]_. + model is recommended to minimize bias :footcite:`GreveEtAl2013`. .. versionadded:: 0.17.0 @@ -143,14 +145,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', References ---------- - .. [1] Greve D. N., Van der Haegen L., Cai Q., Stufflebeam S., Sabuncu M. - R., Fischl B., Brysbaert M. - A Surface-based Analysis of Language Lateralization and Cortical - Asymmetry. Journal of Cognitive Neuroscience 25(9), 1477-1492, 2013. - .. [2] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. + .. footbibliography:: """ src_data, kind, src_subject = _get_src_data(src) subject_from = _check_subject_src(subject_from, src_subject) @@ -179,13 +174,13 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', _check_dep(nibabel='2.1.0', dipy='0.10.1') import nibabel as nib - logger.info('volume source space(s) present...') + logger.info('Volume source space(s) present...') # load moving MRI mri_subpath = op.join('mri', 'brain.mgz') mri_path_from = op.join(subjects_dir, subject_from, mri_subpath) - logger.info('loading %s as "from" volume' % mri_path_from) + logger.info(' Loading %s as "from" volume' % mri_path_from) with warnings.catch_warnings(): mri_from = nib.load(mri_path_from) @@ -194,7 +189,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', mri_path_to = op.join(subjects_dir, subject_to, mri_subpath) if not op.isfile(mri_path_to): raise IOError('cannot read file: %s' % mri_path_to) - logger.info('loading %s as "to" volume' % mri_path_to) + logger.info(' Loading %s as "to" volume' % mri_path_to) with warnings.catch_warnings(): mri_to = nib.load(mri_path_to) @@ -206,11 +201,15 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', 'mixed source space') else: surf_offset = 2 if src_to.kind == 'mixed' else 0 - src_data['to_vox_map'] = ( - src_to[-1]['shape'], src_to[-1]['src_mri_t']['trans'] * - np.array([[1e3, 1e3, 1e3, 1]]).T) + # All of our computations are in RAS (like img.affine), so we need + # to get the transformation from RAS to the source space + # subsampling of vox (src), not MRI (FreeSurfer surface RAS) to src + src_ras_t = np.dot(src_to[-1]['mri_ras_t']['trans'], + src_to[-1]['src_mri_t']['trans']) + src_ras_t[:3] *= 1e3 + src_data['to_vox_map'] = (src_to[-1]['shape'], src_ras_t) vertices_to_vol = [s['vertno'] for s in src_to[surf_offset:]] - zooms_src_to = np.diag(src_data['to_vox_map'][1])[:3] + zooms_src_to = np.diag(src_to[-1]['src_mri_t']['trans'])[:3] * 1000 assert (zooms_src_to[0] == zooms_src_to).all() zooms_src_to = tuple(zooms_src_to) @@ -312,7 +311,7 @@ class SourceMorph(object): Number of levels (``len(niter_sdr)``) and number of iterations per level - for each successive stage of iterative refinement - to perform the Symmetric Diffeomorphic Registration (sdr) - transform [2]_. + transform :footcite:`AvantsEtAl2008`. spacing : int | list | None See :func:`mne.compute_source_morph`. smooth : int | str | None @@ -321,7 +320,7 @@ class SourceMorph(object): Morph across hemisphere. morph_mat : scipy.sparse.csr_matrix The sparse surface morphing matrix for spherical surface - based morphing [1]_. + based morphing :footcite:`GreveEtAl2013`. vertices_to : list of ndarray The destination surface vertices. shape : tuple @@ -344,14 +343,7 @@ class SourceMorph(object): References ---------- - .. [1] Greve D. N., Van der Haegen L., Cai Q., Stufflebeam S., Sabuncu M. - R., Fischl B., Brysbaert M. - A Surface-based Analysis of Language Lateralization and Cortical - Asymmetry. Journal of Cognitive Neuroscience 25(9), 1477-1492, 2013. - .. [2] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. + .. footbibliography:: """ def __init__(self, subject_from, subject_to, kind, zooms, @@ -476,14 +468,16 @@ def _morph_one_vol(self, one): img_to, self.affine, _get_zooms_orig(self), self.zooms) # morph data - img_to = self.sdr_morph.transform(self.pre_affine.transform(img_to)) + img_to = self.pre_affine.transform(img_to) + if self.sdr_morph is not None: + img_to = self.sdr_morph.transform(img_to) # subselect the correct cube if src_to is provided if self.src_data['to_vox_map'] is not None: # order=0 (nearest) should be fine since it's just subselecting - img_to = _get_img_fdata(resample_from_to( - SpatialImage(img_to, self.affine), - self.src_data['to_vox_map'], order=0)) + img_to = SpatialImage(img_to, self.affine) + img_to = resample_from_to(img_to, self.src_data['to_vox_map'], 1) + img_to = _get_img_fdata(img_to) # reshape to nvoxel x nvol: # in the MNE definition of volume source spaces, @@ -889,7 +883,7 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): mri_to = nib.Nifti1Image(mri_to_res, mri_to_res_affine) affine = mri_to.affine - mri_to = _get_img_fdata(mri_to) # to ndarray + mri_to = _get_img_fdata(mri_to).copy() # to ndarray mri_to /= mri_to.max() mri_from_affine = mri_from.affine # get mri_from to world transform mri_from = _get_img_fdata(mri_from) # to ndarray @@ -919,6 +913,13 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): rigid = affreg.optimize( mri_to, mri_from, transforms.RigidTransform3D(), None, affine, mri_from_affine, starting_affine=translation.affine) + dist = np.linalg.norm(rigid.affine[:3, 3]) + angle = np.rad2deg(_angle_between_quats( + np.zeros(3), rot_to_quat(rigid.affine[:3, :3]))) + + logger.info(f'Translation: {dist:5.1f} mm') + logger.info(f'Rotation: {angle:5.1f}°') + logger.info('') # affine transform (translation + rotation + scaling) logger.info('Optimizing full affine:') @@ -928,12 +929,33 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): affine, mri_from_affine, starting_affine=rigid.affine) # compute mapping - sdr = imwarp.SymmetricDiffeomorphicRegistration( - metrics.CCMetric(3), list(niter_sdr)) - logger.info('Optimizing SDR:') - with wrapped_stdout(indent=' '): - sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) - shape = tuple(sdr_morph.domain_shape) # should be tuple of int + mri_from_to = pre_affine.transform(mri_from) + shape = tuple(pre_affine.domain_shape) + if len(niter_sdr): + sdr = imwarp.SymmetricDiffeomorphicRegistration( + metrics.CCMetric(3), list(niter_sdr)) + logger.info('Optimizing SDR:') + with wrapped_stdout(indent=' '): + sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) + assert shape == tuple(sdr_morph.domain_shape) # should be tuple of int + mri_from_to = sdr_morph.transform(mri_from_to) + else: + sdr_morph = None + + mri_to, mri_from_to = mri_to.ravel(), mri_from_to.ravel() + mri_from_to /= np.linalg.norm(mri_from_to) + mri_to /= np.linalg.norm(mri_to) + r2 = 100 * (mri_to @ mri_from_to) + logger.info(f'Variance explained by morph: {r2:0.1f}%') + + # To debug to_vox_map, this can be used: + # from nibabel.processing import resample_from_to + # mri_from_to = sdr_morph.transform(pre_affine.transform(mri_from)) + # mri_from_to = nib.Nifti1Image(mri_from_to, affine) + # fig1 = mri_from_to.orthoview() + # mri_from_to_cut = resample_from_to(mri_from_to, to_vox_map, 1) + # fig2 = mri_from_to_cut.orthoview() + return shape, zooms, affine, pre_affine, sdr_morph diff --git a/mne/source_estimate.py b/mne/source_estimate.py index 5f371348a24..f4ab1f84d46 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -1822,7 +1822,7 @@ class _BaseVolSourceEstimate(_BaseSourceEstimate): @copy_function_doc_to_method_doc(plot_volume_source_estimates) def plot(self, src, subject=None, subjects_dir=None, mode='stat_map', - bg_img=None, colorbar=True, colormap='auto', clim='auto', + bg_img='T1.mgz', colorbar=True, colormap='auto', clim='auto', transparent='auto', show=True, initial_time=None, initial_pos=None, verbose=None): data = self.magnitude() if self._data_ndim == 3 else self diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 0497d2f781b..6ba0785746f 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -23,7 +23,7 @@ from mne.minimum_norm import (apply_inverse, read_inverse_operator, make_inverse_operator) from mne.source_space import (get_volume_labels_from_aseg, _get_mri_info_data, - _get_atlas_values) + _get_atlas_values, _add_interpolator) from mne.utils import (run_tests_if_main, requires_nibabel, check_version, requires_dipy, requires_h5py) from mne.fixes import _get_args @@ -39,16 +39,19 @@ 'sample_audvis_trunc-meg-vol-7-meg-inv.fif') fname_fwd_vol = op.join(sample_dir, 'sample_audvis_trunc-meg-vol-7-fwd.fif') -fname_vol = op.join(sample_dir, - 'sample_audvis_trunc-grad-vol-7-fwd-sensmap-vol.w') +fname_vol_w = op.join(sample_dir, + 'sample_audvis_trunc-grad-vol-7-fwd-sensmap-vol.w') fname_inv_surf = op.join(sample_dir, 'sample_audvis_trunc-meg-eeg-oct-6-meg-inv.fif') fname_fmorph = op.join(data_path, 'MEG', 'sample', 'fsaverage_audvis_trunc-meg') fname_smorph = op.join(sample_dir, 'sample_audvis_trunc-meg') fname_t1 = op.join(subjects_dir, 'sample', 'mri', 'T1.mgz') +fname_vol = op.join(subjects_dir, 'sample', 'bem', 'sample-volume-7mm-src.fif') fname_brain = op.join(subjects_dir, 'sample', 'mri', 'brain.mgz') fname_aseg = op.join(subjects_dir, 'sample', 'mri', 'aseg.mgz') +fname_fs_vol = op.join(subjects_dir, 'fsaverage', 'bem', + 'fsaverage-vol7-nointerp-src.fif.gz') fname_aseg_fs = op.join(subjects_dir, 'fsaverage', 'mri', 'aseg.mgz') fname_stc = op.join(sample_dir, 'fsaverage_audvis_trunc-meg') @@ -196,6 +199,16 @@ def test_surface_source_morph_round_trip(smooth, lower, upper, n_warn): stc_back = morph_back.apply(stc_fs) corr = np.corrcoef(stc.data.ravel(), stc_back.data.ravel())[0, 1] assert lower <= corr <= upper + # check the round-trip power + assert_power_preserved(stc, stc_back) + + +def assert_power_preserved(orig, new, limits=(1., 1.05)): + """Assert that the power is preserved during a round-trip morph.""" + __tracebackhide__ = True + power_ratio = np.linalg.norm(orig.data) / np.linalg.norm(new.data) + min_, max_ = limits + assert min_ < power_ratio < max_, 'Power ratio' @requires_h5py @@ -245,7 +258,7 @@ def test_surface_vector_source_morph(tmpdir): assert isinstance(source_morph_surf.apply(stc_surf), SourceEstimate) # degenerate - stc_vol = read_source_estimate(fname_vol, 'sample') + stc_vol = read_source_estimate(fname_vol_w, 'sample') with pytest.raises(TypeError, match='stc_from must be an instance'): source_morph_surf.apply(stc_vol) @@ -259,7 +272,7 @@ def test_volume_source_morph(tmpdir): """Test volume source estimate morph, special cases and exceptions.""" import nibabel as nib inverse_operator_vol = read_inverse_operator(fname_inv_vol) - stc_vol = read_source_estimate(fname_vol, 'sample') + stc_vol = read_source_estimate(fname_vol_w, 'sample') # check for invalid input type with pytest.raises(TypeError, match='src must be'): @@ -284,7 +297,7 @@ def test_volume_source_morph(tmpdir): with pytest.raises(ValueError, match='Only surface.*sparse morph'): compute_source_morph(src=src, sparse=True, subjects_dir=subjects_dir) - # terrible quality buts fast + # terrible quality but fast zooms = 20 kwargs = dict(zooms=zooms, niter_sdr=(1,), niter_affine=(1,)) source_morph_vol = compute_source_morph( @@ -433,6 +446,87 @@ def test_volume_source_morph(tmpdir): source_morph_vol.apply(stc_vol_bad) +@requires_h5py +@requires_nibabel() +@requires_dipy() +@pytest.mark.slowtest +@testing.requires_testing_data +@pytest.mark.parametrize('subject_from, subject_to, lower, upper', [ + ('sample', 'fsaverage', 8.5, 9), + ('fsaverage', 'fsaverage', 7, 7.5), + ('sample', 'sample', 6, 7), +]) +def test_volume_source_morph_round_trip( + tmpdir, subject_from, subject_to, lower, upper): + """Test volume source estimate morph round-trips well.""" + import nibabel as nib + from nibabel.processing import resample_from_to + src = dict() + if 'sample' in (subject_from, subject_to): + src['sample'] = mne.read_source_spaces(fname_vol) + src['sample'][0]['subject_his_id'] = 'sample' + assert src['sample'][0]['nuse'] == 4157 + if 'fsaverage' in (subject_from, subject_to): + # Created to save space with: + # + # bem = op.join(op.dirname(mne.__file__), 'data', 'fsaverage', + # 'fsaverage-inner_skull-bem.fif') + # src_fsaverage = mne.setup_volume_source_space( + # 'fsaverage', pos=7., bem=bem, mindist=0, + # subjects_dir=subjects_dir, add_interpolator=False) + # mne.write_source_spaces(fname_fs_vol, src_fsaverage, overwrite=True) + # + # For speed we do it without the interpolator because it's huge. + src['fsaverage'] = mne.read_source_spaces(fname_fs_vol) + src['fsaverage'][0].update( + vol_dims=np.array([23, 29, 25]), seg_name='brain') + _add_interpolator(src['fsaverage'], True) + assert src['fsaverage'][0]['nuse'] == 6379 + src_to, src_from = src[subject_to], src[subject_from] + del src + # No SDR just for speed once everything works + kwargs = dict(niter_sdr=(), niter_affine=(1,), + subjects_dir=subjects_dir, verbose=True) + morph_from_to = compute_source_morph( + src=src_from, src_to=src_to, subject_to=subject_to, **kwargs) + morph_to_from = compute_source_morph( + src=src_to, src_to=src_from, subject_to=subject_from, **kwargs) + use = np.linspace(0, src_from[0]['nuse'] - 1, 10).round().astype(int) + stc_from = VolSourceEstimate( + np.eye(src_from[0]['nuse'])[:, use], [src_from[0]['vertno']], 0, 1) + stc_from_rt = morph_to_from.apply(morph_from_to.apply(stc_from)) + maxs = np.argmax(stc_from_rt.data, axis=0) + src_rr = src_from[0]['rr'][src_from[0]['vertno']] + dists = 1000 * np.linalg.norm(src_rr[use] - src_rr[maxs], axis=1) + mu = np.mean(dists) + assert lower <= mu < upper # fsaverage=7.97; 25.4 without src_ras_t fix + # check that pre_affine is close to identity when subject_to==subject_from + if subject_to == subject_from: + for morph in (morph_to_from, morph_from_to): + assert_allclose( + morph.pre_affine.affine, np.eye(4), atol=1e-2) + # check that power is more or less preserved + ratio = stc_from.data.size / stc_from_rt.data.size + limits = ratio * np.array([1, 1.2]) + stc_from.crop(0, 0)._data.fill(1.) + stc_from_rt = morph_to_from.apply(morph_from_to.apply(stc_from)) + assert_power_preserved(stc_from, stc_from_rt, limits=limits) + # before and after morph, check the proportion of vertices + # that are inside and outside the brainmask.mgz + brain = nib.load(op.join(subjects_dir, subject_from, 'mri', 'brain.mgz')) + mask = _get_img_fdata(brain) > 0 + if subject_from == subject_to == 'sample': + for stc in [stc_from, stc_from_rt]: + img = stc.as_volume(src_from, mri_resolution=True) + img = nib.Nifti1Image(_get_img_fdata(img)[:, :, :, 0], img.affine) + img = _get_img_fdata(resample_from_to(img, brain, order=1)) + assert img.shape == mask.shape + in_ = img[mask].astype(bool).mean() + out = img[~mask].astype(bool).mean() + assert 0.97 < in_ < 0.98 + assert out < 0.02 + + @pytest.mark.slowtest @testing.requires_testing_data def test_morph_stc_dense(): diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index aa953281341..299ef72dcf9 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -42,6 +42,7 @@ _ensure_int, _validate_type, _check_option) from .utils import (mne_analyze_colormap, _get_color_list, plt_show, tight_layout, figure_nobar, _check_time_unit) +from .misc import _check_mri from ..bem import (ConductorModel, _bem_find_surface, _surf_dict, _surf_name, read_bem_surfaces) @@ -1811,10 +1812,20 @@ def _ijk_to_cut_coords(ijk, img): return apply_trans(img.affine, ijk) +def _load_subject_mri(mri, stc, subject, subjects_dir, name): + import nibabel as nib + from nibabel.spatialimages import SpatialImage + _validate_type(mri, ('path-like', SpatialImage), name) + if isinstance(mri, str): + subject = _check_subject(stc.subject, subject, True) + mri = nib.load(_check_mri(mri, subject, subjects_dir)) + return mri + + @verbose def plot_volume_source_estimates(stc, src, subject=None, subjects_dir=None, - mode='stat_map', bg_img=None, colorbar=True, - colormap='auto', clim='auto', + mode='stat_map', bg_img='T1.mgz', + colorbar=True, colormap='auto', clim='auto', transparent=None, show=True, initial_time=None, initial_pos=None, verbose=None): @@ -1839,9 +1850,10 @@ def plot_volume_source_estimates(stc, src, subject=None, subjects_dir=None, 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. - bg_img : instance of SpatialImage | None + bg_img : instance of SpatialImage | str The background image used in the nilearn plotting function. - If None, it is the T1.mgz file that is found in the subjects_dir. + Can also be a string to use the ``bg_img`` file in the subject's + MRI directory (default is ``'T1.mgz'``). Not used in "glass brain" plotting. colorbar : bool, optional If True, display a colorbar on the right of the plots. @@ -2071,11 +2083,9 @@ def _onclick(event, params, verbose=None): bg_img = None # not used else: # stat_map if bg_img is None: - subject = _check_subject(stc.subject, subject, True) - subjects_dir = get_subjects_dir(subjects_dir=subjects_dir, - raise_error=True) - t1_fname = op.join(subjects_dir, subject, 'mri', 'T1.mgz') - bg_img = nib.load(t1_fname) + bg_img = 'T1.mgz' + bg_img = _load_subject_mri( + bg_img, stc, subject, subjects_dir, 'bg_img') if initial_time is None: time_sl = slice(0, None) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 4e1fcad9c7a..a30aeb2111e 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -605,13 +605,14 @@ def test_snapshot_brain_montage(renderer): @requires_dipy() @requires_nibabel() @requires_version('nilearn', '0.4') -@pytest.mark.parametrize('mode, stype, init_t, want_t, init_p, want_p', [ - ('glass_brain', 's', None, 2, None, (-30.9, 18.4, 56.7)), - ('stat_map', 'vec', 1, 1, None, (15.7, 16.0, -6.3)), - ('glass_brain', 'vec', None, 1, (10, -10, 20), (6.6, -9.0, 19.9)), - ('stat_map', 's', 1, 1, (-10, 5, 10), (-12.3, 2.0, 7.7))]) +@pytest.mark.parametrize( + 'mode, stype, init_t, want_t, init_p, want_p, bg_img', [ + ('glass_brain', 's', None, 2, None, (-30.9, 18.4, 56.7), None), + ('stat_map', 'vec', 1, 1, None, (15.7, 16.0, -6.3), None), + ('glass_brain', 'vec', None, 1, (10, -10, 20), (6.6, -9., 19.9), None), + ('stat_map', 's', 1, 1, (-10, 5, 10), (-12.3, 2.0, 7.7), 'brain.mgz')]) def test_plot_volume_source_estimates(mode, stype, init_t, want_t, - init_p, want_p): + init_p, want_p, bg_img): """Test interactive plotting of volume source estimates.""" forward = read_forward_solution(fwd_fname) sample_src = forward['src'] @@ -634,7 +635,7 @@ def test_plot_volume_source_estimates(mode, stype, init_t, want_t, fig = stc.plot( sample_src, subject='sample', subjects_dir=subjects_dir, mode=mode, initial_time=init_t, initial_pos=init_p, - verbose=True) + bg_img=bg_img, verbose=True) log = log.getvalue() want_str = 't = %0.3f s' % want_t assert want_str in log, (want_str, init_t) @@ -644,6 +645,10 @@ def test_plot_volume_source_estimates(mode, stype, init_t, want_t, _fake_click(fig, fig.axes[ax_idx], (0.3, 0.5)) fig.canvas.key_press_event('left') fig.canvas.key_press_event('shift+right') + if bg_img is not None: + with pytest.raises(FileNotFoundError, match='MRI file .* not found'): + stc.plot(sample_src, subject='sample', subjects_dir=subjects_dir, + mode='stat_map', bg_img='junk.mgz') @pytest.mark.slowtest # can be slow on OSX diff --git a/tutorials/source-modeling/plot_beamformer_lcmv.py b/tutorials/source-modeling/plot_beamformer_lcmv.py index 6b3ad76578f..6ce86b0208b 100644 --- a/tutorials/source-modeling/plot_beamformer_lcmv.py +++ b/tutorials/source-modeling/plot_beamformer_lcmv.py @@ -9,6 +9,7 @@ .. contents:: Page contents :local: :depth: 2 + """ # Author: Britta Westner #