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:
- 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.
- 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
- 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.
- 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 , I wouldn’t have figured it out without the help!
Kind regards,
Alex