diff --git a/doc/whats_new.rst b/doc/whats_new.rst index 7344e87da..80b7bdd70 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -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 ` .. include:: authors.rst diff --git a/mne_bids/config.py b/mne_bids/config.py index 499116a82..e5cb6c3d9 100644 --- a/mne_bids/config.py +++ b/mne_bids/config.py @@ -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', diff --git a/mne_bids/dig.py b/mne_bids/dig.py index c91bf7faa..82aad6da5 100644 --- a/mne_bids/dig.py +++ b/mne_bids/dig.py @@ -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, @@ -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 @@ -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 @@ -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' @@ -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) @@ -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': @@ -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'] diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index 0e693373d..06f912109 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -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 @@ -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())) + )