Getting iEEG montage volume label and plotting electrodes on subject pial surface

Dear all,

I am currently facing issues with the plotting of iEEG electrodes on a patient

  • MNE-Python version: 0.24.0
  • operating system: linux

I am attempting to achieve three different things in my pipeline. I want first to create a montage for a subject with the electrodes localization. I then need to locate the electrodes on different atlases (Desikan and Destrieux). Finally, I need to plot the participant electrodes on the brain surface. All these steps need to be performed in the subject T1 space as opposed to fsaverage space.

In order to perform these steps, I have available the electrodes reconstruction in T1 space in a tsv file as well as the free surfer reconstruction of the subject.

I am therefore using the following functions:

I am first loading the tsv file of the electrodes in T1 space, using the following function:

def set_montage(raw, bids_path, montage_space="T1"):

    if montage_space == "T1":
        coord_file = "*space-Other_electrodes"
        coord_frame = "head"
    elif montage_space == "MNI":
        coord_file = "*space-fsaverage_electrodes"
        coord_frame = "mni_tal"
    else:
        raise Exception("You have passed a montage space that is not supported. It should be either T1 or MNI! Check "
                        "your config")

    # Loading the coordinate file:
    recon_file = find_files(bids_path.directory, naming_pattern=coord_file, extension=".tsv")
    # Load the file:
    channels_coordinates = pd.read_csv(recon_file[0], sep='\t')  # Loading the coordinates
    # From this file, getting the channels:
    channels = channels_coordinates["name"].tolist()
    # Get the position:
    position = channels_coordinates[["x", "y", "z"]].to_numpy()
    # Create the montage:
    montage = mne.channels.make_dig_montage(ch_pos=dict(zip(channels, position)),
                                            coord_frame=coord_frame)
    # And set the montage on the raw object:
    raw.set_montage(montage, on_missing='warn')

    return raw

I am then performing the mapping of the electrodes into the different atlases using the following function:

def roi_mapping(mne_object, list_parcellations, subject_id, fs_dir, step,
                subject_info):
    labels_df = {parcellation: pd.DataFrame() for parcellation in list_parcellations}
    for parcellation in list_parcellations:
        labels, _ = mne.get_montage_volume_labels(
            mne_object.get_montage(), subject_id, subjects_dir=fs_dir, aseg=parcellation)
        # Keeping only one ROI per electrodes, otherwise we will break stats down the line by taking the same elec
        # several times:
        for ind, channel in enumerate(labels.keys()):
            # Keeping the first ROI that is not unknown, because unknown is uninteresting if we have more than one:
            if len(labels[channel]) > 1:
                roi = [roi for roi in labels[channel] if roi != "Unknown"][0]
            elif len(labels[channel]) == 1:
                roi = labels[channel][0]
            else:
                roi = "Unknown"
            labels_df[parcellation] = labels_df[parcellation].append(
                pd.DataFrame({"channel": channel, "region": roi}, index=[ind]))

    return labels_df

Finally, in order to plot the participant electrodes on their own brain surface, I am calling the plot_alignment function as below:

subject_id="sub-XXX"
fs_subjects_directory="path/to/freesurfer/root"
fig = plot_alignment(raw.info, subject=subject_id, subjects_dir=fs_subjects_directory,
                             surfaces="pial", coord_frame='mri')

Unfortunately, the last call doesn’t work, throwing me the following error:

ValueError: A head->mri transformation matrix is required to plot pial surfaces, trans=None is not allowed

This error indicates that the transformation is missing. I am however unsure how to address this issue. First of all, does this error in the plot_alignment also indicates that there are issues in the localization of the different atlases? And second of all, if that is not the case, can the issue be solved by computing the transformation using

        subj_trans_ecog = mne.coreg.estimate_head_mri_t(
            subject_id, fs_subjects_directory)

at the moment of creating the montage?

Generally, I am a bit confused about the different coordinate frames and their requirements when it comes to iEEG data. Has anyone attempted something similar and could shed lights on the best procedure to follow?

Thanks in advance for your support,

Kind regards,

Alex

Tagging @adam2392 and @alexrockhill, who both work with iEEG data

Hi Alex, your pipeline looks good to me, the trans aspect is a bit confusing but there is also somewhat of a good reason behind it. MNE stores all data in the “head” coordinate frame with the origin between the right and left auricular points and y heading toward the nasion. This is a very reasonable coordinate frame for MEG and EEG because it doesn’t depend on the localization coordinate frame (i.e. polhemous pen etc.). It also is unambiguous for ieeg with the main downside being that you really don’t need these fiducial points in your analysis since everything is in MRI space. However, in MNE it’s nice that everything is consistently in the head space and that that coordinate frame makes sense even if you don’t have the MRI (in practice the error from two users finding fiducial points is possibly acceptable for EEG and MEG but is definitely not acceptable for ieeg but if we just keep track of the trans that won’t be necessary). In summary, everything is in the head coordinate frame for consistency, and this is described in the tutorial “locating intracranial electrode contacts”.

So the solution is to assign fiducials automatically to your montage if they aren’t there already (montage.add_estimated_fiducials) and then use mne.compute_native_head_t to get the trans. Note that this only applies if they are in MRI space, if the coordinates start in head space, you’ll have to align them to the MRI using the coregistration GUI which outputs a trans as stated above though this is really not accurate enough for ieeg so hopefully this is not the case.

Ah I reread your code and if they are in other space in BIDS, hopefully they are in scanner RAS which is the coordinate frame “ras” in which case you need the RAS to surface RAS which freesurfer uses which can be obtained from the T1

import numpy as np
import nibabel as nib
T1 = nib.load('/path/to/T1.mgz')
ras_vox_t = T1.header.get_ras2vox()
vox_mri_t = T1.header.get_vox2ras_tkr()
ras_mri_t = mne.transforms.combine_transforms(ras_vox_t, vox_mri_t)
montage.apply_trans(ras_mri_t)

But if they are already in surface RAS, then you can just mne.compute_native_head_t to get the trans.

If they are in mni_tal (Talairach) coordinates, then you need to transform back to individual mri coordinates with

mni_mri_t = mne.read_talxfm(subject, subjects_dir)
montage.apply_trans(mni_mri_t)
trans = mne.compute_native_head_t(montage)

Now that I’ve written all this code is maybe a good time to mention that all of this is done for you in mne_bids in the tutorial about converting ieeg data since it’s all a bit complicated.

08. Convert iEEG data to BIDS format — MNE-BIDS 0.8 documentation Note that it also contains how to read it out, not just convert it.

A final note just after rereading, we’ve had discussions about BIDS coordinate frames for ieeg and “Other” is very confusing for this reason: is it scanner RAS or surface RAS or maybe even voxel coordinates? It doesn’t really follow the BIDS ethos of specifying this kind of thing to avoid errors. We’ve added Scanner RAS as acceptable for ieeg BIDS data recently which will hopefully clear this up. Although, data still exists in “Other” format so you’ll unfortunately just have to figure out which coordinate frame it’s in and very unfortunately you’ll probably have to understand all of these coordinate frame transforms to do so instead of just relying on mne_bids to do it for you. Like I said, unfortunate but hopefully changing soon.

1 Like

A post was split to a new topic: Plotting sEEG contacts

Dear all,

Thanks a lot for the feedback, this is very helpful. I have been looking closer at the mne tutorial. I was looking at outdated version, the newer ones are much clearer, thanks a lot for the tip. I have managed to plot my electrodes on the subject brain surface by first using the add_estimated_fiducials followed by the compute_native_head_t as recommended. I am unfortunately hitting another issue as a result of this fix.

As you noted, the electrodes localization are found in “other” coordinates system. This is my doing. In my specific case, the electrodes reconstruction is performed off-site. I receive a text file containing the x y and z coordinates of the patient’s electrode “in T1 space” as well as the free surfer reconstruction. I therefore have access to the T1 that was used for the recon. I am therefore creating the montage as follows:

from collections import OrderedDict
import numpy as np
import mne
from mne_bids import (write_raw_bids, BIDSPath)
subject_id = "sub-XXX"
free_surfer_subjects_dir = "freesurfer_root"
elec_recon_file = "some_text_file.txt"
# Formatting the electrodes reconstruction file:
# Loading the electrodes coordinates in a data frame:
elec_coord_raw = np.genfromtxt(elec_recon_file, dtype=str, delimiter=' ',
                               comments=None, encoding='utf-8')

# Declaring the dict to store the electrodes location:
electrode_tsv = OrderedDict()

# Converting each column to a list:
for i, name in enumerate(['name', 'x', 'y', 'z']):
    electrode_tsv[name] = elec_coord_raw[:, i].tolist()

# Get the channels name
ch_names = electrode_tsv['name']
# load in the xyz coordinates as a float
elec = np.empty(shape=(len(ch_names), 3))
for ind, axis in enumerate(['x', 'y', 'z']):
    elec[:, ind] = list(map(float, electrode_tsv[axis]))
# Converting the elec position to meters as required by the coordinate system
elec = elec / 1000.
# --------------------------------------------------------------------------------------------------------------
# Preparing the raw file for saving:
# Making the montage:

# Making the montage:
montage = mne.channels.make_dig_montage(ch_pos=dict(zip(ch_names, elec)),
                                        coord_frame='mri')
# Adding the fiducials:
montage.add_estimated_fiducials(subject_id, free_surfer_subjects_dir)
# Setting the montage:
raw.set_montage(montage, on_missing='warn')

I have the plotted the electrodes on the brain surface and they looked as expected.

Unfortunately, in the next step of my pipeline, when attempting to save the file to bids using the write_to_bids function, the above solution leads to an error:

# Creating an mne raw object with only the ieeg channels
ieeg_raw = raw.copy().pick_types(ecog=True, seeg=True, ecg=True, emg=True)

# Creating the BIDS path:
bids_path = BIDSPath(subject=subject_id, session=session,
         task=task, datatype=data_type, root=bids_root)
write_raw_bids(ieeg_raw, bids_path, overwrite=True, format='auto')

Resulting error:

ValueError: space (CapTrak) is not valid for datatype (ieeg).
Should be one of ['ICBM452AirSpace', 'ICBM452Warp5Space', 'IXI549Space', 'fsaverage', 'fsaverageSym', 'fsLR', 'MNIColin27', 'MNI152Lin', 'MNI152NLin2009aSym', 'MNI152NLin2009bSym', 'MNI152NLin2009cSym', 'MNI152NLin2009aAsym', 'MNI152NLin2009bAsym', 'MNI152NLin2009cAsym', 'MNI152NLin6Sym', 'MNI152NLin6ASym', 'MNI305', 'NIHPD', 'OASIS30AntsOASISAnts', 'OASIS30Atropos', 'Talairach', 'UNCInfant', 'fsaverage3', 'fsaverage4', 'fsaverage5', 'fsaverage6', 'fsaveragesym', 'UNCInfant0V21', 'UNCInfant1V21', 'UNCInfant2V21', 'UNCInfant0V22', 'UNCInfant1V22', 'UNCInfant2V22', 'UNCInfant0V23', 'UNCInfant1V23', 'UNCInfant2V23', 'Other', 'Pixels', 'ACPC']

I have also attempted to use the other described solution:

 # Making the montage:
 montage = mne.channels.make_dig_montage(ch_pos=dict(zip(ch_names, elec)),
                                                                           coord_frame='head')
# Compute the transformation in mri space:
trans = mne.coreg.estimate_head_mri_t(subject_id, free_surfer_subjects_dir)
# Apply it to the montage:
montage.apply_trans(trans)
# Adding the fiducials:
raw.set_montage(montage, on_missing='warn')

Unfortunately, in that case, the electrodes are misplaced (I am only sharing part of the picture due to data sharing concerns):
image

I am therefore rather confused about the best procedure to follow for this specific case. What also adds to my confusion is the interplay between the different coordinate systems and the different transformation and montage creation functions. Indeed, when using the function estimate_head_mri_t, the montage must have been set in “head” coordinates, whereas for the use of add_estimated_fiducials, the montage must be created in “mri” coordinates.

My overall goal would be to load the electrodes localization files and create the montage in T1 space and then save it to bids to then be able when loading the data from the bids converted dataset with the electrodes localization on the T1, using the functions described in the mne_bids tutorial:

montage = raw.get_montage()
# this uses Freesurfer recon-all subject directory
montage.add_estimated_fiducials('sample_seeg', subjects_dir=subjects_dir)
# now the montage is properly in "head" and ready for analysis in MNE
raw.set_montage(montage)

Perhaps one of the solution would be to use the estimate_head_mri_t function before saving to BIDS and disregard the issue with the electrodes being plotted wrong as this is not the case anymore when using the fiducials? This however does not feel appropriate, as the electrodes localization should be right before and after the bids conversion

My apologies if this has been addressed already in your previous answer or in the tutorials and I am missing it. And thanks a lot again for the help, this is highly appreciated!

Kind regards,

Alex

Dear all,

I have figured it out for my specific purpose. My apologies Alex Rockhill, after trying many different things, it turns out your initial answers already contained all the relevant information.

I was misunderstanding the different coordinates system.

Just in case anyone has a similar question, the pipeline I am currently following is as follows:

  1. Saving the data to BIDS:
    I am creating a montage with my T1 coordinates as follows:
montage = mne.channels.make_dig_montage(ch_pos=dict(zip(ch_names, elec)),
                                                                           coord_frame='mri')

I am then applying the montage to the raw object to save the data to BIDS. I am not compute the fiducial at that stage, as adding the fiducial to the montage transforms the data to a format that throws an error with the mne_bids converter.

  1. Loading the data and creating the montage in T1 space:
    I am then loading my data using the mne_bids function read_raw_bids. But because I am saving both the T1 and the montage in MNI space, I am specifically selecting the T1 montage to obtain the electrodes localization in that space. I do so as follows:
def set_montage(raw, bids_path, montage_space="T1"):
    """
    This function sets the montage on the raw data according to the passed montage space. Natively, mne_bids will
    read electrodes localization in the coordinates that were last saved. But we want to control which one to load,
    which is why this function is used! Accepted montage space: T1 or MNI
    :param raw: (mne raw object) contains the data to which the montage will be added
    :param bids_path: (mne_bids path object) contains path information to find the correct files
    :param montage_space: (string) choose which space you want the montage to be in. Accepted: T1 or MNI
    :return:
    """
    # Handle montage type
    if montage_space.upper() == "T1":
        coord_file = "*space-Other_electrodes"
        coord_frame = "mri"
    elif montage_space.upper() == "MNI":
        coord_file = "*space-fsaverage_electrodes"
        coord_frame = "mni_tal"
    else:
        raise Exception("You have passed a montage space that is not supported. It should be either T1 or MNI! Check "
                        "your config")

    # Loading the coordinate file:
    recon_file = find_files(bids_path.directory, naming_pattern=coord_file, extension=".tsv")
    # Load the file:
    channels_coordinates = pd.read_csv(recon_file[0], sep='\t')  # Loading the coordinates
    # From this file, getting the channels:
    channels = channels_coordinates["name"].tolist()
    # Get the position:
    position = channels_coordinates[["x", "y", "z"]].to_numpy()
    # Create the montage:
    montage = mne.channels.make_dig_montage(ch_pos=dict(zip(channels, position)),
                                            coord_frame=coord_frame)

    # And set the montage on the raw object:
    raw.set_montage(montage, on_missing='warn')

    return raw
  1. Atlas mapping on different atlases:
    I am then using the created montage to estimate the brain regions an electrode is found in based on different atlases:
def roi_mapping(mne_object, list_parcellations, subject_id, fs_dir, step,
                subject_info):
    """
    This function maps the electrodes on different atlases. You can pass whatever atlas you have the corresponding
    free surfer parcellation for.
    :param mne_object: (mne raw or epochs object) object containing the montage to extract the roi
    :param list_parcellations: (list of string) list of the parcellation files to use to do the mapping. Must match the
    naming of the free surfer parcellation files.
    :param subject_id: (string) name of the subject to do access the fs recon
    :param fs_dir: (string or pathlib path object) path to the freesurfer directory containing all the participants
    :param step: (string) name of the step to save the data accordingly
    :param subject_info: (custom object) contains info about the patient
    :return: labels_df: (dict of dataframe) one data frame per parcellation with the mapping between roi and channels
    """
    labels_df = {parcellation: pd.DataFrame() for parcellation in list_parcellations}
    for parcellation in list_parcellations:
        labels, _ = mne.get_montage_volume_labels(
            mne_object.get_montage(), subject_id, subjects_dir=fs_dir, aseg=parcellation)
        # Keeping only one ROI per electrodes, otherwise we will break stats down the line by taking the same elec
        # several times:
        for ind, channel in enumerate(labels.keys()):
            # Keeping the first ROI that is not unknown, because unknown is uninteresting if we have more than one:
            if len(labels[channel]) > 1:
                roi = [roi for roi in labels[channel] if roi != "Unknown"][0]
            elif len(labels[channel]) == 1:
                roi = labels[channel][0]
            else:
                roi = "Unknown"
            labels_df[parcellation] = labels_df[parcellation].append(
                pd.DataFrame({"channel": channel, "region": roi}, index=[ind]))

    # Saving the results. This step is completely independent from what happened on the data:
    save_path = Path(subject_info.participant_save_root, step)
    # Creating the directory if it doesn't exists:
    if not os.path.isdir(save_path):
        # Creating the directory:
        os.makedirs(save_path)
    # Looping through the different mapping:
    for mapping in labels_df.keys():
        file_name = file_name_generator(save_path, subject_info.files_prefix, "elecmapping_" + mapping, ".csv",
                                        data_type="ieeg")
        # Saving the corresponding mapping:
        labels_df[mapping].to_csv(Path(file_name), index=False)

    return labels_df

Note that this step must be performed before estimating the fiducials to plot the electrodes on the patient brain surface, as the get_montage_volume_labels function will fail otherwise.

  1. Plot the electrodes in T1 space:
    This last step plots the electrodes on the subject’s brain surface.
    One needs to first add the fiducials:
def add_fiducials(raw, fs_directory, subject_id):
    """
    This function add the estimated fiducials to the montage and compute the transformation
    :param raw: (mne raw object) containing the montage to plot the electrodes
    :param fs_directory: (string path) path to the free surfer directory
    :param subject_id: (string) name of the subject
    :return:
    """
    montage = raw.get_montage()
    montage.add_estimated_fiducials(subject_id, fs_directory)
    trans = mne.channels.compute_native_head_t(montage)
    raw.set_montage(montage, on_missing="warn")

    return raw, trans

Then, we can plot the electrodes on the brain surface with the trans info:

def plot_electrode_localization(mne_object, subject_info, step_name, subject_id=None, fs_subjects_directory=None,
                                data_type="ieeg", file_extension=".png", channels_to_plot=None, montage_space="T1",
                                trans=None):
    """
    This function plots and saved the psd of the chosen electrodes. 
    :param mne_object: (mne object: raw, epochs, evoked...) contains the mne object with the channels info
    :param subject_info: (custom object) contains info about the subject
    :param step_name: (string) name of the step that this is performed under to save the data
    :param subject_id: (string) name of the subject! Not necessary if you want to plot in mni space
    :param fs_subjects_directory: (string or pathlib path) path to the free surfer subjects directory. Not required if
    you want to plot in mni space
    :param data_type: (string) type of data that are being plotted
    :param file_extension: (string) extension of the pic file for saving
    :param channels_to_plot: (list) contains the different channels to plot. Can pass list of channels types, channels
    indices, channels names...
    :param montage_space: (string)
    :param trans: transformation from estimated fiducials to plot the data
    :return:
    """
    if channels_to_plot is None:
        channels_to_plot = ["ecog", "seeg"]
    if fs_subjects_directory is None and montage_space.lower() != "mni":
        raise Exception("For the electrodes plotting, you didn't pass any free surfer directory yet asked to plot the "
                        "electrodes in T1 space. \nThat doesn't work, you should either plot in MNI space or pass a "
                        "freesurfer dir")
    # If we are plotting the electrodes in MNI space, fetching the data:
    if montage_space.lower() == "mni":
        fs_subjects_directory = mne.datasets.sample.data_path() + '/subjects'
        subject_id = "fsaverage"
        fetch_fsaverage(subjects_dir=fs_subjects_directory, verbose=True)

    # Set the path to where the data should be saved:
    save_path = path_generator(subject_info.participant_save_root, step_name, signal=None,
                               previous_steps_list=None, figure=True)
    brain_snapshot_files = []
    # Setting the two views
    snapshot_orientations = {
        "left": {"focalpoint": [0.01, -0.02, 0.01], "az": 180, "elevation": None},
        "front": {"focalpoint": [0.01, -0.02, 0.01], "az": 90, "elevation": None},
        "right": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": None},
        "back": {"focalpoint": [0.01, -0.02, 0.01], "az": -90, "elevation": None},
        "top": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": 180},
        "bottom": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": -180}
    }
    # We want to plot the seeg and ecog channels separately:
    for ch_type in channels_to_plot:
        data_to_plot = mne_object.copy().pick(ch_type)
        # Plotting the brain surface with the electrodes and making a snapshot
        if montage_space == "T1":
            fig = plot_alignment(data_to_plot.info, subject=subject_id, subjects_dir=fs_subjects_directory,
                                 surfaces=['pial'], coord_frame='mri', trans=trans)
        else:
            fig = plot_alignment(data_to_plot.info, subject=subject_id, subjects_dir=fs_subjects_directory,
                                 surfaces=['pial'], coord_frame='mri')

        for ori in snapshot_orientations.keys():
            full_file_name = file_name_generator(save_path, subject_info.files_prefix,
                                                 "elecloc" + ch_type + "_deg" + ori,
                                                 file_extension,
                                                 data_type=data_type)
            mne.viz.set_3d_view(fig, azimuth=snapshot_orientations[ori]["az"],
                                focalpoint=snapshot_orientations[ori]["focalpoint"],
                                elevation=snapshot_orientations[ori]["elevation"],
                                distance=0.5)
            xy, im = snapshot_brain_montage(fig, mne_object.info, hide_sensors=False)
            fig_2, ax = plt.subplots(figsize=(10, 10))
            ax.imshow(im)
            ax.set_axis_off()
            plt.savefig(full_file_name, transparent=True)
            plt.close()
            brain_snapshot_files.append(full_file_name)

    # Closing all the 3d plots
    mne.viz.backends.renderer.backend._close_all()

    # Adding the images to the patients report:
    subject_info.pdf_writer.add_image(brain_snapshot_files, title="Patients electrode localization")

    return None

As Alex had mentioned, this information is already found in the mne doc, here and here
Thanks for the support everyone :slight_smile: , I wouldn’t have figured it out without the help!

Kind regards,

Alex

Hi, so you plot electrodes with MNI coordinates using specific subject’s brain?

Hi Barry,

Not quite, that depends on how I set up my scripts. I have added both options here to show everything, but that might be a bit nontransparent.

As an output of my electrode localization, I have the coordinates both in T1 space and in common average space (MNI). When I only look at single subjects, I plot the electrodes in recon in T1 space on the subject’s brain. When I look at several subjects combined, I plot their electrodes in MNI space on the fsaverage surface. For the electrodes mapping onto atlases, I am using the T1 localization with parcellation on the subject’s brain as well, for maximal accuracy.

I am usually calling these functions consecutively from master scripts. For my preprocessing for example, where I only use T1 space, the pipeline would look something like this:

from mne_bids import BIDSPath, read_raw_bids
import mne
import matplotlib.pyplot as plt
import pandas as pd
from mne.viz import plot_alignment, snapshot_brain_montage
from mne.datasets import fetch_fsaverage
import glob
import os
import numpy as np


def find_files(root, naming_pattern=None, extension=None):
    """
    This function finds files matching a specific naming pattern recursively in directory from root
    :param root: root of the directory among which to search. Must be a string
    :param naming_pattern: string the files must match to be returned
    :param extension: Extension of the file to search for
    :return: list of matching files
    """
    if extension is None:
        extension = '.*'
    if naming_pattern is None:
        naming_pattern = '*'

    matches = []
    for sub_folder, dirnames, filenames in os.walk(root):
        for filename in glob.glob(sub_folder + os.sep + '*' + naming_pattern + '*' + extension):
            matches.append(os.path.join(sub_folder, filename))
    # Getting the file names:
    matches_file_names = [file.split(os.sep)[-1] for file in matches]
    files_order = np.argsort(matches_file_names)
    matches = [matches[ind] for ind in files_order]
    [print(match) for match in matches]

    return matches


def plot_electrode_localization(mne_object, subject_id=None, fs_subjects_directory=None,
                                channels_to_plot=None, montage_space="T1",
                                trans=None, save_path=""):
    """
    This function plots and saved the localization of the chosen electrodes types.
    :param mne_object: (mne object: raw, epochs, evoked...) contains the mne object with the channels info
    :param subject_id: (string) name of the subject! Not necessary if you want to plot in mni space
    :param fs_subjects_directory: (string or pathlib path) path to the free surfer subjects directory. Not required if
    you want to plot in mni space
    :param channels_to_plot: (list) contains the different channels to plot. Can pass list of channels types, channels
    indices, channels names...
    :param montage_space: (string)
    :param trans: transformation from estimated fiducials to plot the data
    :param save_path: (string) path to where the data should be saved
    :return:
    """
    if channels_to_plot is None:
        channels_to_plot = ["ecog", "seeg"]
    if fs_subjects_directory is None and montage_space.lower() != "mni":
        raise Exception("For the electrodes plotting, you didn't pass any free surfer directory yet asked to plot the "
                        "electrodes in T1 space. \nThat doesn't work, you should either plot in MNI space or pass a "
                        "freesurfer dir")
    # If we are plotting the electrodes in MNI space, fetching the data:
    if montage_space.lower() == "mni":
        fs_subjects_directory = mne.datasets.sample.data_path() + '/subjects'
        subject_id = "fsaverage"
        fetch_fsaverage(subjects_dir=fs_subjects_directory, verbose=True)

    # Setting the two views
    snapshot_orientations = {
        "left": {"focalpoint": [0.01, -0.02, 0.01], "az": 180, "elevation": None},
        "front": {"focalpoint": [0.01, -0.02, 0.01], "az": 90, "elevation": None},
        "right": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": None},
        "back": {"focalpoint": [0.01, -0.02, 0.01], "az": -90, "elevation": None},
        "top": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": 180},
        "bottom": {"focalpoint": [0.01, -0.02, 0.01], "az": 0, "elevation": -180}
    }
    # We want to plot the seeg and ecog channels separately:
    for ch_type in channels_to_plot:
        data_to_plot = mne_object.copy().pick(ch_type)
        # Plotting the brain surface with the electrodes and making a snapshot
        if montage_space == "T1":
            fig = plot_alignment(data_to_plot.info, subject=subject_id, subjects_dir=fs_subjects_directory,
                                 surfaces=['pial'], coord_frame='mri', trans=trans)
        else:
            fig = plot_alignment(data_to_plot.info, subject=subject_id, subjects_dir=fs_subjects_directory,
                                 surfaces=['pial'], coord_frame='mri')

        for ori in snapshot_orientations.keys():
            mne.viz.set_3d_view(fig, azimuth=snapshot_orientations[ori]["az"],
                                focalpoint=snapshot_orientations[ori]["focalpoint"],
                                elevation=snapshot_orientations[ori]["elevation"],
                                distance=0.5)
            xy, im = snapshot_brain_montage(fig, mne_object.info, hide_sensors=False)
            fig_2, ax = plt.subplots(figsize=(10, 10))
            ax.imshow(im)
            ax.set_axis_off()
            # Save the data:
            plt.close()

    # Closing all the 3d plots
    mne.viz.backends.renderer.backend._close_all()

    return None


def add_fiducials(raw, fs_directory, subject_id):
    """
    This function add the estimated fiducials to the montage and compute the transformation
    :param raw: (mne raw object) containing the montage to plot the electrodes
    :param fs_directory: (string path) path to the free surfer directory
    :param subject_id: (string) name of the subject
    :return:
    """
    montage = raw.get_montage()
    montage.add_estimated_fiducials(subject_id, fs_directory)
    trans = mne.channels.compute_native_head_t(montage)
    raw.set_montage(montage, on_missing="warn")

    return raw, trans


def set_montage(raw, bids_path, montage_space="T1"):
    """
    This function sets the montage on the raw data according to the passed montage space. Natively, mne_bids will
    read electrodes localization in the coordinates that were last saved. But we want to control which one to load,
    which is why this function is used! Accepted montage space: T1 or MNI
    :param raw: (mne raw object) contains the data to which the montage will be added
    :param bids_path: (mne_bids path object) contains path information to find the correct files
    :param montage_space: (string) choose which space you want the montage to be in. Accepted: T1 or MNI
    :return:
    """
    # Handle montage type
    if montage_space.upper() == "T1":
        coord_file = "*space-Other_electrodes"
        coord_frame = "mri"
    elif montage_space.upper() == "MNI":
        coord_file = "*space-fsaverage_electrodes"
        coord_frame = "mni_tal"
    else:
        raise Exception("You have passed a montage space that is not supported. It should be either T1 or MNI! Check "
                        "your config")

    # Loading the coordinate file:
    recon_file = find_files(bids_path.directory, naming_pattern=coord_file, extension=".tsv")
    # Load the file:
    channels_coordinates = pd.read_csv(recon_file[0], sep='\t')  # Loading the coordinates
    # From this file, getting the channels:
    channels = channels_coordinates["name"].tolist()
    # Get the position:
    position = channels_coordinates[["x", "y", "z"]].to_numpy()
    # Create the montage:
    montage = mne.channels.make_dig_montage(ch_pos=dict(zip(channels, position)),
                                            coord_frame=coord_frame)

    # And set the montage on the raw object:
    raw.set_montage(montage, on_missing='warn')

    return raw


def roi_mapping(mne_object, list_parcellations, subject_id, fs_dir):
    """
    This function maps the electrodes on different atlases. You can pass whatever atlas you have the corresponding
    free surfer parcellation for.
    :param mne_object: (mne raw or epochs object) object containing the montage to extract the roi
    :param list_parcellations: (list of string) list of the parcellation files to use to do the mapping. Must match the
    naming of the free surfer parcellation files.
    :param subject_id: (string) name of the subject to do access the fs recon
    :param fs_dir: (string or pathlib path object) path to the freesurfer directory containing all the participants
    :return: labels_df: (dict of dataframe) one data frame per parcellation with the mapping between roi and channels
    """
    labels_df = {parcellation: pd.DataFrame() for parcellation in list_parcellations}
    for parcellation in list_parcellations:
        labels, _ = mne.get_montage_volume_labels(mne_object.get_montage(), subject_id,
                                                  subjects_dir=fs_dir, aseg=parcellation)
        # Keeping only one ROI per electrodes, otherwise we will break stats down the line by taking the same elec
        # several times:
        for ind, channel in enumerate(labels.keys()):
            # Keeping the first ROI that is not unknown, because unknown is uninteresting if we have more than one:
            if len(labels[channel]) > 1:
                roi = [roi for roi in labels[channel] if roi != "Unknown"][0]
            elif len(labels[channel]) == 1:
                roi = labels[channel][0]
            else:
                roi = "Unknown"
            labels_df[parcellation] = labels_df[parcellation].append(
                pd.DataFrame({"channel": channel, "region": roi}, index=[ind]))

    return labels_df


def preprocessing(bids_root=None, subject=None, session=None, task=None, electrodes_space=None, parcellation=None,
                  fs_dir=None):
    # Creating the bids path
    bids_path = BIDSPath(root=bids_root, subject=subject,
                         session=session,
                         datatype='ieeg',
                         task=task)
    # Loading the raw:
    raw = read_raw_bids(bids_path=bids_path, verbose=True)

    # Creating the appropriate montage:
    raw = set_montage(raw["broadband"], bids_path,
                      montage_space=electrodes_space)

    # Performing atlas mapping:
    electrodes_mapping_df = \
        roi_mapping(raw, parcellation, subject, fs_dir)
    # Save to csv here:

    # Adding the fiducials to the montage:
    raw, trans = add_fiducials(raw, fs_dir, subject)

    # Plotting the electrodes localization:
    plot_electrode_localization(raw, subject_id=subject, fs_subjects_directory=fs_dir, montage_space=electrodes_space,
                                trans=trans, save_path="")


if __name__ == "__main__":
    preprocessing(bids_root="", subject="sub-101", session="V1", task="A", electrodes_space="T1",
                  parcellation=["parc+aseg"], fs_dir="")

So I set the electrode space to be T1, and therefore the electrodes coordinate in T1 space will be loaded and then plotted on the patient’s brain.

I hope this helps,

Kind regards,

Alex

Hi, T1 means “mri” coordinnate frame with ACPC alignment, do I understand that right?

1 Like