Repository: mne-tools/mne-python

Issue:
Orientation in plot_bem
#### Describe the bug
Two things. The first one is more of a feature request: It might be nice to have an optional mri arguments to mne.viz.plot_bem to explicitly pass the location of the MRI file to use in mne.viz.plot_bem.

Second, in the current code of MNE, the orientation argument of mne.viz.plot_bem only produce correct visualization when the MRI volumes are in the "conform" space (e.g, running  mri_convert with --conform). E.g., here is the result with orientation == coronal for a volume that is not in the "conform" space:

![image](https://user-images.githubusercontent.com/5932954/72537087-7a122900-3849-11ea-988f-9316d57d6dac.png)

To the extent that the volumes contain the metadata for the orientation, the plot_bem function should check the orientation metadata and either 1) throw an exception if using non-conform volumes would cause to many issues or 2) give an appropriate view of the volume if managing internally non-conform volumes is reasonable for MNE. I don't think the volume should be made "conform" by MNE, unless MNE also manage rotating all the other artifacts (e.g., BEM surfaces) so that they stay in registration with the volume.

Question: Are there other functions that would be impacted by non "conform" volume?



Discussion:
@larsoner As discussed on the mailing list.
> To the extent that the volumes contain the metadata for the orientation, the plot_bem function should check the orientation metadata and either 1) throw an exception if using non-conform volumes would cause to many issues or 2) give an appropriate view of the volume if managing internally non-conform volumes is reasonable for MNE.

The easiest (well, simplest?) fix is to use [nib.io_orientation](https://nipy.org/nibabel/reference/nibabel.orientations.html#nibabel.orientations.io_orientation) or [nib.aff2axcodes](https://nipy.org/nibabel/reference/nibabel.orientations.html#nibabel.orientations.aff2axcodes) to "just" modify our MRI slicing and create the appropriate reordering/negation affine and apply it to the surface `rr` values. @christian-oreilly interested in trying?

Another option would be to use:

https://nipy.org/nibabel/reference/nibabel.funcs.html#nibabel.funcs.as_closest_canonical

to get an updated image, then use linear algebra between the original affine and the closest-canonical one (it's going to be a dot product between one and the inverse of the other, which should yield a matrix with three non-zero entries all either +1 or -1) to figure out the transformation to apply to the surfaces for display. But this seems like more work for something that would end up being equivalent.

> Are there other functions that would be impacted by non "conform" volume?

Other than `plot_bem`, I would look at `plot_alignment` of your data. This is designed to use the same transformation paths as many other things in MNE (e.g., BEM calculations, forward solution, etc.).

At least `plot_volume_source_estimates` already makes use of `aff2axcodes` to figure out data orientation for clicking and so forth.

Once you go to visualize with PySurfer, it probably will assume your MRI is oriented the standard way and so, e.g., when you ask for a lateral view you will not get it. I think I worked with some data in non-RAS orientation once and had that problem. In theory this could be fixable / should be fixed in PySurfer the same way as above.
@jasmainak since you have worked a bit on the 3D plotting maybe you'd be interested in tackling this? I think the `io_orientation` or `aff2axcodes` would be the way to go for simplicity and speed.
i'm sorry with the late response. How urgent is this? The original request seems straightforward to fix but the rest might need work. I am a bit swamped at the moment due to an upcoming grant deadline, but I can file it in my todos and would be happy to give it a stab after that.

Resolving patch:
diff --git a/mne/source_space.py b/mne/source_space.py
--- a/mne/source_space.py
+++ b/mne/source_space.py
@@ -1544,11 +1544,11 @@ def _check_mri(mri, subject, subjects_dir):
     if not op.isfile(mri):
         if subject is None:
             raise FileNotFoundError(
-                'mri file %r not found and no subject provided' % (mri,))
+                'MRI file %r not found and no subject provided' % (mri,))
         subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
         mri = op.join(subjects_dir, subject, 'mri', mri)
         if not op.isfile(mri):
-            raise FileNotFoundError('mri file %r not found' % (mri,))
+            raise FileNotFoundError('MRI file %r not found' % (mri,))
     return mri
 
 
diff --git a/mne/viz/misc.py b/mne/viz/misc.py
--- a/mne/viz/misc.py
+++ b/mne/viz/misc.py
@@ -28,9 +28,10 @@
 from ..io.proj import make_projector
 from ..io.pick import (_DATA_CH_TYPES_SPLIT, pick_types, pick_info,
                        pick_channels)
-from ..source_space import read_source_spaces, SourceSpaces, _read_mri_info
+from ..source_space import (read_source_spaces, SourceSpaces, _read_mri_info,
+                            _check_mri)
 from ..transforms import invert_transform, apply_trans
-from ..utils import (logger, verbose, get_subjects_dir, warn, _check_option,
+from ..utils import (logger, verbose, warn, _check_option,
                      _mask_to_onsets_offsets, _pl)
 from ..io.pick import _picks_by_type
 from ..filter import estimate_ringing_samples
@@ -300,36 +301,28 @@ def _plot_mri_contours(mri_fname, surfaces, src, orientation='coronal',
     # For ease of plotting, we will do everything in voxel coordinates.
     _check_option('orientation', orientation, ('coronal', 'axial', 'sagittal'))
 
-    # plot axes (x, y, z) as data axes (0, 1, 2)
-    if orientation == 'coronal':
-        x, y, z = 0, 1, 2
-    elif orientation == 'axial':
-        x, y, z = 2, 0, 1
-    else:  # orientation == 'sagittal'
-        x, y, z = 2, 1, 0
-
     # Load the T1 data
-    nim = nib.load(mri_fname)
-    _, vox_mri_t, _, _, _ = _read_mri_info(mri_fname, units='mm')
+    _, vox_mri_t, _, _, _, nim = _read_mri_info(
+        mri_fname, units='mm', return_img=True)
     mri_vox_t = invert_transform(vox_mri_t)['trans']
     del vox_mri_t
-    # We make some assumptions here about our data orientation. Someday we
-    # might want to resample to standard orientation instead:
-    #
-    # vox_ras_t = np.array(  # our standard orientation
-    #     [[-1., 0, 0, 128], [0, 0, 1, -128], [0, -1, 0, 128], [0, 0, 0, 1]])
-    # nim = resample_from_to(nim, ((256, 256, 256), vox_ras_t), order=0)
-    # mri_vox_t = np.dot(np.linalg.inv(vox_ras_t), mri_ras_t['trans'])
-    #
-    # But until someone complains about obnoxious data orientations,
-    # what we have already should work fine (and be faster because no
-    # resampling is done).
-    data = _get_img_fdata(nim)
-    n_sag, n_axi, n_cor = data.shape
-    orientation_name2axis = dict(sagittal=0, axial=1, coronal=2)
-    orientation_axis = orientation_name2axis[orientation]
 
-    n_slices = data.shape[orientation_axis]
+    # plot axes (x, y, z) as data axes
+    axcodes = ''.join(nib.orientations.aff2axcodes(nim.affine))
+    # we don't care about directonality reversal, so convert LPI to RAS. For
+    # conformed images, we get axcodes == 'RSA'
+    axcodes = axcodes.replace('L', 'R').replace('P', 'A').replace('I', 'S')
+    orientation_to_xyz = dict(
+        coronal=('R', 'S', 'A'),
+        axial=('A', 'R', 'S'),
+        sagittal=('A', 'S', 'R'),
+    )
+    x, y, z = [axcodes.index(c) for c in orientation_to_xyz[orientation]]
+    transpose = x < y
+    del orientation
+
+    data = _get_img_fdata(nim)
+    n_slices = data.shape[z]
     if slices is None:
         slices = np.round(
             np.linspace(0, n_slices, 12, endpoint=False)).astype(np.int)
@@ -347,7 +340,7 @@ def _plot_mri_contours(mri_fname, surfaces, src, orientation='coronal',
     for file_name, color in surfaces:
         surf = dict()
         surf['rr'], surf['tris'] = read_surface(file_name)
-        # move back surface to MRI coordinate system
+        # move surface to voxel coordinate system
         surf['rr'] = apply_trans(mri_vox_t, surf['rr'])
         surfs.append((surf, color))
 
@@ -364,14 +357,12 @@ def _plot_mri_contours(mri_fname, surfaces, src, orientation='coronal',
     fig.set_facecolor('k')
     bounds = np.concatenate(
         [[-np.inf], slices[:-1] + np.diff(slices) / 2., [np.inf]])  # float
+    slicer = [slice(None)] * 3
     for ax, sl, lower, upper in zip(axs, slices, bounds[:-1], bounds[1:]):
         # adjust the orientations for good view
-        if orientation == 'coronal':
-            dat = data[:, :, sl].transpose()
-        elif orientation == 'axial':
-            dat = data[:, sl, :]
-        elif orientation == 'sagittal':
-            dat = data[sl, :, :]
+        slicer[z] = sl
+        dat = data[tuple(slicer)]
+        dat = dat.T if transpose else dat
 
         # First plot the anatomical data
         ax.imshow(dat, cmap=plt.cm.gray)
@@ -403,7 +394,7 @@ def _plot_mri_contours(mri_fname, surfaces, src, orientation='coronal',
 
 def plot_bem(subject=None, subjects_dir=None, orientation='coronal',
              slices=None, brain_surfaces=None, src=None, show=True,
-             show_indices=True):
+             show_indices=True, mri='T1.mgz'):
     """Plot BEM contours on anatomical slices.
 
     Parameters
@@ -436,6 +427,11 @@ def plot_bem(subject=None, subjects_dir=None, orientation='coronal',
         Show slice indices if True.
 
         .. versionadded:: 0.20
+    mri : str
+        The name of the MRI to use. Can be a standard FreeSurfer MRI such as
+        ``'T1.mgz'``, or a full path to a custom MRI file.
+
+        .. versionadded:: 0.21
 
     Returns
     -------
@@ -460,12 +456,7 @@ def plot_bem(subject=None, subjects_dir=None, orientation='coronal',
     on top of the midpoint MRI slice with the BEM boundary drawn for that
     slice.
     """
-    subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
-
-    # Get the MRI filename
-    mri_fname = op.join(subjects_dir, subject, 'mri', 'T1.mgz')
-    if not op.isfile(mri_fname):
-        raise IOError('MRI file "%s" does not exist' % mri_fname)
+    mri_fname = _check_mri(mri, subject, subjects_dir)
 
     # Get the BEM surface filenames
     bem_path = op.join(subjects_dir, subject, 'bem')


Test patch:
diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py
--- a/mne/viz/tests/test_misc.py
+++ b/mne/viz/tests/test_misc.py
@@ -118,7 +118,7 @@ def test_plot_cov():
 @requires_nibabel()
 def test_plot_bem():
     """Test plotting of BEM contours."""
-    with pytest.raises(IOError, match='MRI file .* does not exist'):
+    with pytest.raises(IOError, match='MRI file .* not found'):
         plot_bem(subject='bad-subject', subjects_dir=subjects_dir)
     with pytest.raises(ValueError, match="Invalid value for the 'orientation"):
         plot_bem(subject='sample', subjects_dir=subjects_dir,