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.

  2. Load the data, extract information, and save in a new BIDS directory.

  3. Check the result and compare it with the standard.

  4. Cite MNE-BIDS.

  5. Repeat the process for the fsaverage template coordinate space.

The iEEG data will be written by write_raw_bids() with the addition of extra metadata elements in the following files:

  • the sidecar file ieeg.json

  • electrodes.tsv

  • coordsystem.json

  • events.tsv

  • channels.tsv

Compared to EEG data, the main differences are within the coordsystem.json and electrodes.tsv files. For more information on these files, refer to the iEEG part of the BIDS specification.

# Authors: The MNE-BIDS developers
# SPDX-License-Identifier: BSD-3-Clause
import os.path as op
import shutil

import mne
import nibabel as nib
import numpy as np
from nilearn.plotting import plot_anat

from mne_bids import (
    BIDSPath,
    convert_montage_to_mri,
    convert_montage_to_ras,
    get_anat_landmarks,
    print_dir_tree,
    read_raw_bids,
    search_folder_for_text,
    template_to_head,
    write_anat,
    write_raw_bids,
)

Step 1: Download the data#

First, we need some data to work with. We will use the data downloaded via MNE-Python’s datasets API: mne.datasets.misc.data_path()

misc_path = mne.datasets.misc.data_path()

# The electrode coords data are in the tsv file format
# which is easily read in using numpy
raw = mne.io.read_raw_fif(op.join(misc_path, "seeg", "sample_seeg_ieeg.fif"))
raw.info["line_freq"] = 60  # specify power line frequency as required by BIDS
subjects_dir = op.join(misc_path, "seeg")  # Freesurfer recon-all directory
Using default location ~/mne_data for misc...
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.

When the locations of the channels in this dataset were found in Locating Intracranial Electrode Contacts, the T1 was aligned to ACPC. So, this montage is in an ACPC-aligned coordinate system. We can either save the channel positions in the subject’s anatomical space (from their T1 image) or we can transform to a template space such as fsaverage. To save them in the individual space, it is required that the T1 have been aligned to ACPC and then the channel positions be in terms of that coordinate system. Automated alignment to ACPC has not been implemented in MNE yet, so if the channel positions are not in an ACPC-aligned coordinate system, using a template (like fsaverage) is the best option.

# estimate the transformation from "head" to "mri" space
trans = mne.coreg.estimate_head_mri_t("sample_seeg", subjects_dir)

Now let’s convert the montage to “ras”

BIDS vs MNE-Python Coordinate Systems#

BIDS has many acceptable coordinate systems for iEEG, which can be viewed in appendix VIII of the BIDS specification. However, MNE-BIDS depends on MNE-Python and MNE-Python does not support all these coordinate systems (yet).

MNE-Python has a few tutorials on this topic:

MNE-Python supports using mni_tal and mri coordinate frames, corresponding to the fsaverage and ACPC (for an ACPC-aligned T1) BIDS coordinate systems respectively. All other coordinate coordinate frames in MNE-Python, if written with mne_bids.write_raw_bids(), must have an mne_bids.BIDSPath.space specified, and will be read in with the montage channel locations set to the coordinate frame ‘unknown’.

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 write_raw_bids() Generally, 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. write_raw_bids() takes a bunch of inputs, most of which are however optional. The required inputs are:

  • raw

  • bids_basename

  • bids_root

… as you can see in the docstring:

print(write_raw_bids.__doc__)
Save raw data to a BIDS-compliant folder structure.

    .. warning:: * The original file is simply copied over if the original
                   file format is BIDS-supported for that datatype. Otherwise,
                   this function will convert to a BIDS-supported file format
                   while warning the user. For EEG and iEEG data, conversion
                   will be to BrainVision format; for MEG, conversion will be
                   to FIFF.

                 * ``mne-bids`` will infer the manufacturer information
                   from the file extension. If your file format is non-standard
                   for the manufacturer, please update the manufacturer field
                   in the sidecars manually.

    Parameters
    ----------
    raw : mne.io.Raw
        The raw data. It must be an instance of `mne.io.Raw` that is not
        already loaded from disk unless ``allow_preload`` is explicitly set
        to ``True``. See warning for the ``allow_preload`` parameter.
    bids_path : BIDSPath
        The file to write. The :class:`mne_bids.BIDSPath` instance passed here
        **must** have the ``subject``, ``task``, and ``root`` attributes set.
        If the ``datatype`` attribute is not set, it will be inferred from the
        recording data type found in ``raw``. In case of multiple data types,
        the ``.datatype`` attribute must be set.
        Example::

            bids_path = BIDSPath(subject='01', session='01', task='testing',
                                 acquisition='01', run='01', datatype='meg',
                                 root='/data/BIDS')

        This will write the following files in the correct subfolder ``root``::

            sub-01_ses-01_task-testing_acq-01_run-01_meg.fif
            sub-01_ses-01_task-testing_acq-01_run-01_meg.json
            sub-01_ses-01_task-testing_acq-01_run-01_channels.tsv
            sub-01_ses-01_acq-01_coordsystem.json

        and the following one if ``events`` is not ``None``::

            sub-01_ses-01_task-testing_acq-01_run-01_events.tsv

        and add a line to the following files::

            participants.tsv
            scans.tsv

        Note that the extension is automatically inferred from the raw
        object.
    events : path-like | np.ndarray | None
        Use this parameter to specify events to write to the ``*_events.tsv``
        sidecar file, additionally to the object's :class:`~mne.Annotations`
        (which are always written).
        If ``path-like``, specifies the location of an MNE events file.
        If an array, the MNE events array (shape: ``(n_events, 3)``).
        If a path or an array and ``raw.annotations`` exist, the union of
        ``events`` and ``raw.annotations`` will be written.
        Mappings from event names to event codes (listed in the third
        column of the MNE events array) must be specified via the ``event_id``
        parameter; otherwise, an exception is raised. If
        :class:`~mne.Annotations` are present, their descriptions must be
        included in ``event_id`` as well.
        If ``None``, events will only be inferred from the raw object's
        :class:`~mne.Annotations`.

        .. note::
           If specified, writes the union of ``events`` and
           ``raw.annotations``. If you wish to **only** write
           ``raw.annotations``, pass ``events=None``. If you want to
           **exclude** the events in ``raw.annotations`` from being written,
           call ``raw.set_annotations(None)`` before invoking this function.

        .. note::
           Either, descriptions of all event codes must be specified via the
           ``event_id`` parameter or each event must be accompanied by a
           row in ``event_metadata``.

    event_id : dict | None
        Descriptions or names describing the event codes, if you passed
        ``events``. The descriptions will be written to the ``trial_type``
        column in ``*_events.tsv``. The dictionary keys correspond to the event
        description,s and the values to the event codes. You must specify a
        description for all event codes appearing in ``events``. If your data
        contains :class:`~mne.Annotations`, you can use this parameter to
        assign event codes to each unique annotation description (mapping from
        description to event code).
    event_metadata : pandas.DataFrame | None
        Metadata for each event in ``events``. Each row corresponds to an event.
    extra_columns_descriptions : dict | None
        A dictionary that maps column names of the ``event_metadata`` to descriptions.
        Each column of ``event_metadata`` must have a corresponding entry in this.
    anonymize : dict | None
        If `None` (default), no anonymization is performed.
        If a dictionary, data will be anonymized depending on the dictionary
        keys: ``daysback`` is a required key, ``keep_his`` is optional.

        ``daysback`` : int
            Number of days by which to move back the recording date in time.
            In studies with multiple subjects the relative recording date
            differences between subjects can be kept by using the same number
            of ``daysback`` for all subject anonymizations. ``daysback`` should
            be great enough to shift the date prior to 1925 to conform with
            BIDS anonymization rules.

        ``keep_his`` : bool
            If ``False`` (default), all subject information next to the
            recording date will be overwritten as well. If ``True``, keep
            subject information apart from the recording date.

        ``keep_source`` : bool
            Whether to store the name of the ``raw`` input file in the
            ``source`` column of ``scans.tsv``. By default, this information
            is not stored.

    format : 'auto' | 'BrainVision' | 'EDF' | 'FIF' | 'EEGLAB'
        Controls the file format of the data after BIDS conversion. If
        ``'auto'``, MNE-BIDS will attempt to convert the input data to BIDS
        without a change of the original file format. A conversion to a
        different file format will then only take place if the original file
        format lacks some necessary features.
        Conversion may be forced to BrainVision, EDF, or EEGLAB for (i)EEG,
        and to FIF for MEG data.
    symlink : bool
        Instead of copying the source files, only create symbolic links to
        preserve storage space. This is only allowed when not anonymizing the
        data (i.e., ``anonymize`` must be ``None``).

        .. note::
           Symlinks currently only work with FIFF files. In case of split
           files, only a link to the first file will be created, and
           :func:`mne_bids.read_raw_bids` will correctly handle reading the
           data again.

        .. note::
           Symlinks are currently only supported on macOS and Linux. We will
           add support for Windows 10 at a later time.

    empty_room : mne.io.Raw | BIDSPath | None
        The empty-room recording to be associated with this file. This is
        only supported for MEG data.
        If :class:`~mne.io.Raw`, you may pass raw data that was not preloaded
        (otherwise, pass ``allow_preload=True``); i.e., it behaves similar to
        the ``raw`` parameter. The session name will be automatically generated
        from the raw object's ``info['meas_date']``.
        If a :class:`~mne_bids.BIDSPath`, the ``root`` attribute must be the
        same as in ``bids_path``. Pass ``None`` (default) if you do not wish to
        specify an associated empty-room recording.

        .. versionchanged:: 0.11
           Accepts :class:`~mne.io.Raw` data.
    allow_preload : bool
        If ``True``, allow writing of preloaded raw objects (i.e.,
        ``raw.preload`` is ``True``). Because the original file is ignored, you
        must specify what ``format`` to write (not ``auto``).

        .. warning::
            BIDS was originally designed for unprocessed or minimally processed
            data. For this reason, by default, we prevent writing of preloaded
            data that may have been modified. Only use this option when
            absolutely necessary: for example, manually converting from file
            formats not supported by MNE or writing preprocessed derivatives.
            Be aware that these use cases are not fully supported.
    montage : mne.channels.DigMontage | None
        The montage with channel positions if channel position data are
        to be stored in a format other than "head" (the internal MNE
        coordinate frame that the data in ``raw`` is stored in).
    acpc_aligned : bool
        It is difficult to check whether the T1 scan is ACPC aligned which
        means that "mri" coordinate space is "ACPC" BIDS coordinate space.
        So, this flag is required to be True when the digitization data
        is in "mri" for intracranial data to confirm that the T1 is
        ACPC-aligned.
    overwrite : bool
        Whether to overwrite existing files or data in files.
        Defaults to ``False``.

        If ``True``, any existing files with the same BIDS parameters
        will be overwritten with the exception of the ``*_participants.tsv``
        and ``*_scans.tsv`` files. For these files, parts of pre-existing data
        that match the current data will be replaced. For
        ``*_participants.tsv``, specifically, age, sex and hand fields will be
        overwritten, while any manually added fields in ``participants.json``
        and ``participants.tsv`` by a user will be retained.
        If ``False``, no existing data will be overwritten or
        replaced.


    verbose : bool | str | int | None
        Control verbosity of the logging output. If ``None``, use the default
        verbosity level. See the :ref:`logging documentation <tut-logging>` and
        :func:`mne.verbose` for details. Should only be passed as a keyword
        argument.

    Returns
    -------
    bids_path : BIDSPath
        The path of the created data file.

        .. note::
           If you passed empty-room raw data via ``empty_room``, the
           :class:`~mne_bids.BIDSPath` of the empty-room recording can be
           retrieved via ``bids_path.find_empty_room(use_sidecar_only=True)``.

    Notes
    -----
    You should ensure that ``raw.info['subject_info']`` and
    ``raw.info['meas_date']`` are set to proper (not-``None``) values to allow
    for the correct computation of each participant's age when creating
    ``*_participants.tsv``.

    This function will convert existing `mne.Annotations` from
    ``raw.annotations`` to events. Additionally, any events supplied via
    ``events`` will be written too. To avoid writing of annotations,
    remove them from the raw file via ``raw.set_annotations(None)`` before
    invoking ``write_raw_bids``.

    To write events encoded in a ``STIM`` channel, you first need to create the
    events array manually and pass it to this function:

    ..
        events = mne.find_events(raw, min_duration=0.002)
        write_raw_bids(..., events=events)

    See the documentation of :func:`mne.find_events` for more information on
    event extraction from ``STIM`` channels.

    When anonymizing ``.edf`` files, then the file format for EDF limits
    how far back we can set the recording date. Therefore, all anonymized
    EDF datasets will have an internal recording date of ``01-01-1985``,
    and the actual recording date will be stored in the ``scans.tsv``
    file's ``acq_time`` column.

    ``write_raw_bids`` will generate a ``dataset_description.json`` file
    if it does not already exist. Minimal metadata will be written there.
    If one sets ``overwrite`` to ``True`` here, it will not overwrite an
    existing ``dataset_description.json`` file.
    If you need to add more data there, or overwrite it, then you should
    call :func:`mne_bids.make_dataset_description` directly.

    When writing EDF or BDF files, all file extensions are forced to be
    lower-case, in compliance with the BIDS specification.

    See Also
    --------
    mne.io.Raw.anonymize
    mne.find_events
    mne.Annotations
    mne.events_from_annotations

Let us initialize some of the necessary data for the subject.

# There is a subject, and specific task for the dataset.
subject_id = "1"
task = "motor"

# get MNE-Python directory w/ example data
mne_data_dir = mne.get_config("MNE_DATASETS_MISC_PATH")

# There is the root directory for where we will write our data.
bids_root = op.join(mne_data_dir, "ieeg_bids")

To ensure the output path doesn’t contain any leftover files from previous tests and example runs, we simply delete it.

Warning

Do not delete directories that may contain important data!

Now we just need make a mne_bids.BIDSPath to save the data.

Warning

By passing acpc_aligned=True, we are affirming that the T1 in this dataset is aligned to ACPC. This is very difficult to check with a computer which is why this step is required.

# Now convert our data to be in a new BIDS dataset.
bids_path = BIDSPath(subject=subject_id, task=task, root=bids_root)

# plot T1 to show that it is ACPC-aligned
# note that the origin is centered on the anterior commissure (AC)
# with the y-axis passing through the posterior commissure (PC)
T1_fname = op.join(subjects_dir, "sample_seeg", "mri", "T1.mgz")
fig = plot_anat(T1_fname, cut_coords=(0, 0, 0))
fig.axes["x"].ax.annotate(
    "AC",
    (2.0, -2.0),
    (30.0, -40.0),
    color="w",
    arrowprops=dict(facecolor="w", alpha=0.5),
)
fig.axes["x"].ax.annotate(
    "PC",
    (-31.0, -2.0),
    (-80.0, -40.0),
    color="w",
    arrowprops=dict(facecolor="w", alpha=0.5),
)

# write ACPC-aligned T1
landmarks = get_anat_landmarks(T1_fname, raw.info, trans, "sample_seeg", subjects_dir)
T1_bids_path = write_anat(T1_fname, bids_path, deface=True, landmarks=landmarks)

# write `raw` to BIDS and anonymize it (converts to BrainVision format)
#
# we need to pass the `montage` argument for coordinate frames other than
# "head" which is what MNE uses internally in the `raw` object
#
# `acpc_aligned=True` affirms that our MRI is aligned to ACPC
# if this is not true, convert to `fsaverage` (see below)!
write_raw_bids(
    raw,
    bids_path,
    anonymize=dict(daysback=40000),
    montage=montage,
    acpc_aligned=True,
    overwrite=True,
)

# check our output
print_dir_tree(bids_root)
convert ieeg to bids
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/anat/sub-1_T1w.json'...
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.
/home/circleci/project/examples/convert_ieeg_to_bids.py:213: RuntimeWarning: Converting data files to BrainVision format for anonymization
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/README'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-ACPC_electrodes.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-ACPC_coordsystem.json'...
The provided raw data contains annotations, but you did not pass an "event_id" mapping from annotation descriptions to event codes. We will generate arbitrary event codes. To specify custom event codes, please pass "event_id".
Used Annotations descriptions: [np.str_('Fixation'), np.str_('Go Cue'), np.str_('ISI Onset'), np.str_('Response')]
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.json'...
Writing '/home/circleci/mne_data/ieeg_bids/dataset_description.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_ieeg.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_channels.tsv'...
/home/circleci/project/examples/convert_ieeg_to_bids.py:213: RuntimeWarning: Converting data files to BrainVision format
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv'...
Wrote /home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv entry with ieeg/sub-1_task-motor_ieeg.vhdr.
|ieeg_bids/
|--- README
|--- dataset_description.json
|--- participants.json
|--- participants.tsv
|--- sub-1/
|------ sub-1_scans.tsv
|------ anat/
|--------- sub-1_T1w.json
|--------- sub-1_T1w.nii.gz
|------ ieeg/
|--------- sub-1_space-ACPC_coordsystem.json
|--------- sub-1_space-ACPC_electrodes.tsv
|--------- sub-1_task-motor_channels.tsv
|--------- sub-1_task-motor_events.json
|--------- sub-1_task-motor_events.tsv
|--------- sub-1_task-motor_ieeg.eeg
|--------- sub-1_task-motor_ieeg.json
|--------- sub-1_task-motor_ieeg.vhdr
|--------- sub-1_task-motor_ieeg.vmrk

MNE-BIDS has created a suitable directory structure for us, and among other meta data files, it started an events.tsv and channels.tsv file, and created an initial dataset_description.json file 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.

sub-1/ieeg/sub-1_task-motor_ieeg.json
    7        "SoftwareFilters": "n/a",
    10       "iEEGReference": "n/a",
sub-1/ieeg/sub-1_task-motor_events.json
    8            "Description": "Duration of the event in seconds from onset. Must be zero, positive, or 'n/a' if unavailable. A zero value indicates an impulse event. ",
participants.tsv
    1    participa age       sex       hand      weight    height
    2    sub-1     n/a       n/a       n/a       n/a       n/a
sub-1/ieeg/sub-1_space-ACPC_electrodes.tsv
    1    name      x         y         z         size
    2    LENT 1    -0.021200 0.0158514 -0.039700 n/a
    3    LENT 2    -0.023233 0.0168625 -0.038201 n/a
    4    LENT 3    -0.026400 0.0185514 -0.035700 n/a
    5    LENT 4    -0.029233 0.0198625 -0.033922 n/a
    6    LENT 5    -0.031700 0.0206514 -0.032200 n/a
    7    LENT 6    -0.034122 0.0217514 -0.030478 n/a
    8    LENT 7    -0.036855 0.0225292 -0.028108 n/a
    9    LAMY 1    -0.026011 -0.000693 -0.015478 n/a
    10   LAMY 2    -0.030600 -0.000148 -0.015200 n/a
    11   LAMY 3    -0.035536 0.0011150 -0.014882 n/a
    ...
sub-1/ieeg/sub-1_task-motor_channels.tsv
    1    name      type      units     low_cutof high_cuto descripti sampling_ status    status_de
    2    LENT 1    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    3    LENT 2    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    4    LENT 3    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    5    LENT 4    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    6    LENT 5    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    7    LENT 6    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    8    LENT 7    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    9    LAMY 1    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    10   LAMY 2    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      n/a
    11   LAMY 3    SEEG      V         0.0       499.70605 StereoEEG 999.41210 good      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 3: Load channels from BIDS-formatted dataset and compare#

Now we have written our BIDS directory. We can use read_raw_bids() to read in the data.

# read in the BIDS dataset to plot the coordinates
raw2 = read_raw_bids(bids_path=bids_path)
Extracting parameters from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_ieeg.vhdr...
Setting channel info structure...
Reading events from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.tsv.
Reading channel info from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_channels.tsv.
Reading electrode coords from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-ACPC_electrodes.tsv.

Now we have to go back to “head” coordinates.

Note

If you were downloading this from OpenNeuro, you would have to run the Freesurfer recon-all to get the transforms.

montage2 = raw2.get_montage()

# we need to go from scanner RAS back to surface RAS (requires recon-all)
convert_montage_to_mri(montage2, "sample_seeg", subjects_dir=subjects_dir)

# this uses Freesurfer recon-all subject directory
montage2.add_estimated_fiducials("sample_seeg", subjects_dir=subjects_dir)

# get head->mri trans, invert from mri->head
trans2 = mne.transforms.invert_transform(mne.channels.compute_native_head_t(montage2))

# now the montage is properly in "head" and ready for analysis in MNE
raw2.set_montage(montage2)

# get the monage, apply the trans and make sure it's the same
# note: the head coordinates may differ because they are defined by
# the fiducials which are estimated; as long as the head->mri trans
# is computed with the same fiducials, the coordinates will be the same
# in ACPC space which is what matters
montage = raw.get_montage()  # the original montage in 'head' coordinates
montage.apply_trans(trans)
montage2 = raw2.get_montage()  # the recovered montage in 'head' coordinates
montage2.apply_trans(trans2)

# compare with standard
print(
    f"Recovered coordinate: {montage2.get_positions()['ch_pos']['LENT 1']}\n"
    f"Saved coordinate: {montage.get_positions()['ch_pos']['LENT 1']}"
)
Recovered coordinate: [-0.02430001  0.02610001 -0.05600001]
Saved coordinate: [-0.0243  0.0261 -0.056 ]

Step 4: Cite mne-bids#

We can see that the appropriate citations are already written in the README. If you are preparing a manuscript, please make sure to also cite MNE-BIDS there.

readme = op.join(bids_root, "README")
with open(readme, encoding="utf-8-sig") as fid:
    text = fid.read()
print(text)
References
----------
Appelhoff, S., Sanderson, M., Brooks, T., Vliet, M., Quentin, R., Holdgraf, C., Chaumon, M., Mikulan, E., Tavabi, K., Höchenberger, R., Welke, D., Brunner, C., Rockhill, A., Larson, E., Gramfort, A. and Jas, M. (2019). MNE-BIDS: Organizing electrophysiological data into the BIDS format and facilitating their analysis. Journal of Open Source Software 4: (1896).https://doi.org/10.21105/joss.01896

Holdgraf, C., Appelhoff, S., Bickel, S., Bouchard, K., D'Ambrosio, S., David, O., … Hermes, D. (2019). iEEG-BIDS, extending the Brain Imaging Data Structure specification to human intracranial electrophysiology. Scientific Data, 6, 102. https://doi.org/10.1038/s41597-019-0105-7

Step 5: Store coordinates in a template space#

Alternatively, if your T1 is not aligned to ACPC-space or you prefer to store the coordinates in a template space (e.g. fsaverage) for another reason, you can also do that.

Here we’ll use the MNI Talairach transform to get to fsaverage space from “mri” aka surface RAS space. fsaverage is very useful for group analysis as shown in Working with sEEG data. Note, this is only a linear transform and so one loses quite a bit of accuracy relative to the needs of intracranial researchers so it is quite suboptimal. A better option is to use a symmetric diffeomorphic transform to create a one-to-one mapping of brain voxels from the individual’s brain to the template as shown in Locating intracranial electrode contacts. Even so, it’s better to provide the coordinates in the individual’s brain space, as was done above, so that the researcher who uses the coordinates has the ability to tranform them to a template of their choice.

Note

For fsaverage, the template coordinate system was defined so that scanner RAS is equivalent to surface RAS. BIDS requires that template data be in scanner RAS so for coordinate frames where this is not the case, the coordinates must be converted (see below).

# ensure the output path doesn't contain any leftover files from previous
# tests and example runs
if op.exists(bids_root):
    shutil.rmtree(bids_root)

# load our raw data again
raw = mne.io.read_raw_fif(op.join(misc_path, "seeg", "sample_seeg_ieeg.fif"))
raw.info["line_freq"] = 60  # specify power line frequency as required by BIDS

# get Talairach transform
mri_mni_t = mne.read_talxfm("sample_seeg", subjects_dir)
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.

Now let’s convert the montage to MNI Talairach (“mni_tal”).

montage = raw.get_montage()
montage.apply_trans(trans)  # head->mri
montage.apply_trans(mri_mni_t)

# write to BIDS, this time with a template coordinate system
write_raw_bids(
    raw, bids_path, anonymize=dict(daysback=40000), montage=montage, overwrite=True
)

# read in the BIDS dataset
raw2 = read_raw_bids(bids_path=bids_path)
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.
/home/circleci/project/examples/convert_ieeg_to_bids.py:350: RuntimeWarning: Converting data files to BrainVision format for anonymization
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/README'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_electrodes.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_coordsystem.json'...
The provided raw data contains annotations, but you did not pass an "event_id" mapping from annotation descriptions to event codes. We will generate arbitrary event codes. To specify custom event codes, please pass "event_id".
Used Annotations descriptions: [np.str_('Fixation'), np.str_('Go Cue'), np.str_('ISI Onset'), np.str_('Response')]
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.json'...
Writing '/home/circleci/mne_data/ieeg_bids/dataset_description.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_ieeg.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_channels.tsv'...
/home/circleci/project/examples/convert_ieeg_to_bids.py:350: RuntimeWarning: Converting data files to BrainVision format
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv'...
Wrote /home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv entry with ieeg/sub-1_task-motor_ieeg.vhdr.
Extracting parameters from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_ieeg.vhdr...
Setting channel info structure...
Reading events from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_events.tsv.
Reading channel info from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_channels.tsv.
Reading electrode coords from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_electrodes.tsv.

MNE-Python uses head coordinates with a head -> mri trans so we need to make sure to get our data in this form. As shown below, the montage is in the mni_tal coordinate frame but doesn’t have fiducials. The head coordinate frame is defined based on the fiducial points so we need to add these. Fortunately, there is a convenient function (mne_bids.template_to_head()) that loads stored fiducials and takes care of the transformations. Once this function is applied, you can use the raw object and the trans as in any MNE example (e.g. Working with sEEG data).

# use `coord_frame='mri'` to indicate that the montage is in surface RAS
# and `unit='m'` to indicate that the units are in meters
trans2 = template_to_head(raw2.info, space="fsaverage", coord_frame="mri", unit="m")[1]
# this a bit confusing since we transformed from mri->mni and now we're
# saying we're back in 'mri' but that is because we were in the surface RAS
# coordinate frame of `sample_seeg` and transformed to 'mni_tal', which is the
# surface RAS coordinate frame for `fsaverage`: since MNE denotes surface RAS
# as 'mri', both coordinate frames are 'mri', it's just that 'mni_tal' is 'mri'
# when the subject is 'fsaverage'

Let’s check that we can recover the original coordinates from the BIDS dataset now that we are working in the head coordinate frame with a head -> mri trans which is the setup MNE-Python is designed around.

# check that we can recover the coordinates
print(
    "Recovered coordinate head: {recovered}\n"
    "Original coordinate head:  {original}".format(
        recovered=raw2.info["chs"][0]["loc"][:3], original=raw.info["chs"][0]["loc"][:3]
    )
)

# check difference in trans
print(
    f"Recovered trans:\n{trans2['trans'].round(3)}\n"
    # combine head->mri with mri->mni to get head->mni
    # and then invert to get mni->head
    "Original trans:\n"
    f"{np.linalg.inv(np.dot(trans['trans'], mri_mni_t['trans'])).round(3)}"
)

# ensure that the data in MNI coordinates is exactly the same
# (within computer precision)
montage2 = raw2.get_montage()  # get montage after transformed back to head
montage2.apply_trans(trans2)
print(
    f"Recovered coordinate: {montage2.get_positions()['ch_pos']['LENT 1']}\n"
    f"Original coordinate: {montage.get_positions()['ch_pos']['LENT 1']}"
)
Recovered coordinate head: [-0.02488703  0.03772073 -0.0126908 ]
Original coordinate head:  [-0.02599896  0.03496219 -0.01177379]
Recovered trans:
[[ 1.    -0.004 -0.     0.002]
 [ 0.004  0.998 -0.057 -0.029]
 [ 0.     0.057  0.998 -0.041]
 [ 0.     0.     0.     1.   ]]
Original trans:
[[ 0.966  0.029  0.006 -0.003]
 [-0.003  0.926  0.126  0.028]
 [-0.001 -0.053  0.946  0.04 ]
 [ 0.     0.     0.     1.   ]]
Recovered coordinate: [-0.0231477   0.00949445 -0.05183359]
Original coordinate: [-0.0231477   0.00949445 -0.05183359]

As you can see the coordinates stored in the raw object are slightly off. This is because the head coordinate frame is defined by the fiducials (nasion, left and right pre-auricular points), and, in the first case, the fiducials were found on the individual anatomy and then transformed to MNI space, whereas, in the second case, they were found directly on the template brain (this was done once for the template so that we could just load it from a file). This difference means that there are slightly different head->mri transforms. Once these transforms are applied, however, the positions are the same in MNI coordinates which is what is important.

As a final step, let’s go over how to assign coordinate systems that are not recognized by MNE-Python. Many template coordinate systems are allowed by BIDS but are not used in MNE-Python. For these templates, the fiducials have been found and the transformations have been pre-computed so that we can get our coordinates in the head coordinate frame that MNE-Python uses.

Note

As of this writing, BIDS accepts channel coordinates in reference to the the following template spaces: ICBM452AirSpace, ICBM452Warp5Space, IXI549Space, fsaverage, fsaverageSym, fsLR, MNIColin27, MNI152Lin, MNI152NLin2009[a-c][Sym|Asym], MNI152NLin6Sym, MNI152NLin6ASym, MNI305, NIHPD, OASIS30AntsOASISAnts, OASIS30Atropos, Talairach and UNCInfant. As discussed above, it is recommended to share the coordinates in the individual subject’s anatomical reference frame so that researchers who use the data can transform the coordinates to any of these templates that they choose.

BIDS requires that the template be stored in scanner RAS coordinates so first we’ll convert our original data to scanner RAS and then convert it back. Just in case the template electrode coordinates are provided in voxels or the unit is not specified, these options are able to be overridden in mne_bids.template_to_head() for ease of use.

Warning

If no coordinate frame is passed to mne_bids.template_to_head() it will infer voxels if the coordinates are only positive and scanner RAS otherwise. Be sure not to use the wrong coordinate frame! surface RAS and scanner RAS are quite similar which is especially confusing, but, fortunately, in most of the Freesurfer template coordinate systems surface RAS is identical to scanner RAS. surface RAS is a Freesurfer coordinate frame so it is most likely to be used with Freesurfer template coordinate systems). This is the case for fsaverage, MNI305 and fsaverageSym but not fsLR.

The template should be in scanner RAS:

# ensure the output path doesn't contain any leftover files from previous
# tests and example runs
if op.exists(bids_root):
    shutil.rmtree(bids_root)

# get a template mgz image to transform the montage to voxel coordinates
subjects_dir = op.join(mne.datasets.sample.data_path(), "subjects")
template_T1 = nib.load(op.join(subjects_dir, "fsaverage", "mri", "T1.mgz"))

# get voxels to surface RAS and scanner RAS transforms
vox_mri_t = template_T1.header.get_vox2ras_tkr()  # surface RAS
vox_ras_t = template_T1.header.get_vox2ras()  # scanner RAS

raw = mne.io.read_raw_fif(
    op.join(misc_path, "seeg", "sample_seeg_ieeg.fif")  # load our raw data again
)
montage = raw.get_montage()  # get the original montage
montage.apply_trans(trans)  # head->mri
montage.apply_trans(mri_mni_t)  # mri->mni
pos = montage.get_positions()
ch_pos = np.array(list(pos["ch_pos"].values()))  # get an array of positions
# mri -> vox and m -> mm
ch_pos = mne.transforms.apply_trans(np.linalg.inv(vox_mri_t), ch_pos * 1000)
ch_pos = mne.transforms.apply_trans(vox_ras_t, ch_pos)

montage_ras = mne.channels.make_dig_montage(
    ch_pos=dict(zip(pos["ch_pos"].keys(), ch_pos)), coord_frame="ras"
)

# specify our standard template coordinate system space
bids_path.update(datatype="ieeg", space="fsaverage")

# write to BIDS, this time with a template coordinate system in voxels
write_raw_bids(
    raw, bids_path, anonymize=dict(daysback=40000), montage=montage_ras, overwrite=True
)
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.
Opening raw data file /home/circleci/mne_data/MNE-misc-data/seeg/sample_seeg_ieeg.fif...
    Range : 1310640 ... 1370605 =   1311.411 ...  1371.411 secs
Ready.
/home/circleci/project/examples/convert_ieeg_to_bids.py:493: RuntimeWarning: Converting data files to BrainVision format for anonymization
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/README'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/participants.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_electrodes.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_coordsystem.json'...
The provided raw data contains annotations, but you did not pass an "event_id" mapping from annotation descriptions to event codes. We will generate arbitrary event codes. To specify custom event codes, please pass "event_id".
Used Annotations descriptions: [np.str_('Fixation'), np.str_('Go Cue'), np.str_('ISI Onset'), np.str_('Response')]
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_events.tsv'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_events.json'...
Writing '/home/circleci/mne_data/ieeg_bids/dataset_description.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_ieeg.json'...
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_channels.tsv'...
/home/circleci/project/examples/convert_ieeg_to_bids.py:493: RuntimeWarning: Converting data files to BrainVision format
  write_raw_bids(
Writing '/home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv'...
Wrote /home/circleci/mne_data/ieeg_bids/sub-1/sub-1_scans.tsv entry with ieeg/sub-1_task-motor_space-fsaverage_ieeg.vhdr.

BIDSPath(
root: /home/circleci/mne_data/ieeg_bids
datatype: ieeg
basename: sub-1_task-motor_space-fsaverage_ieeg.vhdr)

Now, let’s load our data and convert our montage to head.

raw2 = read_raw_bids(bids_path=bids_path)
trans2 = template_to_head(  # unit='auto' automatically determines it's in mm
    raw2.info, space="fsaverage", coord_frame="ras", unit="auto"
)[1]
Extracting parameters from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_ieeg.vhdr...
Setting channel info structure...
Reading events from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_events.tsv.
Reading channel info from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_task-motor_space-fsaverage_channels.tsv.
Reading electrode coords from /home/circleci/mne_data/ieeg_bids/sub-1/ieeg/sub-1_space-fsaverage_electrodes.tsv.

Let’s check to make sure again that the original coordinates from the BIDS dataset were recovered.

montage2 = raw2.get_montage()  # get montage after transformed back to head
montage2.apply_trans(trans2)  # apply trans to go back to 'mri'
print(
    f"Recovered coordinate: {montage2.get_positions()['ch_pos']['LENT 1']}\n"
    f"Original coordinate: {montage.get_positions()['ch_pos']['LENT 1']}"
)
Recovered coordinate: [-0.0231477   0.00949445 -0.05183359]
Original coordinate: [-0.0231477   0.00949445 -0.05183359]

In summary, as we saw, these standard template spaces that are allowable by BIDS are quite complicated. We therefore only cover these cases because datasets are allowed to be in these coordinate systems, and we want to be able to analyze them with MNE-Python. BIDS data in a template coordinate space doesn’t allow you to convert to a template of your choosing so it is better to save the raw data in the individual’s ACPC space. Thus, we recommend, if at all possible, saving BIDS iEEG data in ACPC coordinate space corresponding to the individual subject’s brain, not in a template coordinate frame.

Total running time of the script: (0 minutes 4.162 seconds)

Gallery generated by Sphinx-Gallery