Skip to content

Commit

Permalink
[BUG, ENH, MRG] Pixels (#976)
Browse files Browse the repository at this point in the history
* wip

* fix montage going to head

* allow fiducials in coordinate frame of data

* fix tests'

* fix flake

* revert unneccesary changes

* Update doc/whats_new.rst

Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>

* Apply suggestions from code review

Co-authored-by: Alexandre Gramfort <alexandre.gramfort@m4x.org>
Co-authored-by: Richard Höchenberger <richard.hoechenberger@gmail.com>
  • Loading branch information
3 people committed Mar 4, 2022
1 parent 048bd0e commit b26f062
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 23 deletions.
4 changes: 4 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Enhancements

- Similarly, :func:`mne_bids.get_head_mri_trans` and :func:`mne_bids.update_anat_landmarks` gained a new ``kind`` parameter to specify which of multiple landmark sets to operate on, by `Alexandre Gramfort`_ and `Richard Höchenberger`_ (:gh:`955`, :gh:`957`)

- Add support for iEEG data in the coordinate frame ``Pixels``; although MNE-Python does not recognize this coordinate frame and so it will be set to ``unknown`` in the montage, MNE-Python can still be used to analyze this kind of data, by `Alex Rockhill`_ (:gh:`976`)

API and behavior changes
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -74,6 +76,8 @@ Bug fixes

- :func:`mne_bids.get_head_mri_trans` now respects ``datatype`` and ``suffix`` of the provided electrophysiological :class:`mne_bids.BIDSPath`, simplifying e.g. reading of derivaties, by `Richard Höchenberger`_ (:gh:`969`)

- Do not convert unknown coordinate frames to ``head``, by `Alex Rockhill`_ (:gh:`976`)

:doc:`Find out what was new in previous releases <whats_new_previous_releases>`

.. include:: authors.rst
7 changes: 7 additions & 0 deletions mne_bids/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,13 @@
'Commissure and the negative y-axis is passing through the '
'Posterior Commissure. The positive z-axis is passing through '
'a mid-hemispheric point in the superior direction.',
'pixels': 'If electrodes are localized in 2D space (only x and y are '
'specified and z is n/a), then the positions in this file '
'must correspond to the locations expressed in pixels on '
'the photo/drawing/rendering of the electrodes on the brain. '
'In this case, coordinates must be (row,column) pairs, with '
'(0,0) corresponding to the upper left pixel and (N,0) '
'corresponding to the lower left pixel.',
'ctf': 'ALS orientation and the origin between the ears',
'elektaneuromag': 'RAS orientation and the origin between the ears',
'4dbti': 'ALS orientation and the origin between the ears',
Expand Down
42 changes: 19 additions & 23 deletions mne_bids/dig.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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_bids.config import (BIDS_IEEG_COORDINATE_FRAMES,
Expand All @@ -36,11 +37,6 @@ def _handle_electrodes_reading(electrodes_fname, coord_frame,
electrodes_dict = _from_tsv(electrodes_fname)
ch_names_tsv = electrodes_dict['name']

summary_str = [(ch, coord) for idx, (ch, coord)
in enumerate(electrodes_dict.items())
if idx < 5]
logger.info("The read in electrodes file is: \n", summary_str)

def _float_or_nan(val):
if val == "n/a":
return np.nan
Expand Down Expand Up @@ -352,15 +348,18 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False,
coord_frame = MNE_TO_BIDS_FRAMES.get(mne_coord_frame, None)

if bids_path.datatype == 'ieeg' and mne_coord_frame == 'mri':
if acpc_aligned:
coord_frame = 'ACPC'
else:
if not acpc_aligned:
raise RuntimeError(
'`acpc_aligned` is False, if your T1 is not aligned '
'to ACPC and the coordinates are in fact in ACPC '
'space there will be no way to relate the coordinates '
'to the T1. If the T1 is ACPC-aligned, use '
'`acpc_aligned=True`')
coord_frame = 'ACPC'

if bids_path.datatype == 'ieeg' and bids_path.space is not None and \
bids_path.space.lower() == 'pixels':
coord_frame = 'Pixels'

# create electrodes/coordsystem files using a subset of entities
# that are specified for these files in the specification
Expand All @@ -378,9 +377,6 @@ def _write_dig_bids(bids_path, raw, montage=None, acpc_aligned=False,
coordsystem_path = BIDSPath(**coord_file_entities, suffix='coordsystem',
extension='.json')

logger.info(f'Writing electrodes file to... {electrodes_path}')
logger.info(f'Writing coordsytem file to... {coordsystem_path}')

if datatype == 'ieeg':
if coord_frame is not None:
# XXX: To improve when mne-python allows coord_frame='unknown'
Expand Down Expand Up @@ -442,13 +438,8 @@ def _read_dig_bids(electrodes_fpath, coordsystem_fpath,
Type of the data recording. Can be ``meg``, ``eeg``,
or ``ieeg``.
raw : mne.io.Raw
The raw data as MNE-Python ``Raw`` object. Will set montage
read in via ``raw.set_montage(montage)``.
Returns
-------
montage : mne.channels.DigMontage
The coordinate data as MNE-Python DigMontage object.
The raw data as MNE-Python ``Raw`` object. The montage
will be set in place.
"""
bids_coord_frame, bids_coord_unit = _handle_coordsystem_reading(
coordsystem_fpath, datatype)
Expand All @@ -472,10 +463,10 @@ def _read_dig_bids(electrodes_fpath, coordsystem_fpath,
# iEEG datatype for mne-python only supports
# mni_tal == fsaverage == MNI305
if bids_coord_frame == 'Pixels':
warn("Coordinate frame of iEEG data in pixels does not "
"get read in by mne-python. Skipping reading of "
"electrodes.tsv ...")
coord_frame = None
warn("Coordinate frame of iEEG data in pixels is not "
"recognized by mne-python, the coordinate frame "
"of the montage will be set to 'unknown'")
coord_frame = 'unknown'
elif bids_coord_frame == 'ACPC':
coord_frame = BIDS_TO_MNE_FRAMES.get(bids_coord_frame, None)
elif bids_coord_frame == 'Other':
Expand Down Expand Up @@ -537,4 +528,9 @@ def _read_dig_bids(electrodes_fpath, coordsystem_fpath,
# (EEG/sEEG/ECoG/DBS/fNIRS). Probably needs a fix in the future.
raw.set_montage(montage, on_missing='warn')

return montage
# 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']
for d in raw.info['dig']:
d['coord_frame'] = _str_to_frame['unknown']
40 changes: 40 additions & 0 deletions mne_bids/tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
write_meg_crosstalk, get_entities_from_fname,
get_anat_landmarks, write, anonymize_dataset)
from mne_bids.write import _get_fid_coords
from mne_bids.dig import _write_dig_bids, _read_dig_bids
from mne_bids.utils import (_stamp_to_dt, _get_anonymization_daysback,
get_anonymization_daysback, _write_json)
from mne_bids.tsv_handler import _from_tsv, _to_tsv
Expand Down Expand Up @@ -3556,3 +3557,42 @@ def test_anonymize_dataset_daysback(tmpdir):
rng=np.random.default_rng(),
show_progress_thresh=20
)


def test_write_dig(tmpdir):
"""Test whether the channel locations are written out properly."""
# Check progress bar output
bids_root = tmpdir / 'bids'
data_path = Path(testing.data_path())
raw_path = data_path / 'MEG' / 'sample' / 'sample_audvis_trunc_raw.fif'

# test coordinates in pixels
bids_path = _bids_path.copy().update(
root=bids_root, datatype='ieeg', space='Pixels')
os.makedirs(op.join(bids_root, 'sub-01', 'ses-01', bids_path.datatype),
exist_ok=True)
raw = _read_raw_fif(raw_path, verbose=False)
raw.pick_types(eeg=True)
raw.del_proj()
raw.set_channel_types({ch: 'ecog' for ch in raw.ch_names})

montage = raw.get_montage()
# fake transform to pixel coordinates
montage.apply_trans(mne.transforms.Transform('head', 'unknown'))
with pytest.warns(RuntimeWarning,
match='assuming identity'):
_write_dig_bids(bids_path, raw, montage)
electrodes_path = bids_path.copy().update(
task=None, run=None, suffix='electrodes', extension='.tsv')
coordsystem_path = bids_path.copy().update(
task=None, run=None, suffix='coordsystem', extension='.json')
with pytest.warns(RuntimeWarning,
match='recognized by mne-python'):
_read_dig_bids(electrodes_path, coordsystem_path,
bids_path.datatype, raw)
montage2 = raw.get_montage()
assert montage2.get_positions()['coord_frame'] == 'unknown'
assert_array_almost_equal(
np.array(list(montage.get_positions()['ch_pos'].values())),
np.array(list(montage2.get_positions()['ch_pos'].values()))
)

0 comments on commit b26f062

Please sign in to comment.