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

[BUG, ENH, MRG] Pixels #976

Merged
merged 8 commits into from
Mar 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()))
)