Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MNT: Change DigMontage representation to use Digitization #6639

Merged
merged 41 commits into from
Aug 22, 2019
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f3ddcaf
WIP: make transformations private [run CIs]
massich Aug 7, 2019
b524af6
cosmit and notes
massich Aug 8, 2019
1d8888c
WIP: move transforms
massich Aug 8, 2019
2f118b1
WIP: move the transformations logic into free helper functions (#28)
massich Aug 8, 2019
af87f24
WIP: Add dig representation
massich Aug 9, 2019
d20d1e3
WIP: Remove transforms from __init__
massich Aug 9, 2019
45f5a6e
FIX dictionary unpacking
massich Aug 19, 2019
23a3a18
fix rebase
massich Aug 19, 2019
052193c
WIP: Keep dig and chnames only
massich Aug 19, 2019
8901f13
fix 052193cc9
massich Aug 19, 2019
2f28847
use deprecated property
agramfort Aug 19, 2019
03aace4
Merge branch 'change_dig_montage_representation' of https://github.co…
agramfort Aug 19, 2019
f8110c2
wip: make point_names private + property
massich Aug 20, 2019
f0f16ce
wip: remove deprecation warnings
massich Aug 20, 2019
041de69
wip: add ch_names attribute
massich Aug 20, 2019
2ec3d37
wip: do not use point_names in __repr__
massich Aug 20, 2019
9beef17
wip: remove _get_dig()
massich Aug 20, 2019
694587e
wip with alex!!!!
massich Aug 20, 2019
f40bbfe
wip: fix wip
massich Aug 20, 2019
f28b26f
fix pep
massich Aug 20, 2019
d2cd65a
xxxx:
massich Aug 20, 2019
61aeb41
wip: deprecate compute_dev_head_t
massich Aug 20, 2019
54851f2
XXX: update XXX comments
massich Aug 20, 2019
f2d0325
wip: add meaningful deprecation message
massich Aug 20, 2019
e534413
wip: use new DigMontage constructor
massich Aug 21, 2019
78f11cc
WIP-TST: deprecated DigMontage contruction
massich Aug 21, 2019
175e4cb
TST: use atol for dev_head_t testing
massich Aug 21, 2019
136bf89
WIP: ADD make_dig_montage (and make it great again)
massich Aug 21, 2019
351214e
wip
massich Aug 21, 2019
69f3c34
fix tests
agramfort Aug 21, 2019
feb4d12
fix test?
agramfort Aug 21, 2019
eec13fb
don't copy
agramfort Aug 21, 2019
deaa80a
cleanup
agramfort Aug 21, 2019
fd09584
more fixes
agramfort Aug 21, 2019
c0aa751
fix
massich Aug 21, 2019
6fc46e8
fix fieldtrip
massich Aug 21, 2019
56e6884
FIX: Link
larsoner Aug 21, 2019
278d813
update whatsnew and trigger CIs
massich Aug 22, 2019
723deb7
use master whatsnew
massich Aug 22, 2019
668ac05
update the new whatsnew
massich Aug 22, 2019
edf7571
Merge branch 'master' into change_dig_montage_representation
massich Aug 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/python_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ Projections:
read_montage
get_builtin_montages
read_dig_montage
make_dig_montage
read_layout
find_layout
make_eeg_layout
Expand Down
7 changes: 3 additions & 4 deletions examples/visualization/plot_3d_to_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,8 @@
mat = loadmat(path_data)
ch_names = mat['ch_names'].tolist()
elec = mat['elec'] # electrode coordinates in meters
dig_ch_pos = dict(zip(ch_names, elec))
mon = mne.channels.DigMontage(dig_ch_pos=dig_ch_pos)
info = mne.create_info(ch_names, 1000., 'ecog', montage=mon)
montage = mne.channels.make_dig_montage(ch_pos=dict(zip(ch_names, elec)))
info = mne.create_info(ch_names, 1000., 'ecog', montage=montage)
print('Created %s channel positions' % len(ch_names))

###############################################################################
Expand All @@ -67,7 +66,7 @@
fig = plot_alignment(info, subject='sample', subjects_dir=subjects_dir,
surfaces=['pial'], meg=False)
set_3d_view(figure=fig, azimuth=200, elevation=70)
xy, im = snapshot_brain_montage(fig, mon)
xy, im = snapshot_brain_montage(fig, montage)

# Convert from a dictionary to array to plot
xy_pts = np.vstack([xy[ch] for ch in info['ch_names']])
Expand Down
2 changes: 1 addition & 1 deletion mne/channels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .layout import (Layout, make_eeg_layout, make_grid_layout, read_layout,
find_layout, generate_2d_layout)
from .montage import (read_montage, read_dig_montage, Montage, DigMontage,
get_builtin_montages)
get_builtin_montages, make_dig_montage)
from .channels import (equalize_channels, rename_channels, fix_mag_coil_types,
read_ch_connectivity, _get_ch_type,
find_ch_connectivity, make_1020_channel_selections)
306 changes: 306 additions & 0 deletions mne/channels/_dig_montage_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# Authors: Alexandre Gramfort <[email protected]>
# Denis Engemann <[email protected]>
# Martin Luessi <[email protected]>
# Eric Larson <[email protected]>
# Marijn van Vliet <[email protected]>
# Jona Sassenhagen <[email protected]>
# Teon Brooks <[email protected]>
# Christian Brodbeck <[email protected]>
# Stefan Appelhoff <[email protected]>
# Joan Massich <[email protected]>
#
# License: Simplified BSD

import xml.etree.ElementTree as ElementTree

import numpy as np

from ..transforms import apply_trans, get_ras_to_neuromag_trans

from ..io.constants import FIFF
from ..io.open import fiff_open
from ..digitization._utils import _read_dig_fif
from ..utils import _check_fname, Bunch, warn


def _fix_data_fiducials(data):
nasion, rpa, lpa = data.nasion, data.rpa, data.lpa
if any(x is None for x in (nasion, rpa, lpa)):
if data.elp is None or data.point_names is None:
raise ValueError('ELP points and names must be specified for '
'transformation.')
names = [name.lower() for name in data.point_names]

# check that all needed points are present
kinds = ('nasion', 'lpa', 'rpa')
missing = [name for name in kinds if name not in names]
if len(missing) > 0:
raise ValueError('The points %s are missing, but are needed '
'to transform the points to the MNE '
'coordinate system. Either add the points, '
'or read the montage with transform=False.'
% str(missing))

data.nasion, data.lpa, data.rpa = [
data.elp[names.index(kind)] for kind in kinds
]

# remove fiducials from elp
mask = np.ones(len(names), dtype=bool)
for fid in ['nasion', 'lpa', 'rpa']:
mask[names.index(fid)] = False
data.elp = data.elp[mask]
data.point_names = [p for pi, p in enumerate(data.point_names)
massich marked this conversation as resolved.
Show resolved Hide resolved
if mask[pi]]
return data


def _transform_to_head_call(data):
"""Transform digitizer points to Neuromag head coordinates.

Parameters
----------
data : Bunch.
replicates DigMontage old structure. Requires the following fields:
['nasion', 'lpa', 'rpa', 'hsp', 'hpi', 'elp', 'coord_frame',
'dig_ch_pos']

Returns
-------
data : Bunch.
transformed version of input data.
"""
if data.coord_frame == 'head': # nothing to do
return data
nasion, rpa, lpa = data.nasion, data.rpa, data.lpa

native_head_t = get_ras_to_neuromag_trans(nasion, lpa, rpa)
data.nasion, data.lpa, data.rpa = apply_trans(
native_head_t, np.array([nasion, lpa, rpa]))
if data.elp is not None:
data.elp = apply_trans(native_head_t, data.elp)
if data.hsp is not None:
data.hsp = apply_trans(native_head_t, data.hsp)
if data.dig_ch_pos is not None:
for key, val in data.dig_ch_pos.items():
data.dig_ch_pos[key] = apply_trans(native_head_t, val)
data.coord_frame = 'head'

return data


_cardinal_ident_mapping = {
FIFF.FIFFV_POINT_NASION: 'nasion',
FIFF.FIFFV_POINT_LPA: 'lpa',
FIFF.FIFFV_POINT_RPA: 'rpa',
}


def _read_dig_montage_fif(
fname,
_raise_transform_err,
_all_data_kwargs_are_none,
):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to

def _read_dig_fif(fid, meas_info):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change how we write functions here? Everywhere else in MNE we do:

def func(a, b, c):

We also never use underscore vars for args/kwargs. For codebase consistency please fix these

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. But this is only here for a really short period of time. This function were just for convenience, and to keep track of all internal things for the refactoring.

I can change them to not _foo. But they day in the next PR.

from .montage import _check_frame # circular dep

if _raise_transform_err:
raise ValueError('transform must be True and dev_head_t must be '
'False for FIF dig montage')
if not _all_data_kwargs_are_none:
raise ValueError('hsp, hpi, elp, point_names, egi must all be '
'None if fif is not None')

_check_fname(fname, overwrite='read', must_exist=True)
# Load the dig data
f, tree = fiff_open(fname)[:2]
with f as fid:
dig = _read_dig_fif(fid, tree)
massich marked this conversation as resolved.
Show resolved Hide resolved

# Split up the dig points by category
hsp = list()
hpi = list()
elp = list()
point_names = list()
fids = dict()
dig_ch_pos = dict()
for d in dig:
if d['kind'] == FIFF.FIFFV_POINT_CARDINAL:
_check_frame(d, 'head')
fids[_cardinal_ident_mapping[d['ident']]] = d['r']
elif d['kind'] == FIFF.FIFFV_POINT_HPI:
_check_frame(d, 'head')
hpi.append(d['r'])
elp.append(d['r'])
point_names.append('HPI%03d' % d['ident'])
elif d['kind'] == FIFF.FIFFV_POINT_EXTRA:
_check_frame(d, 'head')
hsp.append(d['r'])
elif d['kind'] == FIFF.FIFFV_POINT_EEG:
_check_frame(d, 'head')
dig_ch_pos['EEG%03d' % d['ident']] = d['r']

return Bunch(
nasion=fids['nasion'], lpa=fids['lpa'], rpa=fids['rpa'],
hsp=np.array(hsp) if len(hsp) else None,
hpi=hpi, # XXX: why this is returned as empty list instead of None?
elp=np.array(elp) if len(elp) else None,
point_names=point_names,
dig_ch_pos=dig_ch_pos,
coord_frame='head',
)


def _read_dig_montage_egi(
fname,
_scaling,
_all_data_kwargs_are_none,
):

if not _all_data_kwargs_are_none:
raise ValueError('hsp, hpi, elp, point_names, fif must all be '
'None if egi is not None')
_check_fname(fname, overwrite='read', must_exist=True)

root = ElementTree.parse(fname).getroot()
ns = root.tag[root.tag.index('{'):root.tag.index('}') + 1]
sensors = root.find('%ssensorLayout/%ssensors' % (ns, ns))
fids = dict()
dig_ch_pos = dict()

fid_name_map = {'Nasion': 'nasion',
'Right periauricular point': 'rpa',
'Left periauricular point': 'lpa'}

for s in sensors:
name, number, kind = s[0].text, int(s[1].text), int(s[2].text)
coordinates = np.array([float(s[3].text), float(s[4].text),
float(s[5].text)])

coordinates *= _scaling

# EEG Channels
if kind == 0:
dig_ch_pos['EEG %03d' % number] = coordinates
# Reference
elif kind == 1:
dig_ch_pos['EEG %03d' %
(len(dig_ch_pos.keys()) + 1)] = coordinates
# XXX: we should do something with this (ref and eeg get mixed)

# Fiducials
elif kind == 2:
fid_name = fid_name_map[name]
fids[fid_name] = coordinates
# Unknown
else:
warn('Unknown sensor type %s detected. Skipping sensor...'
'Proceed with caution!' % kind)

return Bunch(
# EGI stuff
nasion=fids['nasion'], lpa=fids['lpa'], rpa=fids['rpa'],
dig_ch_pos=dig_ch_pos, coord_frame='unknown',
# not EGI stuff
hsp=None, hpi=None, elp=None, point_names=None,
massich marked this conversation as resolved.
Show resolved Hide resolved
)


def _foo_get_data_from_dig(dig):
larsoner marked this conversation as resolved.
Show resolved Hide resolved
# XXXX:
# This does something really similar to _read_dig_montage_fif but:
# - does not check coord_frame
# - does not do any operation that implies assumptions with the names

# Split up the dig points by category
hsp, hpi, elp = list(), list(), list()
fids, dig_ch_pos_location = dict(), list()

for d in dig:
if d['kind'] == FIFF.FIFFV_POINT_CARDINAL:
fids[_cardinal_ident_mapping[d['ident']]] = d['r']
elif d['kind'] == FIFF.FIFFV_POINT_HPI:
hpi.append(d['r'])
elp.append(d['r'])
# XXX: point_names.append('HPI%03d' % d['ident'])
elif d['kind'] == FIFF.FIFFV_POINT_EXTRA:
hsp.append(d['r'])
elif d['kind'] == FIFF.FIFFV_POINT_EEG:
# XXX: dig_ch_pos['EEG%03d' % d['ident']] = d['r']
dig_ch_pos_location.append(d['r'])

dig_coord_frames = set([d['coord_frame'] for d in dig])
assert len(dig_coord_frames) == 1, 'Only single coordinate frame in dig is supported' # noqa # XXX
larsoner marked this conversation as resolved.
Show resolved Hide resolved

return Bunch(
nasion=fids.get('nasion', None),
lpa=fids.get('lpa', None),
rpa=fids.get('rpa', None),
hsp=np.array(hsp) if len(hsp) else None,
hpi=np.array(hpi) if len(hpi) else None,
elp=np.array(elp) if len(elp) else None,
dig_ch_pos_location=dig_ch_pos_location,
coord_frame=dig_coord_frames.pop(),
)


def _read_dig_montage_bvct(
fname,
unit,
_all_data_kwargs_are_none,
):

if not _all_data_kwargs_are_none:
raise ValueError('hsp, hpi, elp, point_names, fif must all be '
'None if egi is not None')
_check_fname(fname, overwrite='read', must_exist=True)

# CapTrak is natively in mm
scale = dict(mm=1e-3, cm=1e-2, auto=1e-3, m=1)
if unit not in scale:
raise ValueError("Unit needs to be one of %s, not %r" %
(sorted(scale.keys()), unit))
if unit not in ['mm', 'auto']:
warn('Using "{}" as unit for BVCT file. BVCT files are usually '
'specified in "mm". This might lead to errors.'.format(unit),
RuntimeWarning)

root = ElementTree.parse(fname).getroot()
sensors = root.find('CapTrakElectrodeList')

fids = {}
dig_ch_pos = {}

fid_name_map = {'Nasion': 'nasion', 'RPA': 'rpa', 'LPA': 'lpa'}

for s in sensors:
name = s.find('Name').text

# Need to prune "GND" and "REF": these are not included in the raw
# data and will raise errors when we try to do raw.set_montage(...)
# XXX eventually this should be stored in ch['loc'][3:6]
# but we don't currently have such capabilities here
if name in ['GND', 'REF']:
continue

fid = name in fid_name_map
coordinates = np.array([float(s.find('X').text),
float(s.find('Y').text),
float(s.find('Z').text)])

coordinates *= scale[unit]

# Fiducials
if fid:
fid_name = fid_name_map[name]
fids[fid_name] = coordinates
# EEG Channels
else:
dig_ch_pos[name] = coordinates

return Bunch(
# BVCT stuff
nasion=fids['nasion'], lpa=fids['lpa'], rpa=fids['rpa'],
dig_ch_pos=dig_ch_pos, coord_frame='unknown',
# not BVCT stuff
hsp=None, hpi=None, elp=None, point_names=None,
)
Loading