diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adf7448ab..3d26814cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,3 +53,14 @@ The documentation can be built using sphinx. For that, please additionally install the following: $ pip install matplotlib nilearn sphinx numpydoc sphinx-gallery sphinx_bootstrap_theme pillow + +To build the documentation locally, one can run: + + $ cd doc/ + $ make html + +or + + $ make html-noplot + +if you don't want to run the examples to build the documentation. This will result in a faster build but produce no plots in the examples. diff --git a/doc/whats_new.rst b/doc/whats_new.rst index eebd70eab..beb3cf85e 100644 --- a/doc/whats_new.rst +++ b/doc/whats_new.rst @@ -63,6 +63,7 @@ Changelog - :func:`write_anat` allows writing T1 weighted MRI scans for subjects and optionally creating a T1w.json sidecar from a supplied :code:`trans` object, by `Stefan Appelhoff`_ (`#211 `_) - :func:`read_raw_bids` will return the the raw object with :code:`raw.info['bads']` already populated, whenever a :code:`channels.tsv` file is present, by `Stefan Appelhoff`_ (`#209 `_) - :func:`read_raw_bids` is now more likely to find event and channel sidecar json files, by `Marijn van Vliet`_ (`#233 `_) +- Enhanced :func:`read_raw_bids` and :func:`write_raw_bids` for iEEG coordinates along with example and unit test, by `Adam Li`_ (`#335 `_) Bug ~~~ diff --git a/examples/convert_eeg_to_bids.py b/examples/convert_eeg_to_bids.py index 2242a44e8..89289813c 100644 --- a/examples/convert_eeg_to_bids.py +++ b/examples/convert_eeg_to_bids.py @@ -47,11 +47,8 @@ # Conveniently, there is already a data loading function available with # MNE-Python: -# Make a directory to save the data to -home = os.path.expanduser('~') -mne_dir = os.path.join(home, 'mne_data') -if not os.path.exists(mne_dir): - os.makedirs(mne_dir) +# get MNE directory w/ example data +mne_dir = mne.get_config('MNE_DATASETS_SAMPLE_PATH') # Define which tasks we want to download. tasks = [2, # This is 2 minutes of eyes closed rest @@ -120,7 +117,7 @@ subject_id = '001' # zero padding to account for >100 subjects in this dataset task = 'resteyesclosed' raw_file = raw -bids_root = os.path.join(home, 'mne_data', 'eegmmidb_bids') +bids_root = os.path.join(mne_dir, 'eegmmidb_bids') ############################################################################### # Now we just need to specify a few more EEG details to get something sensible: diff --git a/examples/convert_ieeg_to_bids.py b/examples/convert_ieeg_to_bids.py new file mode 100644 index 000000000..3fc53d64b --- /dev/null +++ b/examples/convert_ieeg_to_bids.py @@ -0,0 +1,208 @@ +""" +==================================== +07. Convert iEEG data to BIDS format +==================================== + +In this example, we use MNE-BIDS to create a BIDS-compatible directory of iEEG +data. Specifically, we will follow these steps: + +1. Download some iEEG data from the + `PhysioBank database `_ + and the + `MNE-ECoG ex `_. + +2. Load the data, extract information, and save in a new BIDS directory. + +3. Check the result and compare it with the standard. + +4. Confirm that written iEEG coordinates are the + same before :func:`write_raw_bids` was called. + +The iEEG data will be written by :func:`write_raw_bids` with +the addition of extra metadata elements in the following files: + + * sidecar.json + * electrodes.tsv + * coord_system.json + * events.tsv + * channels.tsv + +Compared to EEG data, the main differences are within the +coord_system and electrodes files. +For more information on these files, +refer to the iEEG-BIDS specification. +""" + +# Authors: Adam Li +# License: BSD (3-clause) + +import os +import tempfile +from pprint import pprint + +import numpy as np +from scipy.io import loadmat + +import mne +from mne_bids import write_raw_bids, make_bids_basename, read_raw_bids +from mne_bids.utils import print_dir_tree + +############################################################################### +# Step 1: Download the data +# ------------------------- +# +# First, we need some data to work with. We will use the +# data downloaded via MNE-Python's API. +# +# ``_. +# +# Conveniently, there is already a data loading function available with +# MNE-Python: + +# get MNE directory w/ example data +mne_dir = mne.get_config('MNE_DATASETS_SAMPLE_PATH') + +# The electrode coords data are in the Matlab format: '.mat'. +# This is easy to read in with :func:`scipy.io.loadmat` function. +mat = loadmat(mne.datasets.misc.data_path() + '/ecog/sample_ecog.mat') +ch_names = mat['ch_names'].tolist() +ch_names = [x.strip() for x in ch_names] +elec = mat['elec'] # electrode positions given in meters + +############################################################################### +# Now we make a montage stating that the iEEG contacts are in MRI +# coordinate system. +montage = mne.channels.make_dig_montage(ch_pos=dict(zip(ch_names, elec)), + coord_frame='mri') +print('Created %s channel positions' % len(ch_names)) +print(dict(zip(ch_names, elec))) + +############################################################################### +# We also need to get some sample iEEG data, +# so we will just generate random data from white noise. +# Here is where you would use your own data if you had it. +ieegdata = np.random.rand(len(ch_names), 1000) + +# We will create a :func:`mne.io.RawArray` object and +# use the montage we created. +info = mne.create_info(ch_names, 1000., 'ecog') +raw = mne.io.RawArray(ieegdata, info) +raw.set_montage(montage) + +############################################################################### +# Let us confirm what our channel coordinates look like. + +# make a plot of the sensors in 2D plane +raw.plot_sensors(ch_type='ecog') + +# Get the first 5 channels and show their locations. +picks = mne.pick_types(raw.info, ecog=True) +dig = [raw.info['dig'][pick] for pick in picks] +chs = [raw.info['chs'][pick] for pick in picks] +pos = np.array([ch['r'] for ch in dig[:5]]) +ch_names = np.array([ch['ch_name'] for ch in chs[:5]]) +print("The channel coordinates before writing into BIDS: ") +pprint([x for x in zip(ch_names, pos)]) + +############################################################################### +# Step 2: Formatting as BIDS +# -------------------------- +# +# Now, let us format the `Raw` object into BIDS. + +############################################################################### +# With this step, we have everything to start a new BIDS directory using +# our data. To do that, we can use :func:`write_raw_bids` +# Generally, :func:`write_raw_bids` tries to extract as much +# meta data as possible from the raw data and then formats it in a BIDS +# compatible way. :func:`write_raw_bids` takes a bunch of inputs, most of +# which are however optional. The required inputs are: +# +# * :code:`raw` +# * :code:`bids_basename` +# * :code:`bids_root` +# +# ... as you can see in the docstring: +print(write_raw_bids.__doc__) + +############################################################################### +# Let us initialize some of the necessary data for the subject +# There is a subject, and specific task for the dataset. +subject_id = '001' # zero padding to account for >100 subjects in this dataset +task = 'testresteyes' + +# There is the root directory for where we will write our data. +bids_root = os.path.join(mne_dir, 'ieegmmidb_bids') + +############################################################################### +# Now we just need to specify a few iEEG details to make things work: +# We need the basename of the dataset. In addition, write_raw_bids +# requires a `filenames` of the Raw object to be non-empty, so since we +# initialized the dataset from an array, we need to do a hack where we +# temporarily save the data to disc before reading it back in. + +# Now convert our data to be in a new BIDS dataset. +bids_basename = make_bids_basename(subject=subject_id, + task=task, + acquisition="ecog") + +# need to set the filenames if we are initializing data from array +with tempfile.TemporaryDirectory() as tmp_root: + tmp_fpath = os.path.join(tmp_root, "test_raw.fif") + raw.save(tmp_fpath) + raw = mne.io.read_raw_fif(tmp_fpath) + + # write `raw` to BIDS and anonymize it into BrainVision format + write_raw_bids(raw, bids_basename, bids_root=bids_root, + anonymize=dict(daysback=30000), overwrite=True) + +############################################################################### +# Step 3: Check and compare with standard +# --------------------------------------- + +# Now we have written our BIDS directory. +print_dir_tree(bids_root) + +############################################################################### +# MNE-BIDS has created a suitable directory structure for us, and among other +# meta data files, it started an `events.tsv` and `channels.tsv` and made an +# initial `dataset_description.json` on top! +# +# Now it's time to manually check the BIDS directory and the meta files to add +# all the information that MNE-BIDS could not infer. For instance, you must +# describe iEEGReference and iEEGGround yourself. It's easy to find these by +# searching for "n/a" in the sidecar files. +# +# `$ grep -i 'n/a' ` +# +# Remember that there is a convenient javascript tool to validate all your BIDS +# directories called the "BIDS-validator", available as a web version and a +# command line tool: +# +# Web version: https://bids-standard.github.io/bids-validator/ +# +# Command line tool: https://www.npmjs.com/package/bids-validator + +############################################################################### +# Step 4: Plot output channels and check that they match! +# ------------------------------------------------------- +# +# Now we have written our BIDS directory. We can use +# :func:`read_raw_bids` to read in the data. + +# read in the BIDS dataset and plot the coordinates +bids_fname = bids_basename + "_ieeg.vhdr" +raw = read_raw_bids(bids_fname, bids_root=bids_root) + +# get the first 5 channels and show their locations +# this should match what was printed earlier. +picks = mne.pick_types(raw.info, ecog=True) +dig = [raw.info['dig'][pick] for pick in picks] +chs = [raw.info['chs'][pick] for pick in picks] +pos = np.array([ch['r'] for ch in dig[:5]]) +ch_names = np.array([ch['ch_name'] for ch in chs[:5]]) +print("The channel coordinates after writing into BIDS: ") +pprint([x for x in zip(ch_names, pos)]) + +# make a plot of the sensors in 2D plane +raw.plot_sensors(ch_type='ecog') diff --git a/mne_bids/read.py b/mne_bids/read.py index cbcf3be54..f859817a9 100644 --- a/mne_bids/read.py +++ b/mne_bids/read.py @@ -157,6 +157,65 @@ def _handle_events_reading(events_fname, raw): return raw +def _handle_electrodes_reading(electrodes_fname, coord_frame, raw, verbose): + """Read associated electrodes.tsv and populate raw. + + Handle xyz coordinates and coordinate frame of each channel. + Assumes units of coordinates are in 'm'. + """ + logger.info('Reading electrode ' + 'coords from {}.'.format(electrodes_fname)) + electrodes_dict = _from_tsv(electrodes_fname) + # First, make sure that ordering of names in channels.tsv matches the + # ordering of names in the raw data. The "name" column is mandatory in BIDS + ch_names_raw = list(raw.ch_names) + ch_names_tsv = electrodes_dict['name'] + + if ch_names_raw != ch_names_tsv: + msg = ('Channels do not correspond between raw data and the ' + 'channels.tsv file. For MNE-BIDS, the channel names in the ' + 'tsv MUST be equal and in the same order as the channels in ' + 'the raw data.\n\n' + '{} channels in tsv file: "{}"\n\n --> {}\n\n' + '{} channels in raw file: "{}"\n\n --> {}\n\n' + .format(len(ch_names_tsv), electrodes_fname, ch_names_tsv, + len(ch_names_raw), raw.filenames, ch_names_raw) + ) + + # XXX: this could be due to MNE inserting a 'STI 014' channel as the + # last channel: In that case, we can work. --> Can be removed soon, + # because MNE will stop the synthesis of stim channels in the near + # future + if not (ch_names_raw[-1] == 'STI 014' and + ch_names_raw[:-1] == ch_names_tsv): + raise RuntimeError(msg) + + if verbose: + print("The read in electrodes file is: \n", electrodes_dict) + + # convert coordinates to float and create list of tuples + ch_names_raw = [x for i, x in enumerate(ch_names_raw) + if electrodes_dict['x'][i] != "n/a"] + electrodes_dict['x'] = [float(x) for x in electrodes_dict['x'] + if x != "n/a"] + electrodes_dict['y'] = [float(x) for x in electrodes_dict['y'] + if x != "n/a"] + electrodes_dict['z'] = [float(x) for x in electrodes_dict['z'] + if x != "n/a"] + + ch_locs = list(zip(electrodes_dict['x'], + electrodes_dict['y'], + electrodes_dict['z'])) + ch_pos = dict(zip(ch_names_raw, ch_locs)) + + # create mne.DigMontage + montage = mne.channels.make_dig_montage(ch_pos=ch_pos, + coord_frame=coord_frame) + raw.set_montage(montage) + + return raw + + def _handle_channels_reading(channels_fname, bids_fname, raw): """Read associated channels.tsv and populate raw. @@ -296,6 +355,37 @@ def read_raw_bids(bids_fname, bids_root, extra_params=None, if channels_fname is not None: raw = _handle_channels_reading(channels_fname, bids_fname, raw) + # Try to find an associated electrodes.tsv and coordsystem.json + # to get information about the status and type of present channels + electrodes_fname = _find_matching_sidecar(bids_fname, bids_root, + 'electrodes.tsv', + allow_fail=True) + coordsystem_fname = _find_matching_sidecar(bids_fname, bids_root, + 'coordsystem.json', + allow_fail=True) + if electrodes_fname is not None: + if coordsystem_fname is None: + raise RuntimeError("BIDS mandates that the coordsystem.json " + "should exist if electrodes.tsv does. " + "Please create coordsystem.json for" + "{}".format(bids_basename)) + # Get MRI landmarks from the JSON sidecar + with open(coordsystem_fname, 'r') as fin: + coordsystem_json = json.load(fin) + + # Get coordinate frames that electrode coordinates are in + if kind == "meg": + coord_frame = coordsystem_json['MEGCoordinateSystem'] + elif kind == "ieeg": + coord_frame = coordsystem_json['iEEGCoordinateSystem'] + else: # noqa + raise RuntimeError("Kind {} not supported yet for " + "coordsystem.json and " + "electrodes.tsv.".format(kind)) + # read in electrode coordinates and attach to raw + raw = _handle_electrodes_reading(electrodes_fname, coord_frame, raw, + verbose) + # Try to find an associated sidecar.json to get information about the # recording snapshot sidecar_fname = _find_matching_sidecar(bids_fname, bids_root, diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py index 5296946a2..c412277f6 100644 --- a/mne_bids/tests/test_read.py +++ b/mne_bids/tests/test_read.py @@ -13,14 +13,15 @@ import mne from mne.io import anonymize_info -from mne.utils import _TempDir, requires_nibabel, check_version +from mne.utils import _TempDir, requires_nibabel, check_version, object_diff from mne.datasets import testing, somato import mne_bids from mne_bids import get_matched_empty_room -from mne_bids.read import _read_raw, get_head_mri_trans, \ - _handle_events_reading, _handle_info_reading -from mne_bids.tsv_handler import _to_tsv +from mne_bids.read import (read_raw_bids, + _read_raw, get_head_mri_trans, + _handle_events_reading, _handle_info_reading) +from mne_bids.tsv_handler import _to_tsv, _from_tsv from mne_bids.utils import (_find_matching_sidecar, _update_sidecar) from mne_bids.write import write_anat, write_raw_bids, make_bids_basename @@ -231,6 +232,52 @@ def test_handle_info_reading(): raw = mne_bids.read_raw_bids(bids_fname, bids_root) +def test_handle_coords_reading(): + """Test reading coordinates from BIDS files.""" + bids_root = _TempDir() + + data_path = op.join(testing.data_path(), 'EDF') + raw_fname = op.join(data_path, 'test_reduced.edf') + + raw = mne.io.read_raw_edf(raw_fname) + + # ensure we are writing 'ecog'/'ieeg' data + raw.set_channel_types({ch: 'ecog' + for ch in raw.ch_names}) + + # set a `random` montage + ch_names = raw.ch_names + elec_locs = np.random.random((len(ch_names), 3)).astype(float) + ch_pos = dict(zip(ch_names, elec_locs)) + montage = mne.channels.make_dig_montage(ch_pos=ch_pos, + coord_frame="mri") + raw.set_montage(montage) + write_raw_bids(raw, bids_basename, bids_root, overwrite=True) + + # read in the data and assert montage is the same + bids_fname = bids_basename + "_ieeg.edf" + raw_test = read_raw_bids(bids_fname, bids_root) + + # obtain the sensor positions + orig_locs = raw.info['dig'][1] + test_locs = raw_test.info['dig'][1] + assert orig_locs == test_locs + assert not object_diff(raw.info['chs'], raw_test.info['chs']) + + # test error message if electrodes don't match + electrodes_fname = _find_matching_sidecar(bids_fname, bids_root, + "electrodes.tsv", + allow_fail=True) + electrodes_dict = _from_tsv(electrodes_fname) + # pop off 5 channels + for key in electrodes_dict.keys(): + for i in range(5): + electrodes_dict[key].pop() + _to_tsv(electrodes_dict, electrodes_fname) + with pytest.raises(RuntimeError, match='Channels do not correspond'): + raw_test = read_raw_bids(bids_fname, bids_root) + + @requires_nibabel() def test_get_head_mri_trans_ctf(): """Test getting a trans object from BIDS data in CTF.""" diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py index ac29667bb..a27aa9d04 100644 --- a/mne_bids/tests/test_write.py +++ b/mne_bids/tests/test_write.py @@ -149,6 +149,13 @@ def test_fif(_bids_validate): 'sample_audvis_trunc_raw-eve.fif') raw = mne.io.read_raw_fif(raw_fname) + # add data in as a montage for MEG + ch_names = raw.ch_names + elec_locs = np.random.random((len(ch_names), 3)).tolist() + ch_pos = dict(zip(ch_names, elec_locs)) + meg_montage = mne.channels.make_dig_montage(ch_pos=ch_pos, + coord_frame='head') + raw.set_montage(meg_montage) write_raw_bids(raw, bids_basename, bids_root, events_data=events_fname, event_id=event_id, overwrite=False) @@ -656,6 +663,13 @@ def test_edf(_bids_validate): extra_params=dict(foo='bar')) bids_fname = bids_basename.replace('run-01', 'run-%s' % run2) + # add data in as a montage + ch_names = raw.ch_names + elec_locs = np.random.random((len(ch_names), 3)).tolist() + ch_pos = dict(zip(ch_names, elec_locs)) + eeg_montage = mne.channels.make_dig_montage(ch_pos=ch_pos, + coord_frame='head') + raw.set_montage(eeg_montage) write_raw_bids(raw, bids_fname, bids_root, overwrite=True) _bids_validate(bids_root) @@ -684,6 +698,18 @@ def test_edf(_bids_validate): write_raw_bids(ieeg_raw, bids_basename, bids_root) _bids_validate(bids_root) + # test writing electrode coordinates (.tsv) + # and coordinate system (.json) + ch_names = raw.ch_names + elec_locs = np.random.random((len(ch_names), 3)).tolist() + ch_pos = dict(zip(ch_names, elec_locs)) + ecog_montage = mne.channels.make_dig_montage(ch_pos=ch_pos, + coord_frame='mri') + raw.set_montage(ecog_montage) + bids_root = _TempDir() + write_raw_bids(raw, bids_basename, bids_root) + _bids_validate(bids_root) + # test anonymize and convert if check_version('mne', '0.20') and check_version('pybv', '0.2.0'): raw = mne.io.read_raw_edf(raw_fname) diff --git a/mne_bids/write.py b/mne_bids/write.py index 8573651fd..249a5e2f6 100644 --- a/mne_bids/write.py +++ b/mne_bids/write.py @@ -28,7 +28,7 @@ except ImportError: from mne._digitization._utils import _get_fid_coords from mne.channels.channels import _unit2human -from mne.utils import check_version, has_nibabel +from mne.utils import check_version, has_nibabel, _check_ch_locs from mne_bids.pick import coil_type from mne_bids.utils import (_write_json, _write_tsv, _read_events, _mkdir_p, @@ -145,6 +145,56 @@ def _channels_tsv(raw, fname, overwrite=False, verbose=True): return fname +def _electrodes_tsv(raw, fname, kind, overwrite=False, verbose=True): + """ + Create an electrodes.tsv file and save it. + + Parameters + ---------- + raw : instance of Raw + The data as MNE-Python Raw object. + fname : str + Filename to save the electrodes.tsv to. + kind : str + Acquisition type to save electrodes in. For iEEG, requires size. + overwrite : bool + Defaults to False. + Whether to overwrite the existing data in the file. + If there is already data for the given `fname` and overwrite is False, + an error will be raised. + verbose : bool + Set verbose output to true or false. + """ + x, y, z, names = list(), list(), list(), list() + for ch in raw.info['chs']: + if _check_ch_locs([ch]): + x.append(ch['loc'][0]) + y.append(ch['loc'][1]) + z.append(ch['loc'][2]) + else: + x.append('n/a') + y.append('n/a') + z.append('n/a') + names.append(ch['ch_name']) + + if kind == "ieeg": + sizes = ['n/a'] * len(names) + data = OrderedDict([('name', names), + ('x', x), + ('y', y), + ('z', z), + ('size', sizes), + ]) + else: + data = OrderedDict([('name', names), + ('x', x), + ('y', y), + ('z', z), + ]) + _write_tsv(fname, data, overwrite=overwrite, verbose=verbose) + return fname + + def _events_tsv(events, raw, fname, trial_type, overwrite=False, verbose=True): """Create an events.tsv file and save it. @@ -310,6 +360,67 @@ def _participants_json(fname, overwrite=False, verbose=True): return fname +def _coordsystem_json(raw, unit, orient, coordsystem_name, fname, + kind, overwrite=False, verbose=True): + """Create a coordsystem.json file and save it. + + Parameters + ---------- + raw : instance of Raw + The data as MNE-Python Raw object. + unit : str + Units to be used in the coordsystem specification. + orient : str + Used to define the coordinate system for the head coils. + coordsystem_name : str + Name of the coordinate system for the sensor positions. + fname : str + Filename to save the coordsystem.json to. + kind : str + Type of the data as in ALLOWED_KINDS. + overwrite : bool + Whether to overwrite the existing file. + Defaults to False. + verbose : bool + Set verbose output to true or false. + + """ + dig = raw.info['dig'] + coords = _extract_landmarks(dig) + hpi = {d['ident']: d for d in dig if d['kind'] == FIFF.FIFFV_POINT_HPI} + if hpi: + for ident in hpi.keys(): + coords['coil%d' % ident] = hpi[ident]['r'].tolist() + + coord_frame = set([dig[ii]['coord_frame'] for ii in range(len(dig))]) + if len(coord_frame) > 1: # noqa E501 + raise ValueError('All HPI, electrodes, and fiducials must be in the ' + 'same coordinate frame. Found: "{}"' + .format(coord_frame)) + + if kind == 'meg': + hpi = {d['ident']: d for d in dig if d['kind'] == FIFF.FIFFV_POINT_HPI} + if hpi: + for ident in hpi.keys(): + coords['coil%d' % ident] = hpi[ident]['r'].tolist() + + fid_json = {'MEGCoordinateSystem': coordsystem_name, + 'MEGCoordinateUnits': unit, # XXX validate this + 'HeadCoilCoordinates': coords, + 'HeadCoilCoordinateSystem': orient, + 'HeadCoilCoordinateUnits': unit # XXX validate this + } + elif kind == "ieeg": + fid_json = { + 'iEEGCoordinateSystem': coordsystem_name, # MRI, Pixels, or ACPC + 'iEEGCoordinateUnits': unit, # m (MNE), mm, cm , or pixels + } + + _write_json(fname, fid_json, overwrite, verbose) + + return fname + + def _scans_tsv(raw, raw_fname, fname, overwrite=False, verbose=True): """Create a scans.tsv file and save it. @@ -360,53 +471,6 @@ def _scans_tsv(raw, raw_fname, fname, overwrite=False, verbose=True): return fname -def _coordsystem_json(raw, unit, orient, manufacturer, fname, - overwrite=False, verbose=True): - """Create a coordsystem.json file and save it. - - Parameters - ---------- - raw : instance of Raw - The data as MNE-Python Raw object. - unit : str - Units to be used in the coordsystem specification. - orient : str - Used to define the coordinate system for the head coils. - manufacturer : str - Used to define the coordinate system for the MEG sensors. - fname : str - Filename to save the coordsystem.json to. - overwrite : bool - Whether to overwrite the existing file. - Defaults to False. - verbose : bool - Set verbose output to true or false. - - """ - dig = raw.info['dig'] - coords = _extract_landmarks(dig) - hpi = {d['ident']: d for d in dig if d['kind'] == FIFF.FIFFV_POINT_HPI} - if hpi: - for ident in hpi.keys(): - coords['coil%d' % ident] = hpi[ident]['r'].tolist() - - coord_frame = set([dig[ii]['coord_frame'] for ii in range(len(dig))]) - if len(coord_frame) > 1: - err = 'All HPI and Fiducials must be in the same coordinate frame.' - raise ValueError(err) - - fid_json = {'MEGCoordinateSystem': manufacturer, - 'MEGCoordinateUnits': unit, # XXX validate this - 'HeadCoilCoordinates': coords, - 'HeadCoilCoordinateSystem': orient, - 'HeadCoilCoordinateUnits': unit # XXX validate this - } - - _write_json(fname, fid_json, overwrite, verbose) - - return fname - - def _meg_landmarks_to_mri_landmarks(meg_landmarks, trans): """Convert landmarks from head space to MRI space. @@ -1166,10 +1230,39 @@ def write_raw_bids(raw, bids_basename, bids_root, events_data=None, _participants_json(participants_json_fname, True, verbose) _scans_tsv(raw, op.join(kind, bids_fname), scans_fname, overwrite, verbose) - # TODO: Implement coordystem.json and electrodes.tsv for EEG and iEEG + # TODO: Implement coordystem.json and electrodes.tsv for EEG + electrodes_fname = make_bids_basename( + subject=subject_id, session=session_id, acquisition=acquisition, + suffix='electrodes.tsv', prefix=data_path) if kind == 'meg' and not emptyroom: - _coordsystem_json(raw, unit, orient, manufacturer, coordsystem_fname, + _coordsystem_json(raw, unit, orient, + manufacturer, coordsystem_fname, kind, overwrite, verbose) + elif kind == 'ieeg': + coord_frame = "mri" # defaults to MRI coordinates + unit = "m" # defaults to meters + else: + coord_frame = None + unit = None + + # We only write electrodes.tsv and accompanying coordsystem.json + # if we have an available DigMontage + if raw.info['dig'] is not None: + if kind == "ieeg": + coords = _extract_landmarks(raw.info['dig']) + + # Rescale to MNE-Python "head" coord system, which is the + # "ElektaNeuromag" system (equivalent to "CapTrak" system) + if set(['RPA', 'NAS', 'LPA']) != set(list(coords.keys())): + # Now write the data to the elec coords and the coordsystem + _electrodes_tsv(raw, electrodes_fname, + kind, overwrite, verbose) + _coordsystem_json(raw, unit, orient, + coord_frame, coordsystem_fname, kind, + overwrite, verbose) + elif kind != "meg": + warn('Writing of electrodes.tsv is not supported for kind "{}". ' + 'Skipping ...'.format(kind)) events, event_id = _read_events(events_data, event_id, raw, ext) if events is not None and len(events) > 0 and not emptyroom: