Repository: mne-tools/mne-python

Issue:
implement draw_sensors
implement draw_sensors

https://github.com/mne-tools/mne-matlab/blob/master/matlab/mne_load_coil_def.m#L160

in plot_trans function.



Discussion:
We have to be careful because that was translated from BrainStorm code, which I think is GPL'ed. So we'll have to ask permission to relicense, right @agramfort?

hum... @ftadel what do you say?

I don't think we can even trace back who wrote that function...
I think you can just re-use whatever you need to :)

ok cool.

any volunteer?

@agramfort Is anybody working on this? I would like to volunteer for this in case nobody's working on it.

go for it.

#2599 or #2598 might be a little easier though

This one might be easy too: https://github.com/mne-tools/mne-python/issues/2770


Resolving patch:
diff --git a/examples/forward/plot_coregistration_transform.py b/examples/forward/plot_coregistration_transform.py
--- a/examples/forward/plot_coregistration_transform.py
+++ b/examples/forward/plot_coregistration_transform.py
@@ -20,8 +20,6 @@
 print(__doc__)
 
 
-data_path = sample.data_path()
-
 data_path = sample.data_path()
 subjects_dir = data_path + '/subjects'
 evoked_fname = data_path + '/MEG/sample/sample_audvis-ave.fif'
@@ -30,4 +28,4 @@
 condition = 'Left Auditory'
 evoked = read_evokeds(evoked_fname, condition=condition, baseline=(-0.2, 0.0))
 plot_trans(evoked.info, trans_fname, subject='sample', dig=True,
-           subjects_dir=subjects_dir)
+           meg_sensors=True, subjects_dir=subjects_dir)
diff --git a/examples/visualization/plot_meg_sensors.py b/examples/visualization/plot_meg_sensors.py
new file mode 100644
--- /dev/null
+++ b/examples/visualization/plot_meg_sensors.py
@@ -0,0 +1,41 @@
+"""
+======================================
+Plotting sensor layouts of MEG systems
+======================================
+
+In this example, sensor layouts of different MEG systems
+are shown.
+"""
+# Author: Eric Larson <larson.eric.d@gmail.com>
+#
+# License: BSD (3-clause)
+
+import os.path as op
+
+from mayavi import mlab
+
+import mne
+from mne.io import Raw, read_raw_ctf, read_raw_bti, read_raw_kit
+from mne.datasets import sample, spm_face
+from mne.viz import plot_trans
+
+print(__doc__)
+
+bti_path = op.abspath(op.dirname(mne.__file__)) + '/io/bti/tests/data/'
+kit_path = op.abspath(op.dirname(mne.__file__)) + '/io/kit/tests/data/'
+raws = dict(
+    Neuromag=Raw(sample.data_path() + '/MEG/sample/sample_audvis_raw.fif'),
+    CTF_275=read_raw_ctf(spm_face.data_path() +
+                         '/MEG/spm/SPM_CTF_MEG_example_faces1_3D.ds'),
+    Magnes_3600wh=read_raw_bti(bti_path + 'test_pdf_linux',
+                               bti_path + 'test_config_linux',
+                               bti_path + 'test_hs_linux'),
+    KIT=read_raw_kit(kit_path + 'test.sqd'),
+)
+
+for system, raw in raws.items():
+    # We don't have coil definitions for KIT refs, so exclude them
+    ref_meg = False if system == 'KIT' else True
+    fig = plot_trans(raw.info, trans=None, dig=False, eeg_sensors=False,
+                     meg_sensors=True, coord_frame='meg', ref_meg=ref_meg)
+    mlab.title(system)
diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py
--- a/mne/viz/_3d.py
+++ b/mne/viz/_3d.py
@@ -20,13 +20,14 @@
 from scipy import linalg
 
 from ..externals.six import string_types, advance_iterator
+from ..io import _loc_to_coil_trans, Info
 from ..io.pick import pick_types
 from ..io.constants import FIFF
 from ..surface import (get_head_surf, get_meg_helmet_surf, read_surface,
                        transform_surface_to)
 from ..transforms import (read_trans, _find_trans, apply_trans,
                           combine_transforms, _get_trans, _ensure_trans,
-                          invert_transform)
+                          invert_transform, Transform)
 from ..utils import get_subjects_dir, logger, _check_subject, verbose, warn
 from ..fixes import _get_args
 from ..defaults import _handle_default
@@ -268,16 +269,18 @@ def _plot_mri_contours(mri_fname, surf_fnames, orientation='coronal',
 @verbose
 def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
                ch_type=None, source=('bem', 'head'), coord_frame='head',
-               meg_sensors=False, dig=False, verbose=None):
+               meg_sensors=False, eeg_sensors=True, dig=False, ref_meg=False,
+               verbose=None):
     """Plot MEG/EEG head surface and helmet in 3D.
 
     Parameters
     ----------
     info : dict
         The measurement info.
-    trans : str | 'auto' | dict
+    trans : str | 'auto' | dict | None
         The full path to the head<->MRI transform ``*-trans.fif`` file
-        produced during coregistration.
+        produced during coregistration. If trans is None, no head
+        surface will be shown.
     subject : str | None
         The subject name corresponding to FreeSurfer environment
         variable SUBJECT.
@@ -297,8 +300,12 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
         Coordinate frame to use, 'head', 'meg', or 'mri'.
     meg_sensors : bool
         If True, plot MEG sensors as points in addition to showing the helmet.
+    eeg_sensors : bool
+        If True, plot EEG sensors as points.
     dig : bool
         If True, plot the digitization points.
+    ref_meg : bool
+        If True (default False), include reference MEG sensors.
     verbose : bool, str, int, or None
         If not None, override default verbose level (see mne.verbose).
 
@@ -307,26 +314,39 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
     fig : instance of mlab.Figure
         The mayavi figure.
     """
+    from ..forward import _create_meg_coils
+    if not isinstance(info, Info):
+        raise TypeError('info must be an instance of Info, got %s'
+                        % type(info))
     if coord_frame not in ['head', 'meg', 'mri']:
         raise ValueError('coord_frame must be "head" or "meg"')
     if ch_type not in [None, 'eeg', 'meg']:
         raise ValueError('Argument ch_type must be None | eeg | meg. Got %s.'
                          % ch_type)
 
+    show_head = True
     if isinstance(trans, string_types):
         if trans == 'auto':
             # let's try to do this in MRI coordinates so they're easy to plot
+            subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
             trans = _find_trans(subject, subjects_dir)
         trans = read_trans(trans)
+    elif trans is None:
+        trans = Transform('head', 'mri', np.eye(4))
+        show_head = False
     elif not isinstance(trans, dict):
-        raise TypeError('trans must be str or dict')
+        raise TypeError('trans must be str, dict, or None')
     head_mri_t = _ensure_trans(trans, 'head', 'mri')
     del trans
 
     # both the head and helmet will be in MRI coordinates after this
-    surfs = [get_head_surf(subject, source=source, subjects_dir=subjects_dir)]
+    surfs = dict()
+    if show_head:
+        subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
+        surfs['head'] = get_head_surf(subject, source=source,
+                                      subjects_dir=subjects_dir)
     if ch_type is None or ch_type == 'meg':
-        surfs.append(get_meg_helmet_surf(info, head_mri_t))
+        surfs['helmet'] = get_meg_helmet_surf(info, head_mri_t)
     if coord_frame == 'meg':
         surf_trans = combine_transforms(info['dev_head_t'], head_mri_t,
                                         'meg', 'mri')
@@ -334,15 +354,16 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
         surf_trans = head_mri_t
     else:  # coord_frame == 'mri'
         surf_trans = None
-    surfs = [transform_surface_to(surf, coord_frame, surf_trans)
-             for surf in surfs]
+    for key in surfs.keys():
+        surfs[key] = transform_surface_to(surfs[key], coord_frame, surf_trans)
     del surf_trans
 
     # determine points
-    meg_loc = list()
+    meg_rrs, meg_tris = list(), list()
     ext_loc = list()
     car_loc = list()
-    if ch_type is None or ch_type == 'eeg':
+    eeg_loc = list()
+    if eeg_sensors and (ch_type is None or ch_type == 'eeg'):
         eeg_loc = np.array([info['chs'][k]['loc'][:3]
                            for k in pick_types(info, meg=False, eeg=True)])
         if len(eeg_loc) > 0:
@@ -360,17 +381,30 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
                 warn('EEG electrode locations not found. Cannot plot EEG '
                      'electrodes.')
     if meg_sensors:
-        meg_loc = np.array([info['chs'][k]['loc'][:3]
-                           for k in pick_types(info)])
-        if len(meg_loc) > 0:
-            # Transform MEG coordinates from meg if necessary
-            if coord_frame == 'head':
-                meg_loc = apply_trans(info['dev_head_t'], meg_loc)
-            elif coord_frame == 'mri':
-                t = combine_transforms(info['dev_head_t'], head_mri_t,
+        meg_picks = pick_types(info, meg=True, ref_meg=ref_meg)
+        coil_transs = [_loc_to_coil_trans(info['chs'][pick]['loc'])
+                       for pick in meg_picks]
+        # Transform MEG coordinates from meg if necessary
+        trans = None
+        if coord_frame == 'head':
+            trans = info['dev_head_t']
+        elif coord_frame == 'mri':
+            trans = combine_transforms(info['dev_head_t'], head_mri_t,
                                        'meg', 'mri')
-                meg_loc = apply_trans(t, meg_loc)
-        else:
+        coils = _create_meg_coils([info['chs'][pick] for pick in meg_picks],
+                                  acc='normal')
+        offset = 0
+        for coil, coil_trans in zip(coils, coil_transs):
+            rrs, tris = _sensor_shape(coil)
+            rrs = apply_trans(coil_trans, rrs)
+            if trans is not None:
+                rrs = apply_trans(trans, rrs)
+            meg_rrs.append(rrs)
+            meg_tris.append(tris + offset)
+            offset += len(meg_rrs[-1])
+        meg_rrs = np.concatenate(meg_rrs, axis=0)
+        meg_tris = np.concatenate(meg_tris, axis=0)
+        if len(meg_rrs) == 0:
             warn('MEG electrodes not found. Cannot plot MEG locations.')
     if dig:
         ext_loc = np.array([d['r'] for d in info['dig']
@@ -391,9 +425,9 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
     from mayavi import mlab
     fig = mlab.figure(bgcolor=(0.0, 0.0, 0.0), size=(600, 600))
 
-    alphas = [1.0, 0.5]  # head, helmet
-    colors = [(0.6, 0.6, 0.6), (0.0, 0.0, 0.6)]
-    for surf, alpha, color in zip(surfs, alphas, colors):
+    alphas = dict(head=1.0, helmet=0.5)
+    colors = dict(head=(0.6, 0.6, 0.6), helmet=(0.0, 0.0, 0.6))
+    for key, surf in surfs.items():
         x, y, z = surf['rr'].T
         nn = surf['nn']
         # make absolutely sure these are normalized for Mayavi
@@ -404,21 +438,86 @@ def plot_trans(info, trans='auto', subject=None, subjects_dir=None,
             mesh = mlab.pipeline.triangular_mesh_source(x, y, z, surf['tris'])
         mesh.data.point_data.normals = nn
         mesh.data.cell_data.normals = None
-        mlab.pipeline.surface(mesh, color=color, opacity=alpha)
+        mlab.pipeline.surface(mesh, color=colors[key], opacity=alphas[key])
 
-    datas = (eeg_loc, meg_loc, car_loc, ext_loc)
-    colors = ((1., 0., 0.), (0., 0.25, 0.5), (1., 1., 0.), (1., 0.5, 0.))
-    alphas = (1.0, 0.25, 0.5, 0.25)
-    scales = (0.005, 0.0025, 0.015, 0.0075)
+    datas = (eeg_loc, car_loc, ext_loc)
+    colors = ((1., 0., 0.), (1., 1., 0.), (1., 0.5, 0.))
+    alphas = (1.0, 0.5, 0.25)
+    scales = (0.005, 0.015, 0.0075)
     for data, color, alpha, scale in zip(datas, colors, alphas, scales):
         if len(data) > 0:
             with warnings.catch_warnings(record=True):  # traits
                 mlab.points3d(data[:, 0], data[:, 1], data[:, 2],
                               color=color, scale_factor=scale, opacity=alpha)
+    if len(meg_rrs) > 0:
+        color, alpha = (0., 0.25, 0.5), 0.25
+        mlab.triangular_mesh(meg_rrs[:, 0], meg_rrs[:, 1], meg_rrs[:, 2],
+                             meg_tris, color=color, opacity=alpha)
     mlab.view(90, 90)
     return fig
 
 
+def _make_tris_fan(n_vert):
+    """Helper to make tris given a number of vertices of a circle-like obj"""
+    tris = np.zeros((n_vert - 2, 3), int)
+    tris[:, 2] = np.arange(2, n_vert)
+    tris[:, 1] = tris[:, 2] - 1
+    return tris
+
+
+def _sensor_shape(coil):
+    """Get the sensor shape vertices"""
+    rrs = np.empty([0, 2])
+    tris = np.empty([0, 3], int)
+    id_ = coil['type'] & 0xFFFF
+    if id_ in (2, 3012, 3013, 3011):
+        # square figure eight
+        # wound by right hand rule such that +x side is "up" (+z)
+        long_side = coil['size']  # length of long side (meters)
+        offset = 0.0025  # offset of the center portion of planar grad coil
+        rrs = np.array([
+            [offset, -long_side / 2.],
+            [long_side / 2., -long_side / 2.],
+            [long_side / 2., long_side / 2.],
+            [offset, long_side / 2.],
+            [-offset, -long_side / 2.],
+            [-long_side / 2., -long_side / 2.],
+            [-long_side / 2., long_side / 2.],
+            [-offset, long_side / 2.]])
+        tris = np.concatenate((_make_tris_fan(4),
+                               _make_tris_fan(4) + 4), axis=0)
+    elif id_ in (2000, 3022, 3023, 3024):
+        # square magnetometer (potentially point-type)
+        size = 0.001 if id_ == 2000 else (coil['size'] / 2.)
+        rrs = np.array([[-1., 1.], [1., 1.], [1., -1.], [-1., -1.]]) * size
+        tris = _make_tris_fan(4)
+    elif id_ in (4001, 4003, 5002, 7002, 7003):
+        # round magnetometer
+        n_pts = 15  # number of points for circle
+        circle = np.exp(2j * np.pi * np.arange(n_pts) / float(n_pts))
+        circle = np.concatenate(([0.], circle))
+        circle *= coil['size'] / 2.  # radius of coil
+        rrs = np.array([circle.real, circle.imag]).T
+        tris = _make_tris_fan(n_pts + 1)
+    elif id_ in (4002, 5001, 5003, 5004, 4004, 4005, 6001, 7001):
+        # round coil 1st order (off-diagonal) gradiometer
+        baseline = coil['base'] if id_ in (5004, 4005) else 0.
+        n_pts = 16  # number of points for circle
+        # This time, go all the way around circle to close it fully
+        circle = np.exp(2j * np.pi * np.arange(-1, n_pts) / float(n_pts - 1))
+        circle[0] = 0  # center pt for triangulation
+        circle *= coil['size'] / 2.
+        rrs = np.array([  # first, second coil
+            np.concatenate([circle.real + baseline / 2.,
+                            circle.real - baseline / 2.]),
+            np.concatenate([circle.imag, -circle.imag])]).T
+        tris = np.concatenate([_make_tris_fan(n_pts + 1),
+                               _make_tris_fan(n_pts + 1) + n_pts + 1])
+    # Go from (x,y) -> (x,y,z)
+    rrs = np.pad(rrs, ((0, 0), (0, 1)), mode='constant')
+    return rrs, tris
+
+
 def _limits_to_control_points(clim, stc_data, colormap):
     """Private helper function to convert limits (values or percentiles)
     to control points.


Test patch:
diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py
--- a/mne/viz/tests/test_3d.py
+++ b/mne/viz/tests/test_3d.py
@@ -15,6 +15,7 @@
 
 from mne import (make_field_map, pick_channels_evoked, read_evokeds,
                  read_trans, read_dipole, SourceEstimate)
+from mne.io import read_raw_ctf, read_raw_bti, read_raw_kit
 from mne.viz import (plot_sparse_source_estimates, plot_source_estimates,
                      plot_trans)
 from mne.utils import requires_mayavi, requires_pysurfer, run_tests_if_main
@@ -33,10 +34,17 @@
 src_fname = op.join(data_dir, 'subjects', 'sample', 'bem',
                     'sample-oct-6-src.fif')
 dip_fname = op.join(data_dir, 'MEG', 'sample', 'sample_audvis_trunc_set1.dip')
+ctf_fname = op.join(data_dir, 'CTF', 'testdata_ctf.ds')
 
-base_dir = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data')
+io_dir = op.join(op.abspath(op.dirname(__file__)), '..', '..', 'io')
+base_dir = op.join(io_dir, 'tests', 'data')
 evoked_fname = op.join(base_dir, 'test-ave.fif')
 
+base_dir = op.join(io_dir, 'bti', 'tests', 'data')
+pdf_fname = op.join(base_dir, 'test_pdf_linux')
+config_fname = op.join(base_dir, 'test_config_linux')
+hs_fname = op.join(base_dir, 'test_hs_linux')
+sqd_fname = op.join(io_dir, 'kit', 'tests', 'data', 'test.sqd')
 warnings.simplefilter('always')  # enable b/c these tests throw warnings
 
 
@@ -96,15 +104,32 @@ def test_plot_evoked_field():
 @testing.requires_testing_data
 @requires_mayavi
 def test_plot_trans():
-    """Test plotting of -trans.fif files
+    """Test plotting of -trans.fif files and MEG sensor layouts
     """
-    evoked = read_evokeds(evoked_fname, condition='Left Auditory',
-                          baseline=(-0.2, 0.0))
-    plot_trans(evoked.info, trans_fname, subject='sample',
-               subjects_dir=subjects_dir)
-    assert_raises(ValueError, plot_trans, evoked.info, trans_fname,
+    with warnings.catch_warnings(record=True):  # 4D weight tables
+        bti = read_raw_bti(pdf_fname, config_fname, hs_fname, convert=True,
+                           preload=False).info
+    infos = dict(
+        Neuromag=read_evokeds(evoked_fname)[0].info,
+        CTF=read_raw_ctf(ctf_fname).info,
+        BTi=bti,
+        KIT=read_raw_kit(sqd_fname).info,
+    )
+    for system, info in infos.items():
+        ref_meg = False if system == 'KIT' else True
+        plot_trans(info, trans_fname, subject='sample', meg_sensors=True,
+                   subjects_dir=subjects_dir, ref_meg=ref_meg)
+    # KIT ref sensor coil def not defined
+    assert_raises(RuntimeError, plot_trans, infos['KIT'], None,
+                  meg_sensors=True, ref_meg=True)
+    info = infos['Neuromag']
+    assert_raises(ValueError, plot_trans, info, trans_fname,
                   subject='sample', subjects_dir=subjects_dir,
                   ch_type='bad-chtype')
+    assert_raises(TypeError, plot_trans, 'foo', trans_fname,
+                  subject='sample', subjects_dir=subjects_dir)
+    # no-head version
+    plot_trans(info, None, meg_sensors=True, dig=True, coord_frame='head')
 
 
 @testing.requires_testing_data