Skip to content

Commit 5798e49

Browse files
authored
[ENH, MRG] Allow template coordinate frame writing (#980)
* wip * allow template coordinate frames for write * fix flake * fix tests * richard review, fix codecov * fix raw file modified issue
1 parent 33cbe67 commit 5798e49

File tree

9 files changed

+428
-161
lines changed

9 files changed

+428
-161
lines changed

doc/whats_new.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ Enhancements
5656

5757
- All non-MNE-Python BIDS coordinate frames are now set to ``'unknown'`` on reading, by `Alex Rockhill`_ (:gh:`979`)
5858

59+
- :func:`mne_bids.write_raw_bids` can now write to template coordinates by `Alex Rockhill`_ (:gh:`980`)
60+
5961
API and behavior changes
6062
^^^^^^^^^^^^^^^^^^^^^^^^
6163

@@ -65,6 +67,8 @@ API and behavior changes
6567

6668
- Passing ``fs_subject=None`` to :func:`get_head_mri_trans` has been deprecated. Please pass the FreeSurfer subject name explicitly, by Richard Höchenberger`_ (:gh:`977`)
6769

70+
- Corrupted or missing fiducials in ``head`` coordinates now raise an error instead of warning in :func:`mne_bids.write_raw_bids` by `Alex Rockhill`_ (:gh:`980`)
71+
6872
Requirements
6973
^^^^^^^^^^^^
7074

examples/convert_ieeg_to_bids.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
2121
5. Repeat the process for the ``fsaverage`` template coordinate frame.
2222
23+
6. Repeat the process for one of the other standard template coordinate frames
24+
allowed by BIDS.
25+
2326
The iEEG data will be written by :func:`write_raw_bids` with
2427
the addition of extra metadata elements in the following files:
2528
@@ -50,6 +53,7 @@
5053
# %%
5154

5255
import os.path as op
56+
import numpy as np
5357
import shutil
5458

5559
from nilearn.plotting import plot_anat
@@ -331,3 +335,142 @@
331335
# ``montage.add_estimated_fiducials(template, os.environ['FREESURFER_HOME'])``
332336
# where ``template`` maybe be ``cvs_avg35_inMNI152`` for instance
333337
raw.set_montage(montage)
338+
339+
# %%
340+
# Step 6: Store coordinates in another template space accepted by BIDS
341+
# --------------------------------------------------------------------
342+
# As of this writing, BIDS accepts channel coordinates in reference to the
343+
# the following template spaces: ``ICBM452AirSpace``, ``ICBM452Warp5Space``,
344+
# ``IXI549Space``, ``fsaverage``, ``fsaverageSym``, ``fsLR``, ``MNIColin27``,
345+
# ``MNI152Lin``, ``MNI152NLin2009[a-c][Sym|Asym]``, ``MNI152NLin6Sym``,
346+
# ``MNI152NLin6ASym``, ``MNI305``, ``NIHPD``, ``OASIS30AntsOASISAnts``,
347+
# ``OASIS30Atropos``, ``Talairach`` and ``UNCInfant``. As discussed above,
348+
# it is recommended to share the coordinates in the individual subject's
349+
# anatomical reference frame so that researchers who use the data can
350+
# transform the coordinates to any of these templates that they choose. If
351+
# BIDS-formatted data is shared in one of these template coordinate frames,
352+
# the data can still be analyzed in MNE-Python. However, MNE-Python only
353+
# recognizes a few coordinate frames (so that coordinate frames that are
354+
# not regularly used by the MNE community don't misleadingly appear to be
355+
# being fully maintained when they are not) so you'll need a bit more
356+
# know-how, which we will go over below.
357+
358+
# ensure the output path doesn't contain any leftover files from previous
359+
# tests and example runs
360+
if op.exists(bids_root):
361+
shutil.rmtree(bids_root)
362+
363+
364+
# first we'll write our data as if it were MNI152NLin2009bAsym coordinates
365+
# (you would need to transform your coordinates to this template first)
366+
bids_path.update(datatype='ieeg', space='MNI152NLin2009bAsym')
367+
368+
# load our raw data again
369+
raw = mne.io.read_raw_fif(op.join(
370+
misc_path, 'seeg', 'sample_seeg_ieeg.fif'))
371+
raw.info['line_freq'] = 60 # specify power line frequency as required by BIDS
372+
373+
# get the montage as stored in raw
374+
# MNE stores coordinates in raw objects in "head" coordinates for consistency
375+
montage = raw.get_montage()
376+
377+
# define a transform to MNI152NLin2009bAsym (fake it)
378+
# MNE-Python doesn't recognize MNI152NLin2009bAsym, so we have to use
379+
# the unknown coordinate frame
380+
head_template_t = np.array([[1.0, 0.1, 0.2, 10.1],
381+
[-0.1, 1.0, 0.1, -20.3],
382+
[0.2, -0.1, 1.0, -30.7],
383+
[0.0, 0.0, 0.0, 1.0]])
384+
head_template_trans = mne.transforms.Transform(
385+
fro='head', to='unknown', trans=head_template_t)
386+
montage.apply_trans(head_template_trans)
387+
388+
# write to BIDS, with the montage in fsaverage coordinates
389+
write_raw_bids(raw, bids_path, anonymize=dict(daysback=40000),
390+
montage=montage, overwrite=True)
391+
392+
# %%
393+
# Now, let's see how we would work with the data in MNE. As shown below, the
394+
# montage has the same coordinates as when it was written, but the coordinate
395+
# frame is unknown.
396+
397+
# read back in the raw data
398+
raw = read_raw_bids(bids_path=bids_path)
399+
400+
# get the montage
401+
montage2 = raw.get_montage()
402+
print('Montage set to: ' + montage2.get_positions()['coord_frame'])
403+
404+
# check that we can recover the coordinates
405+
print('Recovered coordinate: {recovered}\n'
406+
'Saved coordinate: {saved}'.format(
407+
recovered=montage2.dig[0]['r'],
408+
saved=montage.dig[0]['r']))
409+
410+
# %%
411+
# To work with this data in the template coordinate frame, all the same steps
412+
# can be followed in :ref:`tut-working-with-seeg` and
413+
# :ref:`tut-working-with-ecog` after the montage is transformed to surface
414+
# RAS coordinates for the template MRI (if it isn't there already).
415+
#
416+
# First we'll need the ``subjects_dir`` where the recon-all for the template
417+
# brain is stored.
418+
#
419+
# .. code-block:: python
420+
#
421+
# subjects_dir = op.join(misc_path, 'subjects') # for example
422+
# # add some plotting keyword arguments
423+
# brain_kwargs = dict(cortex='low_contrast', alpha=0.2, background='white')
424+
#
425+
# If the montage is already in surface RAS for the template MRI, we can use:
426+
#
427+
# .. code-block:: python
428+
#
429+
# # identity transform since 'unknown' is already 'mri' == surface RAS
430+
# # for the template brain MRI
431+
# trans = mne.transforms.Transform(
432+
# fro='unknown',
433+
# to='mri',
434+
# trans=np.eye(4)
435+
# )
436+
# brain = mne.viz.Brain('sample_seeg', subjects_dir=subjects_dir,
437+
# **brain_kwargs)
438+
# brain.add_sensors(raw.info, trans=trans)
439+
#
440+
# If the montage was in voxel coordinates, we'll first have to transform
441+
# to surface RAS:
442+
#
443+
# .. code-block:: python
444+
#
445+
# import nibabel as nib
446+
# template_T1 = nib.load(op.join(subjects_dir, 'MNI152NLin2009bAsym',
447+
# 'mri', 'T1.mgz'))
448+
# trans = mne.transforms.Transform( # use vox to surface RAS transform
449+
# fro='unknown',
450+
# to='mri',
451+
# trans=template_T1.header.get_vox2ras_tkr()
452+
# )
453+
# brain = mne.viz.Brain(
454+
# 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs)
455+
# brain.add_sensors(raw.info, trans=trans)
456+
#
457+
#
458+
# Finally, if the montage was in scanner RAS coordinates, we'll have to
459+
# transform it back to voxels first before going to surface RAS:
460+
#
461+
# .. code-block:: python
462+
#
463+
# import nibabel as nib
464+
# template_T1 = nib.load(op.join(subjects_dir, 'MNI152NLin2009bAsym',
465+
# 'mri', 'T1.mgz'))
466+
# ras_vox_t = template_T1.header.get_ras2vox()
467+
# vox_mri_t = template_T1.header.get_vox2ras_tkr()
468+
# ras_mri_t = np.dot(ras_vox_t, vox_mri_t) # ras->vox with vox->mri
469+
# trans = mne.transforms.Transform(
470+
# fro='unknown',
471+
# to='mri',
472+
# trans=ras_mri_t
473+
# )
474+
# brain = mne.viz.Brain(
475+
# 'sample_seeg', subjects_dir=subjects_dir, **brain_kwargs)
476+
# brain.add_sensors(raw.info, trans=trans)

mne_bids/config.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,12 @@
109109
]
110110

111111
# allowed extensions (data formats) in BIDS spec
112-
ALLOWED_DATATYPE_EXTENSIONS = {'meg': allowed_extensions_meg,
113-
'eeg': allowed_extensions_eeg,
114-
'ieeg': allowed_extensions_ieeg,
115-
'nirs': allowed_extensions_nirs}
112+
ALLOWED_DATATYPE_EXTENSIONS = {
113+
'meg': allowed_extensions_meg,
114+
'eeg': allowed_extensions_eeg,
115+
'ieeg': allowed_extensions_ieeg,
116+
'nirs': allowed_extensions_nirs
117+
}
116118

117119
# allow additional extensions that are not BIDS
118120
# compliant, but we will convert to the
@@ -163,7 +165,7 @@
163165
# Annotations to never remove during reading or writing
164166
ANNOTATIONS_TO_KEEP = ('BAD_ACQ_SKIP',)
165167

166-
coordsys_standard_template = [
168+
BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES = [
167169
'ICBM452AirSpace',
168170
'ICBM452Warp5Space',
169171
'IXI549Space',
@@ -216,7 +218,7 @@
216218
# accepted coordinate SI units
217219
BIDS_COORDINATE_UNITS = ['m', 'cm', 'mm']
218220
coordsys_wildcard = ['Other']
219-
BIDS_SHARED_COORDINATE_FRAMES = (coordsys_standard_template +
221+
BIDS_SHARED_COORDINATE_FRAMES = (BIDS_STANDARD_TEMPLATE_COORDINATE_FRAMES +
220222
coordsys_standard_template_deprecated +
221223
coordsys_wildcard)
222224

@@ -264,11 +266,7 @@
264266
MNE_TO_BIDS_FRAMES = {
265267
'ctf_head': 'CTF',
266268
'head': 'CapTrak',
267-
'mni_tal': 'fsaverage',
268-
# 'fs_tal': 'fsaverage', # XXX: not used
269-
'unknown': 'Other',
270-
'ras': 'Other',
271-
'mri': 'Other'
269+
'mni_tal': 'fsaverage'
272270
}
273271

274272
# these coordinate frames in mne-python are related to scalp/meg
@@ -318,8 +316,62 @@
318316
'fsaverage': 'Defined by FreeSurfer, the MRI (surface RAS) origin is '
319317
'at the center of a 256×256×256 mm^3 anisotropic volume '
320318
'(may not be in the center of the head).',
319+
'icbm452airspace': 'Reference space defined by the "average of 452 '
320+
'T1-weighted MRIs of normal young adult brains" '
321+
'with "linear transforms of the subjects into the '
322+
'atlas space using a 12-parameter affine '
323+
'transformation"',
324+
'icbm452warp5space': 'Reference space defined by the "average of 452 '
325+
'T1-weighted MRIs of normal young adult brains" '
326+
'"based on a 5th order polynomial transformation '
327+
'into the atlas space"',
328+
'ixi549space': 'Reference space defined by the average of the "549 (...) '
329+
'subjects from the IXI dataset" linearly transformed to '
330+
'ICBM MNI 452.',
331+
'fsaveragesym': 'The fsaverage is a dual template providing both '
332+
'volumetric and surface coordinates references. The '
333+
'volumetric template corresponds to a FreeSurfer variant '
334+
'of MNI305 space. The fsaverageSym atlas also defines a '
335+
'symmetric surface reference system (formerly described '
336+
'as fsaveragesym).',
337+
'fslr': 'The fsLR is a dual template providing both volumetric and '
338+
'surface coordinates references. The volumetric template '
339+
'corresponds to MNI152NLin6Asym. Surface templates are given '
340+
'at several sampling densities: 164k (used by HCP pipelines '
341+
'for 3T and 7T anatomical analysis), 59k (used by HCP pipelines '
342+
'for 7T MRI bold and DWI analysis), 32k (used by HCP pipelines '
343+
'for 3T MRI bold and DWI analysis), or 4k (used by HCP '
344+
'pipelines for MEG analysis) fsaverage_LR surface '
345+
'reconstructed from the T1w image.',
346+
'mnicolin27': 'Average of 27 T1 scans of a single subject.',
347+
'mni152lin': 'Also known as ICBM (version with linear coregistration).',
348+
'mni152nlin6sym': 'Also known as symmetric ICBM 6th generation '
349+
'(non-linear coregistration).',
350+
'mni152nlin6asym': 'A variation of MNI152NLin6Sym built by A. Janke that '
351+
'is released as the MNI template of FSL. Volumetric '
352+
'templates included with HCP-Pipelines correspond to '
353+
'this template too.',
354+
'mni305': 'Also known as avg305.',
355+
'nihpd': 'Pediatric templates generated from the NIHPD sample. Available '
356+
'for different age groups (4.5–18.5 y.o., 4.5–8.5 y.o., '
357+
'7–11 y.o., 7.5–13.5 y.o., 10–14 y.o., 13–18.5 y.o. This '
358+
'template also comes in either -symmetric or -asymmetric flavor.',
359+
'oasis30antsoasisants':
360+
'See https://figshare.com/articles/ANTs_ANTsR_Brain_Templates/915436',
361+
'oasis30atropos':
362+
'See https://mindboggle.info/data.html',
363+
'talairach': 'Piecewise linear scaling of the brain is implemented as '
364+
'described in TT88.',
365+
'uncinfant': 'Infant Brain Atlases from Neonates to 1- and 2-year-olds.'
321366
}
322367

368+
for letter in ('a', 'b', 'c'):
369+
for sym in ('Sym', 'Asym'):
370+
BIDS_COORD_FRAME_DESCRIPTIONS[f'mni152nlin2009{letter}{sym}'] = \
371+
'Also known as ICBM (non-linear coregistration with 40 iterations,'
372+
' released in 2009). It comes in either three different flavours '
373+
'each in symmetric or asymmetric version.'
374+
323375
REFERENCES = {'mne-bids':
324376
'Appelhoff, S., Sanderson, M., Brooks, T., Vliet, M., '
325377
'Quentin, R., Holdgraf, C., Chaumon, M., Mikulan, E., '

0 commit comments

Comments
 (0)