-
Notifications
You must be signed in to change notification settings - Fork 98
[ENH, MRG] Add BIDS Standard Template Transformations #983
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
Changes from 9 commits
4c6ee96
23c42ed
0dac514
b18af9c
2b0874e
3709a47
287f279
cdd4bfd
040ed8d
c483681
2742a84
477b021
84b9e89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,30 @@ | ||
| """Read/write BIDS compatible electrode/coords structures from MNE.""" | ||
| # Authors: Adam Li <[email protected]> | ||
| # Stefan Appelhoff <[email protected]> | ||
| # Alex Rockhill <[email protected]> | ||
| # | ||
| # License: BSD-3-Clause | ||
| import json | ||
| from collections import OrderedDict | ||
| from pathlib import Path | ||
| import re | ||
| import warnings | ||
| from importlib_resources import files | ||
|
|
||
| import mne | ||
| import numpy as np | ||
| from mne.io.constants import FIFF | ||
| from mne.transforms import _str_to_frame | ||
| from mne.utils import logger, warn | ||
| from mne.utils import logger, warn, _validate_type, _check_option | ||
| from mne.io.pick import _picks_to_idx | ||
|
|
||
| from mne_bids.config import (ALLOWED_SPACES, | ||
| BIDS_COORDINATE_UNITS, | ||
| from mne_bids.config import (ALLOWED_SPACES, BIDS_COORDINATE_UNITS, | ||
| MNE_TO_BIDS_FRAMES, BIDS_TO_MNE_FRAMES, | ||
| MNE_FRAME_TO_STR, BIDS_COORD_FRAME_DESCRIPTIONS) | ||
| MNE_FRAME_TO_STR, MNE_STR_TO_FRAME, | ||
| BIDS_COORD_FRAME_DESCRIPTIONS, | ||
| BIDS_STANDARD_TEMPLATE_COORDINATE_SYSTEMS) | ||
| from mne_bids.tsv_handler import _from_tsv | ||
| from mne_bids.utils import _scale_coord_to_meters, _write_json, _write_tsv | ||
| from mne_bids.utils import (_scale_coord_to_meters, _write_json, _write_tsv, | ||
| verbose) | ||
| from mne_bids.path import BIDSPath | ||
|
|
||
|
|
||
|
|
@@ -396,9 +399,9 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False, | |
| message='.*nasion not found', module='mne') | ||
| raw.set_montage(montage) | ||
| for ch in raw.info['chs']: | ||
| ch['coord_frame'] = _str_to_frame[montage_coord_frame] | ||
| ch['coord_frame'] = MNE_STR_TO_FRAME[montage_coord_frame] | ||
| for d in raw.info['dig']: | ||
| d['coord_frame'] = _str_to_frame[montage_coord_frame] | ||
| d['coord_frame'] = MNE_STR_TO_FRAME[montage_coord_frame] | ||
| with raw.info._unlock(): # add back fiducials | ||
| raw.info['dig'] = fids + raw.info['dig'] | ||
|
|
||
|
|
@@ -543,6 +546,104 @@ def _read_dig_bids(electrodes_fpath, coordsystem_fpath, | |
| # put back in unknown for unknown coordinate frame | ||
| if coord_frame == 'unknown': | ||
| for ch in raw.info['chs']: | ||
| ch['coord_frame'] = _str_to_frame['unknown'] | ||
| ch['coord_frame'] = MNE_STR_TO_FRAME['unknown'] | ||
| for d in raw.info['dig']: | ||
| d['coord_frame'] = _str_to_frame['unknown'] | ||
| d['coord_frame'] = MNE_STR_TO_FRAME['unknown'] | ||
|
|
||
|
|
||
| @verbose | ||
| def template_to_head(raw, space, coord_frame='auto', unit='auto', | ||
| verbose=None): | ||
| """Transform a BIDS standard template montage to the head coordinate frame. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it head to template or template to head?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The template montage is getting transformed to head and you need a head->mri transform to get it to work with functions that require a
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah I see. Hard to do the mental gymnastics sometimes... In that case, I wonder if it's worth adding a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither You can plot the alignment with the T1 of the template space, if you put the wrong one, it will look off... We can add that later, there's an issue open because |
||
|
|
||
| Parameters | ||
| ---------- | ||
| raw : mne.io.Raw | ||
| The MNE-Python Raw object with a montage in one of the BIDS standard | ||
| template coordinate frames (modified in place). | ||
|
||
| space : str | ||
| The name of the BIDS standard template. See | ||
| https://bids-specification.readthedocs.io/en/stable/99-appendices/08-coordinate-systems.html#standard-template-identifiers | ||
| for a list of acceptable spaces. | ||
| coord_frame : str | ||
| BIDS template coordinate systems do not specify a coordinate frame | ||
| so this must be determined by inspecting the documentation for the | ||
| dataset or the electrodes.tsv file. Must be ``mri``, ``mri_voxel`` | ||
| or ``ras``. If ``auto``, the montage will be assumed to be in | ||
| ``mri_voxel`` if the coordinates are strictly positive and | ||
| ``scanner RAS`` otherwise. | ||
alexrockhill marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| .. warning:: | ||
|
|
||
| ``scanner RAS`` and ``surface RAS`` coordinates frames are similar | ||
| so be very careful not to assume a BIDS dataset's coordinates are | ||
| in one when they are actually in the other. The only way to tell | ||
| for template coordinate systems, currently, is if it is specified | ||
| in the dataset documentation. | ||
|
|
||
| unit : str | ||
| The unit that was used in the coordinate system specification; either | ||
| ``m`` or ``mm``. If ``auto``, ``m`` will be inferred if the montage | ||
| spans less than -1 to 1 in units (meters) and ``mm`` otherwise. If the | ||
| ``coord_frame`` is 'mri_voxel', ``unit`` will be ignored. | ||
alexrockhill marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| %(verbose)s | ||
|
|
||
| Returns | ||
| ------- | ||
| trans : mne.transforms.Transform | ||
| The data transformation matrix from ``head`` to ``mri`` coordinates. | ||
alexrockhill marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
alexrockhill marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| """ | ||
| _validate_type(raw, mne.io.BaseRaw) | ||
| _check_option('space', space, BIDS_STANDARD_TEMPLATE_COORDINATE_SYSTEMS) | ||
| _check_option('coord_frame', coord_frame, | ||
| ('auto', 'mri', 'mri_voxel', 'ras')) | ||
| _check_option('unit', unit, ('auto', 'm', 'mm')) | ||
| montage = raw.get_montage() | ||
| if montage is None: | ||
| raise RuntimeError('No montage found in the `raw` object') | ||
| montage.remove_fiducials() # we will add fiducials so remove any | ||
| pos = montage.get_positions() | ||
| if pos['coord_frame'] not in ('mni_tal', 'unknown'): | ||
| raise RuntimeError( | ||
| "Montage coordinate frame '{}' not expected for a template " | ||
| "montage, should be 'unknown' or 'mni_tal'".format( | ||
| pos['coord_frame'])) | ||
| locs = np.array(list(pos['ch_pos'].values())) | ||
| locs = locs[~np.any(np.isnan(locs), axis=1)] # only channels with loc | ||
| if locs.size == 0: | ||
| raise RuntimeError('No channel locations found in the montage') | ||
| if unit == 'auto': | ||
| unit = 'm' if abs(locs - locs.mean(axis=0)).max() < 1 else 'mm' | ||
| if coord_frame == 'auto': | ||
| coord_frame = 'mri_voxel' if locs.min() >= 0 else 'ras' | ||
| # transform montage to head | ||
| data_dir = files('mne_bids.data') | ||
| # set to the right coordinate frame as specified by the user | ||
| for d in montage.dig: # ensure same coordinate frame | ||
| d['coord_frame'] = MNE_STR_TO_FRAME[coord_frame] | ||
| # do the transforms, first ras -> vox if needed | ||
| if montage.get_positions()['coord_frame'] == 'ras': | ||
| ras_vox_trans = mne.read_trans( | ||
| data_dir.joinpath(f'space-{space}_ras-vox_trans.fif')) | ||
| if unit == 'm': # must be in mm here | ||
| for d in montage.dig: | ||
| d['r'] *= 1000 | ||
| montage.apply_trans(ras_vox_trans) | ||
| if montage.get_positions()['coord_frame'] == 'mri_voxel': | ||
| vox_mri_trans = mne.read_trans( | ||
| data_dir.joinpath(f'space-{space}_vox-mri_trans.fif')) | ||
| montage.apply_trans(vox_mri_trans) | ||
| assert montage.get_positions()['coord_frame'] == 'mri' | ||
| if not (unit == 'm' and coord_frame == 'mri'): # if so, already in m | ||
| for d in montage.dig: | ||
| d['r'] /= 1000 # mm -> m | ||
| # now add fiducials (in mri coordinates) | ||
| fids = mne.io.read_fiducials( | ||
| data_dir.joinpath(f'space-{space}_fiducials.fif'))[0] | ||
| montage.dig = fids + montage.dig # add fiducials | ||
| for fid in fids: # ensure also in mri | ||
| fid['coord_frame'] = MNE_STR_TO_FRAME['mri'] | ||
| raw.set_montage(montage) # transform to head | ||
| # finally return montage | ||
| return mne.read_trans(data_dir.joinpath(f'space-{space}_trans.fif')) | ||
alexrockhill marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.