Skip to content

Commit e73dfe8

Browse files
larsonermassich
authored andcommitted
MRG, ENH: Make use of fiducials (mne-tools#6791)
Add fiducials to all standard templates Update coil files
1 parent 9bb4bf2 commit e73dfe8

File tree

18 files changed

+180
-156
lines changed

18 files changed

+180
-156
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ script:
175175
pip install -e .;
176176
python mne/tests/test_evoked.py;
177177
fi;
178-
- echo pytest -m "${CONDITION}" ${USE_DIRS}
179-
- pytest -m "${CONDITION}" ${USE_DIRS}
178+
- echo pytest -m "${CONDITION}" --cov=mne ${USE_DIRS}
179+
- pytest -m "${CONDITION}" --cov=mne ${USE_DIRS}
180180
- if [ "${DEPS}" == "nodata" ]; then
181181
make pep;
182182
fi;

azure-pipelines.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ jobs:
7676
displayName: 'Print config'
7777
- script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)"
7878
displayName: 'Get test data'
79-
- script: pytest -m "not ultraslowtest" mne
79+
- script: pytest -m "not ultraslowtest" --cov=mne mne
8080
displayName: 'Run tests'
8181
- script: codecov --root %BUILD_REPOSITORY_LOCALPATH% -t %CODECOV_TOKEN%
8282
displayName: 'Codecov'

doc/python_reference.rst

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ Datasets
190190
fetch_hcp_mmp_parcellation
191191
hf_sef.data_path
192192
kiloword.data_path
193+
limo.load_data
193194
misc.data_path
194195
mtrf.data_path
195196
multimodal.data_path

examples/visualization/plot_montage.py

+12-28
Original file line numberDiff line numberDiff line change
@@ -20,55 +20,39 @@
2020
from mne.datasets import fetch_fsaverage
2121
from mne.viz import set_3d_title, set_3d_view
2222

23-
import warnings
24-
25-
warnings.simplefilter("ignore")
2623

2724
###############################################################################
2825
# Check all montages against a sphere
29-
#
30-
31-
sphere = mne.make_sphere_model(r0=(0., 0., 0.), head_radius=0.085)
3226

3327
for current_montage in get_builtin_montages():
34-
3528
montage = mne.channels.make_standard_montage(current_montage)
29+
info = mne.create_info(
30+
ch_names=montage.ch_names, sfreq=100., ch_types='eeg', montage=montage)
31+
sphere = mne.make_sphere_model(r0='auto', head_radius='auto', info=info)
3632
fig = mne.viz.plot_alignment(
3733
# Plot options
38-
show_axes=True, dig=True, surfaces='head', bem=sphere,
39-
40-
# Create dummy info
41-
info=mne.create_info(
42-
ch_names=montage.ch_names,
43-
sfreq=1,
44-
ch_types='eeg',
45-
montage=montage,
46-
),
47-
)
34+
show_axes=True, dig='fiducials', surfaces='head',
35+
bem=sphere, info=info)
4836
set_3d_view(figure=fig, azimuth=135, elevation=80)
4937
set_3d_title(figure=fig, title=current_montage)
5038

5139

5240
###############################################################################
5341
# Check all montages against fsaverage
54-
#
5542

5643
subjects_dir = op.dirname(fetch_fsaverage())
5744

5845
for current_montage in get_builtin_montages():
5946
montage = mne.channels.make_standard_montage(current_montage)
47+
# Create dummy info
48+
info = mne.create_info(
49+
ch_names=montage.ch_names, sfreq=100., ch_types='eeg', montage=montage)
6050
fig = mne.viz.plot_alignment(
6151
# Plot options
62-
show_axes=True, dig=True, surfaces='head', trans=None,
63-
subject='fsaverage', subjects_dir=subjects_dir,
64-
65-
# Create dummy info
66-
info=mne.create_info(
67-
ch_names=montage.ch_names,
68-
sfreq=1,
69-
ch_types='eeg',
70-
montage=montage,
71-
),
52+
show_axes=True, dig='fiducials', surfaces='head', mri_fiducials=True,
53+
subject='fsaverage', subjects_dir=subjects_dir, info=info,
54+
coord_frame='mri',
55+
trans='fsaverage', # transform from head coords to fsaverage's MRI
7256
)
7357
set_3d_view(figure=fig, azimuth=135, elevation=80)
7458
set_3d_title(figure=fig, title=current_montage)

mne/_digitization/_utils.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#
88
# License: BSD (3-clause)
99

10+
from collections import OrderedDict
1011
import datetime
1112
import os.path as op
1213
import re
@@ -337,7 +338,9 @@ def _make_dig_points(nasion=None, lpa=None, rpa=None, hpi=None,
337338
'kind': FIFF.FIFFV_POINT_EXTRA,
338339
'coord_frame': coord_frame})
339340
if dig_ch_pos is not None:
340-
keys = sorted(dig_ch_pos.keys())
341+
keys = dig_ch_pos.keys()
342+
if not isinstance(dig_ch_pos, OrderedDict):
343+
keys = sorted(keys)
341344
try: # use the last 3 as int if possible (e.g., EEG001->1)
342345
idents = []
343346
for key in keys:

mne/channels/_standard_montage_utils.py

+58-36
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414

1515
MONTAGE_PATH = op.join(op.dirname(_CHANNELS_INIT_FILE), 'data', 'montages')
1616

17-
HEAD_SIZE_DEFAULT = 0.085 # in [m]
1817
_str = 'U100'
1918

2019

21-
def _egi_256():
20+
# In standard_1020, T9=LPA, T10=RPA, Nasion is the same as Iz with a
21+
# sign-flipped Y value
22+
23+
def _egi_256(head_size):
2224
fname = op.join(MONTAGE_PATH, 'EGI_256.csd')
2325
# Label, Theta, Phi, Radius, X, Y, Z, off sphere surface
2426
options = dict(comments='//',
@@ -27,45 +29,66 @@ def _egi_256():
2729
pos = np.stack([xs, ys, zs], axis=-1)
2830

2931
# Fix pos to match Montage code
30-
# pos -= np.mean(pos, axis=0)
31-
pos = HEAD_SIZE_DEFAULT * (pos / np.linalg.norm(pos, axis=1).mean())
32+
pos *= head_size / np.median(np.linalg.norm(pos, axis=1))
33+
34+
# For this cap, the Nasion is the frontmost electrode,
35+
# LPA/RPA we approximate by putting 75% of the way (toward the front)
36+
# between the two electrodes that are halfway down the ear holes
37+
nasion = pos[ch_names.index('E31')]
38+
lpa = (0.75 * pos[ch_names.index('E67')] +
39+
0.25 * pos[ch_names.index('E94')])
40+
rpa = (0.75 * pos[ch_names.index('E219')] +
41+
0.25 * pos[ch_names.index('E190')])
3242

3343
return make_dig_montage(
3444
ch_pos=OrderedDict(zip(ch_names, pos)),
35-
coord_frame='head',
45+
coord_frame='unknown', nasion=nasion, lpa=lpa, rpa=rpa,
3646
)
3747

3848

39-
def _easycap(basename):
49+
def _easycap(basename, head_size):
4050
fname = op.join(MONTAGE_PATH, basename)
4151
options = dict(skip_header=1, dtype=(_str, 'i4', 'i4'))
4252
ch_names, theta, phi = _safe_np_loadtxt(fname, **options)
4353

44-
radii = np.full_like(phi, 1) # XXX: HEAD_SIZE_DEFAULT should work
54+
radii = np.full(len(phi), head_size)
4555
pos = _sph_to_cart(np.stack(
4656
[radii, np.deg2rad(phi), np.deg2rad(theta)],
4757
axis=-1,
4858
))
49-
50-
# scale up to realistic head radius (8.5cm == 85mm):
51-
pos *= HEAD_SIZE_DEFAULT # XXXX: this should be done through radii
59+
nasion = np.concatenate([[0], pos[ch_names.index('Fpz'), 1:]])
60+
nasion *= head_size / np.linalg.norm(nasion)
61+
lpa = np.mean([pos[ch_names.index('FT9')],
62+
pos[ch_names.index('TP9')]], axis=0)
63+
lpa *= head_size / np.linalg.norm(lpa) # on sphere
64+
rpa = np.mean([pos[ch_names.index('FT10')],
65+
pos[ch_names.index('TP10')]], axis=0)
66+
rpa *= head_size / np.linalg.norm(rpa)
5267

5368
return make_dig_montage(
5469
ch_pos=OrderedDict(zip(ch_names, pos)),
55-
coord_frame='head',
70+
coord_frame='unknown', nasion=nasion, lpa=lpa, rpa=rpa,
5671
)
5772

5873

59-
def _hydrocel(basename, fid_names=('FidNz', 'FidT9', 'FidT10')):
74+
def _hydrocel(basename, head_size):
75+
fid_names = ('FidNz', 'FidT9', 'FidT10')
6076
fname = op.join(MONTAGE_PATH, basename)
6177
options = dict(dtype=(_str, 'f4', 'f4', 'f4'))
6278
ch_names, xs, ys, zs = _safe_np_loadtxt(fname, **options)
6379

64-
pos = np.stack([xs, ys, zs], axis=-1) * 0.01
80+
pos = np.stack([xs, ys, zs], axis=-1)
6581
ch_pos = OrderedDict(zip(ch_names, pos))
66-
_ = [ch_pos.pop(n, None) for n in fid_names]
82+
nasion, lpa, rpa = [ch_pos.pop(n) for n in fid_names]
83+
scale = head_size / np.median(np.linalg.norm(pos, axis=-1))
84+
for value in ch_pos.values():
85+
value *= scale
86+
nasion *= scale
87+
lpa *= scale
88+
rpa *= scale
6789

68-
return make_dig_montage(ch_pos=ch_pos, coord_frame='head')
90+
return make_dig_montage(ch_pos=ch_pos, coord_frame='unknown',
91+
nasion=nasion, rpa=rpa, lpa=lpa)
6992

7093

7194
def _str_names(ch_names):
@@ -79,39 +102,32 @@ def _safe_np_loadtxt(fname, **kwargs):
79102
return (ch_names,) + others
80103

81104

82-
def _biosemi(basename, fid_names=('Nz', 'LPA', 'RPA')):
105+
def _biosemi(basename, head_size):
106+
fid_names = ('Nz', 'LPA', 'RPA')
83107
fname = op.join(MONTAGE_PATH, basename)
84108
options = dict(skip_header=1, dtype=(_str, 'i4', 'i4'))
85109
ch_names, theta, phi = _safe_np_loadtxt(fname, **options)
86110

87-
radii = np.full_like(phi, 1) # XXX: HEAD_SIZE_DEFAULT should work
111+
radii = np.full(len(phi), head_size)
88112
pos = _sph_to_cart(np.stack(
89113
[radii, np.deg2rad(phi), np.deg2rad(theta)],
90114
axis=-1,
91115
))
92116

93-
# scale up to realistic head radius (8.5cm == 85mm):
94-
pos *= HEAD_SIZE_DEFAULT # XXXX: this should be done through radii
95-
96117
ch_pos = OrderedDict(zip(ch_names, pos))
97-
_ = [ch_pos.pop(n, None) for n in fid_names]
118+
nasion, lpa, rpa = [ch_pos.pop(n) for n in fid_names]
98119

99-
return make_dig_montage(ch_pos=ch_pos, coord_frame='head')
120+
return make_dig_montage(ch_pos=ch_pos, coord_frame='unknown',
121+
nasion=nasion, lpa=lpa, rpa=rpa)
100122

101123

102-
def _mgh_or_standard(basename, fid_names=('Nz', 'LPA', 'RPA')):
124+
def _mgh_or_standard(basename, head_size):
125+
fid_names = ('Nz', 'LPA', 'RPA')
103126
fname = op.join(MONTAGE_PATH, basename)
104127

105128
ch_names_, pos = [], []
106129
with open(fname) as fid:
107-
# Default units are meters
108-
for line in fid:
109-
if 'UnitPosition' in line:
110-
units = line.split()[1]
111-
scale_factor = dict(m=1., mm=1e-3)[units]
112-
break
113-
else:
114-
raise RuntimeError('Could not detect units in file %s' % fname)
130+
# Ignore units as we will scale later using the norms anyway
115131
for line in fid:
116132
if 'Positions\n' in line:
117133
break
@@ -125,12 +141,18 @@ def _mgh_or_standard(basename, fid_names=('Nz', 'LPA', 'RPA')):
125141
break
126142
ch_names_.append(line.strip(' ').strip('\n'))
127143

128-
pos = np.array(pos) * scale_factor
129-
144+
pos = np.array(pos)
130145
ch_pos = OrderedDict(zip(ch_names_, pos))
131-
_ = [ch_pos.pop(n, None) for n in fid_names]
132-
133-
return make_dig_montage(ch_pos=ch_pos, coord_frame='head')
146+
nasion, lpa, rpa = [ch_pos.pop(n) for n in fid_names]
147+
scale = head_size / np.median(np.linalg.norm(pos, axis=1))
148+
for value in ch_pos.values():
149+
value *= scale
150+
nasion *= scale
151+
lpa *= scale
152+
rpa *= scale
153+
154+
return make_dig_montage(ch_pos=ch_pos, coord_frame='unknown',
155+
nasion=nasion, lpa=lpa, rpa=rpa)
134156

135157

136158
standard_montage_look_up_table = {

mne/channels/montage.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
from .channels import DEPRECATED_PARAM
5252

53+
HEAD_SIZE_DEFAULT = 0.095 # in [m]
5354

5455
_BUILT_IN_MONTAGES = [
5556
'EGI_256',
@@ -840,6 +841,7 @@ def dig_ch_pos(self):
840841

841842
def _get_ch_pos(self):
842843
pos = [d['r'] for d in _get_dig_eeg(self.dig)]
844+
assert len(self.ch_names) == len(pos)
843845
return dict(zip(self.ch_names, pos))
844846

845847
def _get_dig_names(self):
@@ -1718,7 +1720,7 @@ def compute_dev_head_t(montage):
17181720
return Transform(fro='meg', to='head', trans=trans)
17191721

17201722

1721-
def make_standard_montage(kind):
1723+
def make_standard_montage(kind, head_size=HEAD_SIZE_DEFAULT):
17221724
"""Read a generic (built-in) montage.
17231725
17241726
Individualized (digitized) electrode positions should be read in using
@@ -1729,6 +1731,8 @@ def make_standard_montage(kind):
17291731
kind : str
17301732
The name of the montage file without the file extension (e.g.
17311733
kind='easycap-M10' for 'easycap-M10.txt'). See notes for valid kinds.
1734+
head_size : float
1735+
The head size (in meters) to use for spherical montages.
17321736
17331737
Returns
17341738
-------
@@ -1795,5 +1799,4 @@ def make_standard_montage(kind):
17951799
raise ValueError('Could not find the montage %s. Please provide one '
17961800
'among: %s' % (kind,
17971801
standard_montage_look_up_table.keys()))
1798-
else:
1799-
return standard_montage_look_up_table[kind]()
1802+
return standard_montage_look_up_table[kind](head_size=head_size)

mne/channels/tests/test_montage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ def test_set_montage():
10341034
assert ((orig_pos != new_pos).all())
10351035

10361036
r0 = _fit_sphere(new_pos)[1]
1037-
assert_allclose(r0, [0., -0.016, 0.], atol=1e-3)
1037+
assert_allclose(r0, [0.000775, 0.006881, 0.047398], atol=1e-3)
10381038

10391039

10401040
# XXX: this does not check ch_names + it cannot work because of write_dig

0 commit comments

Comments
 (0)