/
_3d.py
3356 lines (2998 loc) · 135 KB
/
_3d.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""Functions to make 3D plots with M/EEG data."""
# Authors: Alexandre Gramfort <alexandre.gramfort@inria.fr>
# Denis Engemann <denis.engemann@gmail.com>
# Martin Luessi <mluessi@nmr.mgh.harvard.edu>
# Eric Larson <larson.eric.d@gmail.com>
# Mainak Jas <mainak@neuro.hut.fi>
# Mark Wronkiewicz <wronk.mark@gmail.com>
#
# License: Simplified BSD
from itertools import cycle
import os.path as op
import warnings
from collections.abc import Iterable
from functools import partial
from dataclasses import dataclass
from typing import Optional
import numpy as np
from ..defaults import DEFAULTS
from ..fixes import _crop_colorbar, _get_img_fdata
from .._freesurfer import (_read_mri_info, _check_mri, _get_head_surface,
_get_skull_surface)
from ..io import _loc_to_coil_trans
from ..io.pick import (pick_types, channel_type, pick_info,
_FNIRS_CH_TYPES_SPLIT, _MEG_CH_TYPES_SPLIT)
from ..io.constants import FIFF
from ..io.meas_info import read_fiducials, create_info
from ..source_space import (_ensure_src, _create_surf_spacing, _check_spacing,
SourceSpaces, read_freesurfer_lut)
from ..surface import (get_meg_helmet_surf, _read_mri_surface, _DistanceQuery,
_project_onto_surface, _reorder_ccw, _CheckInside)
from ..transforms import (apply_trans, rot_to_quat, combine_transforms,
_get_trans, _ensure_trans, Transform, rotation,
read_ras_mni_t, _print_coord_trans, _find_trans,
transform_surface_to, _frame_to_str,
_get_transforms_to_coord_frame)
from ..utils import (get_subjects_dir, logger, _check_subject, verbose, warn,
has_nibabel, check_version, fill_doc, _pl, get_config,
_ensure_int, _validate_type, _check_option, _to_rgb)
from .utils import (mne_analyze_colormap, _get_color_list,
plt_show, tight_layout, figure_nobar, _check_time_unit)
from ..bem import ConductorModel, _bem_find_surface, _ensure_bem_surfaces
verbose_dec = verbose
FIDUCIAL_ORDER = (FIFF.FIFFV_POINT_LPA, FIFF.FIFFV_POINT_NASION,
FIFF.FIFFV_POINT_RPA)
# XXX: to unify with digitization
def _fiducial_coords(points, coord_frame=None):
"""Generate 3x3 array of fiducial coordinates."""
points = points or [] # None -> list
if coord_frame is not None:
points = [p for p in points if p['coord_frame'] == coord_frame]
points_ = {p['ident']: p for p in points if
p['kind'] == FIFF.FIFFV_POINT_CARDINAL}
if points_:
return np.array([points_[i]['r'] for i in FIDUCIAL_ORDER])
else:
# XXX eventually this should probably live in montage.py
if coord_frame is None or coord_frame == FIFF.FIFFV_COORD_HEAD:
# Try converting CTF HPI coils to fiducials
out = np.empty((3, 3))
out.fill(np.nan)
for p in points:
if p['kind'] == FIFF.FIFFV_POINT_HPI:
if np.isclose(p['r'][1:], 0, atol=1e-6).all():
out[0 if p['r'][0] < 0 else 2] = p['r']
elif np.isclose(p['r'][::2], 0, atol=1e-6).all():
out[1] = p['r']
if np.isfinite(out).all():
return out
return np.array([])
@fill_doc
def plot_head_positions(pos, mode='traces', cmap='viridis', direction='z',
show=True, destination=None, info=None, color='k',
axes=None):
"""Plot head positions.
Parameters
----------
pos : ndarray, shape (n_pos, 10) | list of ndarray
The head position data. Can also be a list to treat as a
concatenation of runs.
mode : str
Can be 'traces' (default) to show position and quaternion traces,
or 'field' to show the position as a vector field over time.
cmap : colormap
Colormap to use for the trace plot, default is "viridis".
direction : str
Can be any combination of "x", "y", or "z" (default: "z") to show
directional axes in "field" mode.
show : bool
Show figure if True. Defaults to True.
destination : str | array-like, shape (3,) | None
The destination location for the head, assumed to be in head
coordinates. See :func:`mne.preprocessing.maxwell_filter` for
details.
.. versionadded:: 0.16
%(info)s If provided, will be used to show the destination position when
``destination is None``, and for showing the MEG sensors.
.. versionadded:: 0.16
color : color object
The color to use for lines in ``mode == 'traces'`` and quiver
arrows in ``mode == 'field'``.
.. versionadded:: 0.16
axes : array-like, shape (3, 2)
The matplotlib axes to use. Only used for ``mode == 'traces'``.
.. versionadded:: 0.16
Returns
-------
fig : instance of matplotlib.figure.Figure
The figure.
"""
from ..chpi import head_pos_to_trans_rot_t
from ..preprocessing.maxwell import _check_destination
import matplotlib.pyplot as plt
_check_option('mode', mode, ['traces', 'field'])
dest_info = dict(dev_head_t=None) if info is None else info
destination = _check_destination(destination, dest_info, head_frame=True)
if destination is not None:
destination = _ensure_trans(destination, 'head', 'meg') # probably inv
destination = destination['trans'][:3].copy()
destination[:, 3] *= 1000
if not isinstance(pos, (list, tuple)):
pos = [pos]
for ii, p in enumerate(pos):
p = np.array(p, float)
if p.ndim != 2 or p.shape[1] != 10:
raise ValueError('pos (or each entry in pos if a list) must be '
'dimension (N, 10), got %s' % (p.shape,))
if ii > 0: # concatenation
p[:, 0] += pos[ii - 1][-1, 0] - p[0, 0]
pos[ii] = p
borders = np.cumsum([len(pp) for pp in pos])
pos = np.concatenate(pos, axis=0)
trans, rot, t = head_pos_to_trans_rot_t(pos) # also ensures pos is okay
# trans, rot, and t are for dev_head_t, but what we really want
# is head_dev_t (i.e., where the head origin is in device coords)
use_trans = np.einsum('ijk,ik->ij', rot[:, :3, :3].transpose([0, 2, 1]),
-trans) * 1000
use_rot = rot.transpose([0, 2, 1])
use_quats = -pos[:, 1:4] # inverse (like doing rot.T)
surf = rrs = lims = None
if info is not None:
meg_picks = pick_types(info, meg=True, ref_meg=False, exclude=())
if len(meg_picks) > 0:
rrs = 1000 * np.array([info['chs'][pick]['loc'][:3]
for pick in meg_picks], float)
if mode == 'traces':
lims = np.array((rrs.min(0), rrs.max(0))).T
else: # mode == 'field'
surf = get_meg_helmet_surf(info)
transform_surface_to(surf, 'meg', info['dev_head_t'],
copy=False)
surf['rr'] *= 1000.
helmet_color = DEFAULTS['coreg']['helmet_color']
if mode == 'traces':
if axes is None:
axes = plt.subplots(3, 2, sharex=True)[1]
else:
axes = np.array(axes)
if axes.shape != (3, 2):
raise ValueError('axes must have shape (3, 2), got %s'
% (axes.shape,))
fig = axes[0, 0].figure
labels = ['xyz', ('$q_1$', '$q_2$', '$q_3$')]
for ii, (quat, coord) in enumerate(zip(use_quats.T, use_trans.T)):
axes[ii, 0].plot(t, coord, color, lw=1., zorder=3)
axes[ii, 0].set(ylabel=labels[0][ii], xlim=t[[0, -1]])
axes[ii, 1].plot(t, quat, color, lw=1., zorder=3)
axes[ii, 1].set(ylabel=labels[1][ii], xlim=t[[0, -1]])
for b in borders[:-1]:
for jj in range(2):
axes[ii, jj].axvline(t[b], color='r')
for ii, title in enumerate(('Position (mm)', 'Rotation (quat)')):
axes[0, ii].set(title=title)
axes[-1, ii].set(xlabel='Time (s)')
if rrs is not None:
pos_bads = np.any([(use_trans[:, ii] <= lims[ii, 0]) |
(use_trans[:, ii] >= lims[ii, 1])
for ii in range(3)], axis=0)
for ii in range(3):
oidx = list(range(ii)) + list(range(ii + 1, 3))
# knowing it will generally be spherical, we can approximate
# how far away we are along the axis line by taking the
# point to the left and right with the smallest distance
from scipy.spatial.distance import cdist
dists = cdist(rrs[:, oidx], use_trans[:, oidx])
left = rrs[:, [ii]] < use_trans[:, ii]
left_dists_all = dists.copy()
left_dists_all[~left] = np.inf
# Don't show negative Z direction
if ii != 2 and np.isfinite(left_dists_all).any():
idx = np.argmin(left_dists_all, axis=0)
left_dists = rrs[idx, ii]
bads = ~np.isfinite(
left_dists_all[idx, np.arange(len(idx))]) | pos_bads
left_dists[bads] = np.nan
axes[ii, 0].plot(t, left_dists, color=helmet_color,
ls='-', lw=0.5, zorder=2)
else:
axes[ii, 0].axhline(lims[ii][0], color=helmet_color,
ls='-', lw=0.5, zorder=2)
right_dists_all = dists
right_dists_all[left] = np.inf
if np.isfinite(right_dists_all).any():
idx = np.argmin(right_dists_all, axis=0)
right_dists = rrs[idx, ii]
bads = ~np.isfinite(
right_dists_all[idx, np.arange(len(idx))]) | pos_bads
right_dists[bads] = np.nan
axes[ii, 0].plot(t, right_dists, color=helmet_color,
ls='-', lw=0.5, zorder=2)
else:
axes[ii, 0].axhline(lims[ii][1], color=helmet_color,
ls='-', lw=0.5, zorder=2)
for ii in range(3):
axes[ii, 1].set(ylim=[-1, 1])
if destination is not None:
vals = np.array([destination[:, 3],
rot_to_quat(destination[:, :3])]).T.ravel()
for ax, val in zip(fig.axes, vals):
ax.axhline(val, color='r', ls=':', zorder=2, lw=1.)
else: # mode == 'field':
from matplotlib.colors import Normalize
from mpl_toolkits.mplot3d.art3d import Line3DCollection
from mpl_toolkits.mplot3d import Axes3D # noqa: F401, analysis:ignore
fig, ax = plt.subplots(1, subplot_kw=dict(projection='3d'))
# First plot the trajectory as a colormap:
# http://matplotlib.org/examples/pylab_examples/multicolored_line.html
pts = use_trans[:, np.newaxis]
segments = np.concatenate([pts[:-1], pts[1:]], axis=1)
norm = Normalize(t[0], t[-2])
lc = Line3DCollection(segments, cmap=cmap, norm=norm)
lc.set_array(t[:-1])
ax.add_collection(lc)
# now plot the head directions as a quiver
dir_idx = dict(x=0, y=1, z=2)
kwargs = dict(pivot='tail')
for d, length in zip(direction, [5., 2.5, 1.]):
use_dir = use_rot[:, :, dir_idx[d]]
# draws stems, then heads
array = np.concatenate((t, np.repeat(t, 2)))
ax.quiver(use_trans[:, 0], use_trans[:, 1], use_trans[:, 2],
use_dir[:, 0], use_dir[:, 1], use_dir[:, 2], norm=norm,
cmap=cmap, array=array, length=length, **kwargs)
if destination is not None:
ax.quiver(destination[0, 3],
destination[1, 3],
destination[2, 3],
destination[dir_idx[d], 0],
destination[dir_idx[d], 1],
destination[dir_idx[d], 2], color=color,
length=length, **kwargs)
mins = use_trans.min(0)
maxs = use_trans.max(0)
if surf is not None:
ax.plot_trisurf(*surf['rr'].T, triangles=surf['tris'],
color=helmet_color, alpha=0.1, shade=False)
ax.scatter(*rrs.T, s=1, color=helmet_color)
mins = np.minimum(mins, rrs.min(0))
maxs = np.maximum(maxs, rrs.max(0))
scale = (maxs - mins).max() / 2.
xlim, ylim, zlim = (maxs + mins)[:, np.newaxis] / 2. + [-scale, scale]
ax.set(xlabel='x', ylabel='y', zlabel='z',
xlim=xlim, ylim=ylim, zlim=zlim)
_set_aspect_equal(ax)
ax.view_init(30, 45)
tight_layout(fig=fig)
plt_show(show)
return fig
def _set_aspect_equal(ax):
# XXX recent MPL throws an error for 3D axis aspect setting, not much
# we can do about it at this point
try:
ax.set_aspect('equal')
except NotImplementedError:
pass
@verbose
def plot_evoked_field(evoked, surf_maps, time=None, time_label='t = %0.0f ms',
n_jobs=1, fig=None, vmax=None, n_contours=21,
verbose=None):
"""Plot MEG/EEG fields on head surface and helmet in 3D.
Parameters
----------
evoked : instance of mne.Evoked
The evoked object.
surf_maps : list
The surface mapping information obtained with make_field_map.
time : float | None
The time point at which the field map shall be displayed. If None,
the average peak latency (across sensor types) is used.
time_label : str | None
How to print info about the time instant visualized.
%(n_jobs)s
fig : instance of Figure3D | None
If None (default), a new figure will be created, otherwise it will
plot into the given figure.
.. versionadded:: 0.20
vmax : float | None
Maximum intensity. Can be None to use the max(abs(data)).
.. versionadded:: 0.21
n_contours : int
The number of contours.
.. versionadded:: 0.21
%(verbose)s
Returns
-------
fig : instance of Figure3D
The figure.
"""
# Update the backend
from .backends.renderer import _get_renderer
types = [t for t in ['eeg', 'grad', 'mag'] if t in evoked]
_validate_type(vmax, (None, 'numeric'), 'vmax')
n_contours = _ensure_int(n_contours, 'n_contours')
time_idx = None
if time is None:
time = np.mean([evoked.get_peak(ch_type=t)[1] for t in types])
del types
if not evoked.times[0] <= time <= evoked.times[-1]:
raise ValueError('`time` (%0.3f) must be inside `evoked.times`' % time)
time_idx = np.argmin(np.abs(evoked.times - time))
# Plot them
alphas = [1.0, 0.5]
colors = [(0.6, 0.6, 0.6), (1.0, 1.0, 1.0)]
colormap = mne_analyze_colormap(format='vtk')
colormap_lines = np.concatenate([np.tile([0., 0., 255., 255.], (127, 1)),
np.tile([0., 0., 0., 255.], (2, 1)),
np.tile([255., 0., 0., 255.], (127, 1))])
renderer = _get_renderer(fig, bgcolor=(0.0, 0.0, 0.0), size=(600, 600))
for ii, this_map in enumerate(surf_maps):
surf = this_map['surf']
map_data = this_map['data']
map_type = this_map['kind']
map_ch_names = this_map['ch_names']
if map_type == 'eeg':
pick = pick_types(evoked.info, meg=False, eeg=True)
else:
pick = pick_types(evoked.info, meg=True, eeg=False, ref_meg=False)
ch_names = [evoked.ch_names[k] for k in pick]
set_ch_names = set(ch_names)
set_map_ch_names = set(map_ch_names)
if set_ch_names != set_map_ch_names:
message = ['Channels in map and data do not match.']
diff = set_map_ch_names - set_ch_names
if len(diff):
message += ['%s not in data file. ' % list(diff)]
diff = set_ch_names - set_map_ch_names
if len(diff):
message += ['%s not in map file.' % list(diff)]
raise RuntimeError(' '.join(message))
data = np.dot(map_data, evoked.data[pick, time_idx])
# Make a solid surface
if vmax is None:
vmax = np.max(np.abs(data))
vmax = float(vmax)
alpha = alphas[ii]
renderer.surface(surface=surf, color=colors[ii],
opacity=alpha)
# Now show our field pattern
renderer.surface(surface=surf, vmin=-vmax, vmax=vmax,
scalars=data, colormap=colormap,
polygon_offset=-1)
# And the field lines on top
renderer.contour(surface=surf, scalars=data, contours=n_contours,
vmin=-vmax, vmax=vmax, opacity=alpha,
colormap=colormap_lines)
if time_label is not None:
if '%' in time_label:
time_label %= (1e3 * evoked.times[time_idx])
renderer.text2d(x_window=0.01, y_window=0.01, text=time_label)
renderer.set_camera(azimuth=10, elevation=60)
renderer.show()
return renderer.scene()
@verbose
def plot_alignment(info=None, trans=None, subject=None, subjects_dir=None,
surfaces='auto', coord_frame='auto',
meg=None, eeg='original', fwd=None,
dig=False, ecog=True, src=None, mri_fiducials=False,
bem=None, seeg=True, fnirs=True, show_axes=False, dbs=True,
fig=None, interaction='terrain', verbose=None):
"""Plot head, sensor, and source space alignment in 3D.
Parameters
----------
%(info)s If None (default), no sensor information will be shown.
%(trans)s
%(subject)s Can be omitted if ``src`` is provided.
%(subjects_dir)s
surfaces : str | list | dict
Surfaces to plot. Supported values:
* scalp: one of 'head', 'outer_skin' (alias for 'head'),
'head-dense', or 'seghead' (alias for 'head-dense')
* skull: 'outer_skull', 'inner_skull', 'brain' (alias for
'inner_skull')
* brain: one of 'pial', 'white', 'inflated', or 'brain'
(alias for 'pial').
Can be dict to specify alpha values for each surface. Use None
to specify default value. Specified values must be between 0 and 1.
for example::
surfaces=dict(brain=0.4, outer_skull=0.6, head=None)
Defaults to 'auto', which will look for a head surface and plot
it if found.
.. note:: For single layer BEMs it is recommended to use ``'brain'``.
coord_frame : 'auto' | 'head' | 'meg' | 'mri'
The coordinate frame to use. If ``'auto'`` (default), chooses ``'mri'``
if ``trans`` was passed, and ``'head'`` otherwise.
.. versionchanged:: 1.0
Defaults to ``'auto'``.
%(meg)s
%(eeg)s
%(fwd)s
dig : bool | 'fiducials'
If True, plot the digitization points; 'fiducials' to plot fiducial
points only.
%(ecog)s
src : instance of SourceSpaces | None
If not None, also plot the source space points.
mri_fiducials : bool | str
Plot MRI fiducials (default False). If ``True``, look for a file with
the canonical name (``bem/{subject}-fiducials.fif``). If ``str``,
it can be ``'estimated'`` to use :func:`mne.coreg.get_mni_fiducials`,
otherwise it should provide the full path to the fiducials file.
.. versionadded:: 0.22
Support for ``'estimated'``.
bem : list of dict | instance of ConductorModel | None
Can be either the BEM surfaces (list of dict), a BEM solution or a
sphere model. If None, we first try loading
``'$SUBJECTS_DIR/$SUBJECT/bem/$SUBJECT-$SOURCE.fif'``, and then look
for ``'$SUBJECT*$SOURCE.fif'`` in the same directory. For
``'outer_skin'``, the subjects bem and bem/flash folders are searched.
Defaults to None.
%(seeg)s
%(fnirs)s
.. versionadded:: 0.20
show_axes : bool
If True (default False), coordinate frame axis indicators will be
shown:
* head in pink.
* MRI in gray (if ``trans is not None``).
* MEG in blue (if MEG sensors are present).
.. versionadded:: 0.16
%(dbs)s
fig : Figure3D | None
PyVista scene in which to plot the alignment.
If ``None``, creates a new 600x600 pixel figure with black background.
.. versionadded:: 0.16
%(interaction_scene)s
.. versionadded:: 0.16
.. versionchanged:: 1.0
Defaults to ``'terrain'``.
%(verbose)s
Returns
-------
fig : instance of Figure3D
The figure.
See Also
--------
mne.viz.plot_bem
Notes
-----
This function serves the purpose of checking the validity of the many
different steps of source reconstruction:
- Transform matrix (keywords ``trans``, ``meg`` and ``mri_fiducials``),
- BEM surfaces (keywords ``bem`` and ``surfaces``),
- sphere conductor model (keywords ``bem`` and ``surfaces``) and
- source space (keywords ``surfaces`` and ``src``).
.. versionadded:: 0.15
"""
# Update the backend
from .backends.renderer import _get_renderer
meg, eeg, fnirs, warn_meg = _handle_sensor_types(meg, eeg, fnirs)
_check_option('interaction', interaction, ['trackball', 'terrain'])
info = create_info(1, 1000., 'misc') if info is None else info
_validate_type(info, "info")
# Handle surfaces:
if surfaces == 'auto' and trans is None:
surfaces = list() # if no `trans` can't plot mri surfaces
if isinstance(surfaces, str):
surfaces = [surfaces]
if isinstance(surfaces, dict):
user_alpha = surfaces.copy()
for key, val in user_alpha.items():
_validate_type(key, "str", f"surfaces key {repr(key)}")
_validate_type(val, (None, "numeric"), f"surfaces[{repr(key)}]")
if val is not None:
user_alpha[key] = float(val)
if not 0 <= user_alpha[key] <= 1:
raise ValueError(
f'surfaces[{repr(key)}] ({val}) must be'
' between 0 and 1'
)
else:
user_alpha = {}
surfaces = list(surfaces)
for si, s in enumerate(surfaces):
_validate_type(s, "str", f"surfaces[{si}]")
bem = _ensure_bem_surfaces(bem, extra_allow=(ConductorModel, None))
assert isinstance(bem, ConductorModel) or bem is None
_check_option('coord_frame', coord_frame, ['head', 'meg', 'mri', 'auto'])
if coord_frame == 'auto':
coord_frame = 'head' if trans is None else 'mri'
if src is not None:
src = _ensure_src(src)
src_subject = src._subject
subject = src_subject if subject is None else subject
if src_subject is not None and subject != src_subject:
raise ValueError(f'subject ("{subject}") did not match the '
f'subject name in src ("{src_subject}")')
# configure transforms
if trans == 'auto':
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
trans = _find_trans(subject, subjects_dir)
picks = pick_types(info, meg=('sensors' in meg), ref_meg=('ref' in meg),
eeg=(len(eeg) > 0), ecog=ecog, seeg=seeg, dbs=dbs,
fnirs=(len(fnirs) > 0))
if trans is None:
# Some stuff is natively in head coords, others in MRI coords
msg = ('A head<->mri transformation matrix (trans) is required '
f'to plot %s in {coord_frame} coordinates, '
'`trans=None` is not allowed')
if fwd is not None:
fwd_frame = _frame_to_str[fwd['coord_frame']]
if fwd_frame != coord_frame:
raise ValueError(
msg % f'a {fwd_frame}-coordinate forward solution')
if src is not None:
src_frame = _frame_to_str[src[0]['coord_frame']]
if src_frame != coord_frame:
raise ValueError(
msg % f'a {src_frame}-coordinate source space')
if mri_fiducials is not False and coord_frame != 'mri':
raise ValueError(msg % 'mri fiducials')
# only enforce needing `trans` if there are channels in "head"/"device"
if picks.size and coord_frame == 'mri':
raise ValueError(msg % 'sensors')
# if only plotting sphere model no trans needed
if bem is not None:
if not bem['is_sphere']:
if coord_frame != 'mri':
raise ValueError(msg % 'a BEM')
elif surfaces not in (['brain'], []): # can only plot these
raise ValueError(msg % (', '.join(surfaces) + ' surfaces'))
elif len(surfaces) > 0 and coord_frame != 'mri':
raise ValueError(msg % (', '.join(surfaces) + ' surfaces'))
trans = Transform('head', 'mri') # not used so just use identity
# get transforms
head_mri_t = _get_trans(trans, 'head', 'mri')[0]
to_cf_t = _get_transforms_to_coord_frame(
info, head_mri_t, coord_frame=coord_frame)
# Surfaces:
# both the head and helmet will be in MRI coordinates after this
surfs = dict()
# Brain surface:
brain = sorted(
set(surfaces) & set(['brain', 'pial', 'white', 'inflated']))
if len(brain) > 1:
raise ValueError(
f'Only one brain surface can be plotted, got {brain}.')
brain = brain[0] if brain else False
if brain is not False:
surfaces.pop(surfaces.index(brain))
if bem is not None and bem['is_sphere'] and brain == 'brain':
surfs['lh'] = _bem_find_surface(bem, 'brain')
else:
brain = 'pial' if brain == 'brain' else brain
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
for hemi in ['lh', 'rh']:
brain_fname = op.join(subjects_dir, subject, 'surf',
f'{hemi}.{brain}')
if not op.isfile(brain_fname):
raise RuntimeError(
f'No brain surface found for subject {subject}, '
f'expected {brain_fname} to exist')
surfs[hemi] = _read_mri_surface(brain_fname)
# Head surface:
head_keys = ('auto', 'head', 'outer_skin', 'head-dense', 'seghead')
head = [s for s in surfaces if s in head_keys]
if len(head) > 1:
raise ValueError('Can only supply one head-like surface name, '
f'got {head}')
head = head[0] if head else False
if head is not False:
surfaces.pop(surfaces.index(head))
elif 'projected' in eeg:
raise ValueError('A head surface is required to project EEG, '
'"head", "outer_skin", "head-dense" or "seghead" '
'must be in surfaces or surfaces must be "auto"')
# Skull surface:
skulls = [s for s in surfaces if s in ('outer_skull', 'inner_skull')]
for skull_name in skulls:
surfaces.pop(surfaces.index(skull_name))
skull = _get_skull_surface(
skull_name.split('_')[0], subject, subjects_dir, bem=bem)
skull['name'] = skull_name # set name for alpha
surfs[skull_name] = skull
# we've looked through all of them, raise if some remain
if len(surfaces) > 0:
raise ValueError(f'Unknown surface type{_pl(surfaces)}: {surfaces}')
# set colors and alphas
defaults = DEFAULTS['coreg']
no_deep = not (dbs or seeg) or \
pick_types(info, dbs=True, seeg=True).size == 0
max_alpha = 1.0 if no_deep else 0.75
hemi_val = 0.5
if src is None or (brain and any(s['type'] == 'surf' for s in src)):
hemi_val = max_alpha
alpha_range = np.linspace(max_alpha / 2., 0, 5)[:len(skulls) + 1]
if src is None and brain is False and len(skulls) == 0 and not show_axes:
head_alpha = max_alpha
else:
head_alpha = alpha_range[0]
alphas = dict(lh=hemi_val, rh=hemi_val)
colors = dict(lh=(0.5,) * 3, rh=(0.5,) * 3)
for idx, name in enumerate(skulls):
alphas[name] = alpha_range[idx + 1]
colors[name] = (0.95 - idx * 0.2, 0.85, 0.95 - idx * 0.2)
if brain is not False and brain in user_alpha:
alphas['lh'] = alphas['rh'] = user_alpha.pop(brain)
# replace default alphas with specified user_alpha
for k, v in user_alpha.items():
if v is not None:
alphas[k] = v
if k in head_keys and v is not None:
head_alpha = v
fid_colors = tuple(
defaults[f'{key}_color'] for key in ('lpa', 'nasion', 'rpa'))
# initialize figure
renderer = _get_renderer(fig, name=f'Sensor alignment: {subject}',
bgcolor=(0.5, 0.5, 0.5), size=(800, 800))
renderer.set_interaction(interaction)
# plot head
_, _, head_surf = _plot_head_surface(
renderer, head, subject, subjects_dir, bem, coord_frame,
to_cf_t, alpha=head_alpha)
# plot helmet
if 'helmet' in meg and pick_types(info, meg=True).size > 0:
_, _, src_surf = _plot_helmet(
renderer, info, to_cf_t, head_mri_t, coord_frame)
# plot surfaces
if brain and 'lh' not in surfs: # one layer sphere
assert bem['coord_frame'] == FIFF.FIFFV_COORD_HEAD
center = bem['r0'].copy()
center = apply_trans(to_cf_t['head'], center)
renderer.sphere(center, scale=0.01, color=colors['lh'],
opacity=alphas['lh'])
if show_axes:
_plot_axes(renderer, info, to_cf_t, head_mri_t)
# plot points
_check_option('dig', dig, (True, False, 'fiducials'))
if dig:
if dig is True:
_plot_hpi_coils(renderer, info, to_cf_t)
_plot_head_shape_points(renderer, info, to_cf_t)
_plot_head_fiducials(renderer, info, to_cf_t, fid_colors)
if mri_fiducials:
_plot_mri_fiducials(renderer, mri_fiducials, subjects_dir, subject,
to_cf_t, fid_colors)
for key, surf in surfs.items():
# Surfs can sometimes be in head coords (e.g., if coming from sphere)
assert isinstance(surf, dict), f'{key}: {type(surf)}'
surf = transform_surface_to(
surf, coord_frame, [to_cf_t['mri'], to_cf_t['head']], copy=True)
renderer.surface(surface=surf, color=colors[key],
opacity=alphas[key],
backface_culling=(key != 'helmet'))
# plot sensors (NB snapshot_brain_montage relies on the last thing being
# plotted being the sensors, so we need to do this after the surfaces)
if picks.size > 0:
_plot_sensors(renderer, info, to_cf_t, picks, meg, eeg, fnirs,
warn_meg, head_surf, 'm')
if src is not None:
atlas_ids, colors = read_freesurfer_lut()
for ss in src:
src_rr = ss['rr'][ss['inuse'].astype(bool)]
src_nn = ss['nn'][ss['inuse'].astype(bool)]
# update coordinate frame
src_trans = to_cf_t[_frame_to_str[src[0]['coord_frame']]]
src_rr = apply_trans(src_trans, src_rr)
src_nn = apply_trans(src_trans, src_nn, move=False)
# volume sources
if ss['type'] == 'vol':
seg_name = ss.get('seg_name', None)
if seg_name is not None and seg_name in colors:
color = colors[seg_name][:3]
color = tuple(i / 256. for i in color)
else:
color = (1., 1., 0.)
# surface and discrete sources
else:
color = (1., 1., 0.)
if len(src_rr) > 0:
renderer.quiver3d(
x=src_rr[:, 0], y=src_rr[:, 1], z=src_rr[:, 2],
u=src_nn[:, 0], v=src_nn[:, 1], w=src_nn[:, 2],
color=color, mode='cylinder', scale=3e-3,
opacity=0.75, glyph_height=0.25,
glyph_center=(0., 0., 0.), glyph_resolution=20,
backface_culling=True)
if fwd is not None:
_plot_forward(renderer, fwd,
to_cf_t[_frame_to_str[fwd['coord_frame']]])
renderer.set_camera(azimuth=90, elevation=90,
distance=0.6, focalpoint=(0., 0., 0.))
renderer.show()
return renderer.scene()
def _handle_sensor_types(meg, eeg, fnirs):
"""Handle plotting inputs for sensors types."""
if eeg is True:
eeg = ['original']
elif eeg is False:
eeg = list()
warn_meg = meg is not None # only warn if the value is explicit
if meg is True:
meg = ['helmet', 'sensors', 'ref']
elif meg is None:
meg = ['helmet', 'sensors']
elif meg is False:
meg = list()
if fnirs is True:
fnirs = ['pairs']
elif fnirs is False:
fnirs = list()
if isinstance(meg, str):
meg = [meg]
if isinstance(eeg, str):
eeg = [eeg]
if isinstance(fnirs, str):
fnirs = [fnirs]
for kind, var in zip(('eeg', 'meg', 'fnirs'), (eeg, meg, fnirs)):
if not isinstance(var, (list, tuple)) or \
not all(isinstance(x, str) for x in var):
raise TypeError(
f'{kind} must be list or tuple of str, got {type(kind)}')
for xi, x in enumerate(meg):
_check_option(f'meg[{xi}]', x, ('helmet', 'sensors', 'ref'))
for xi, x in enumerate(eeg):
_check_option(f'eeg[{xi}]', x, ('original', 'projected'))
for xi, x in enumerate(fnirs):
_check_option(f'fnirs[{xi}]', x, ('channels', 'pairs',
'sources', 'detectors'))
return meg, eeg, fnirs, warn_meg
@verbose
def _ch_pos_in_coord_frame(info, to_cf_t, warn_meg=True, verbose=None):
"""Transform positions from head/device/mri to a coordinate frame."""
from ..forward import _create_meg_coils
chs = dict(ch_pos=dict(), sources=dict(), detectors=dict())
unknown_chs = list() # prepare for chs with unknown coordinate frame
type_counts = dict()
for idx in range(info['nchan']):
ch_type = channel_type(info, idx)
if ch_type in type_counts:
type_counts[ch_type] += 1
else:
type_counts[ch_type] = 1
type_slices = dict(ch_pos=slice(0, 3))
if ch_type in _FNIRS_CH_TYPES_SPLIT:
# add sensors and detectors too for fNIRS
type_slices.update(sources=slice(3, 6), detectors=slice(6, 9))
for type_name, type_slice in type_slices.items():
if ch_type in _MEG_CH_TYPES_SPLIT + ('ref_meg',):
coil_trans = _loc_to_coil_trans(info['chs'][idx]['loc'])
# Here we prefer accurate geometry in case we need to
# ConvexHull the coil, we want true 3D geometry (and not, for
# example, a straight line / 1D geometry)
this_coil = [info['chs'][idx]]
try:
coil = _create_meg_coils(this_coil, acc='accurate')[0]
except RuntimeError: # we don't have an accurate one
coil = _create_meg_coils(this_coil, acc='normal')[0]
# store verts as ch_coord
ch_coord, triangles = _sensor_shape(coil)
ch_coord = apply_trans(coil_trans, ch_coord)
if len(ch_coord) == 0 and warn_meg:
warn(f'MEG sensor {info.ch_names[idx]} not found.')
else:
ch_coord = info['chs'][idx]['loc'][type_slice]
ch_coord_frame = info['chs'][idx]['coord_frame']
if ch_coord_frame not in (FIFF.FIFFV_COORD_UNKNOWN,
FIFF.FIFFV_COORD_DEVICE,
FIFF.FIFFV_COORD_HEAD,
FIFF.FIFFV_COORD_MRI):
raise RuntimeError(
f'Channel {info.ch_names[idx]} has coordinate frame '
f'{ch_coord_frame}, must be "meg", "head" or "mri".')
# set unknown as head first
if ch_coord_frame == FIFF.FIFFV_COORD_UNKNOWN:
unknown_chs.append(info.ch_names[idx])
ch_coord_frame = FIFF.FIFFV_COORD_HEAD
ch_coord = apply_trans(
to_cf_t[_frame_to_str[ch_coord_frame]], ch_coord)
if ch_type in _MEG_CH_TYPES_SPLIT + ('ref_meg',):
chs[type_name][info.ch_names[idx]] = (ch_coord, triangles)
else:
chs[type_name][info.ch_names[idx]] = ch_coord
if unknown_chs:
warn(f'Got coordinate frame "unknown" for {unknown_chs}, assuming '
'"head" coordinates.')
logger.info('Channel types::\t' + ', '.join(
[f'{ch_type}: {count}' for ch_type, count in type_counts.items()]))
return chs['ch_pos'], chs['sources'], chs['detectors']
def _plot_head_surface(renderer, head, subject, subjects_dir, bem,
coord_frame, to_cf_t, alpha, color=None):
"""Render a head surface in a 3D scene."""
color = DEFAULTS['coreg']['head_color'] if color is None else color
actor = None
src_surf = dst_surf = None
if head is not False:
src_surf = _get_head_surface(head, subject, subjects_dir, bem=bem)
src_surf = transform_surface_to(
src_surf, coord_frame, [to_cf_t['mri'], to_cf_t['head']],
copy=True)
actor, dst_surf = renderer.surface(
surface=src_surf, color=color, opacity=alpha,
backface_culling=False)
return actor, dst_surf, src_surf
def _plot_helmet(renderer, info, to_cf_t, head_mri_t, coord_frame,
alpha=0.25, color=None):
color = DEFAULTS['coreg']['helmet_color'] if color is None else color
src_surf = get_meg_helmet_surf(info, head_mri_t)
assert src_surf['coord_frame'] == FIFF.FIFFV_COORD_MRI
src_surf = transform_surface_to(
src_surf, coord_frame, [to_cf_t['mri'], to_cf_t['head']], copy=True)
actor, dst_surf = renderer.surface(
surface=src_surf, color=color, opacity=alpha,
backface_culling=False)
return actor, dst_surf, src_surf
def _plot_axes(renderer, info, to_cf_t, head_mri_t):
"""Render different axes a 3D scene."""
axes = [(to_cf_t['head'], (0.9, 0.3, 0.3))] # always show head
if not np.allclose(head_mri_t['trans'], np.eye(4)): # Show MRI
axes.append((to_cf_t['mri'], (0.6, 0.6, 0.6)))
if pick_types(info, meg=True).size > 0: # Show MEG
axes.append((to_cf_t['meg'], (0., 0.6, 0.6)))
actors = list()
for ax in axes:
x, y, z = np.tile(ax[0]['trans'][:3, 3], 3).reshape((3, 3)).T
u, v, w = ax[0]['trans'][:3, :3]
actor, _ = renderer.sphere(center=np.column_stack((x[0], y[0], z[0])),
color=ax[1], scale=3e-3)
actors.append(actor)
actor, _ = renderer.quiver3d(x=x, y=y, z=z, u=u, v=v, w=w,
mode='arrow', scale=2e-2, color=ax[1],
scale_mode='scalar', resolution=20,
scalars=[0.33, 0.66, 1.0])
actors.append(actor)
return actors
def _plot_head_fiducials(renderer, info, to_cf_t, fid_colors):
defaults = DEFAULTS['coreg']
car_loc = _fiducial_coords(info['dig'], FIFF.FIFFV_COORD_HEAD)
car_loc = apply_trans(to_cf_t['head'], car_loc)
if len(car_loc) == 0:
warn('Digitization points not found. Cannot plot digitization.')
actors = list()
for color, data in zip(fid_colors, car_loc):
actor, _ = renderer.sphere(center=data, color=color,
scale=defaults['dig_fid_scale'],
opacity=defaults['dig_fid_opacity'],
backface_culling=True)
actors.append(actor)
return actors
def _plot_mri_fiducials(renderer, mri_fiducials, subjects_dir, subject,
to_cf_t, fid_colors):
from ..coreg import get_mni_fiducials
defaults = DEFAULTS['coreg']
if mri_fiducials is True:
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
if subject is None:
raise ValueError("Subject needs to be specified to "
"automatically find the fiducials file.")
mri_fiducials = op.join(subjects_dir, subject, 'bem',
subject + '-fiducials.fif')
if isinstance(mri_fiducials, str):
if mri_fiducials == 'estimated':
mri_fiducials = get_mni_fiducials(subject, subjects_dir)
else:
mri_fiducials, cf = read_fiducials(mri_fiducials)
if cf != FIFF.FIFFV_COORD_MRI:
raise ValueError("Fiducials are not in MRI space")
if isinstance(mri_fiducials, np.ndarray):
fid_loc = mri_fiducials
else:
fid_loc = _fiducial_coords(mri_fiducials, FIFF.FIFFV_COORD_MRI)
fid_loc = apply_trans(to_cf_t['mri'], fid_loc)
transform = np.eye(4)
transform[:3, :3] = to_cf_t['mri']['trans'][:3, :3] * \
defaults['mri_fid_scale']
# rotate around Z axis 45 deg first
transform = transform @ rotation(0, 0, np.pi / 4)
actors = list()
for color, data in zip(fid_colors, fid_loc):
actor, _ = renderer.quiver3d(
x=data[0], y=data[1], z=data[2],