From 344685dd014665f82e48529dfc69a5bd94a21e4a Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Sun, 6 Apr 2025 13:01:33 -0400 Subject: [PATCH 01/23] Create atlas and coordinate_space #241 --- naplib/localization/freesurfer.py | 90 +++++++++++++++++++------------ 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 72088aa..a1ab3b0 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -171,6 +171,8 @@ def __init__( hemi: str, surf_type: str = "pial", subject: str = "fsaverage", + coordinate_space: str = 'FSAverage', + atlas: str = '', subject_dir=None, ): """ @@ -193,41 +195,47 @@ def __init__( raise ValueError(f"Argument `hemi` should be in {HEMIS}.") if surf_type not in SURF_TYPES: raise ValueError(f"Argument `surf_type` should be in {SURF_TYPES}.") + if not atlas: + if coordinate_space == 'FSAverage': + atlas = 'Destrieux' + # Use DK for MNI152 or any other + else: + atlas = 'Desikan-Killiany' self.hemi = hemi self.surf_type = surf_type self.subject = subject + self.coordinate_space = coordinate_space + self.atlas = atlas if subject_dir is None: subject_dir = os.environ.get("SUBJECTS_DIR", "./") self.subject_dir = subject_dir - - self.atlas = 'FSAverage' - if os.path.exists(self.surf_file(f"{hemi}.{surf_type}")): - self.surf = read_geometry(self.surf_file(f"{hemi}.{surf_type}")) - else: + # Check if fsaverage geometry exists + if self.coordinate_space == 'FSAverage': + if os.path.exists(self.surf_file(f"{hemi}.{surf_type}")): + self.surf = read_geometry(self.surf_file(f"{hemi}.{surf_type}")) + self.surf_pial = read_geometry(self.surf_file(f"{hemi}.pial")) + else: + self.coordinate_space = 'MNI152' + print('Trying MNI152 coordinate space') + # Use MN152 coordinate space if not + if self.coordinate_space == 'MNI152': # try to find .mat file surf_ = loadmat(self.surf_file(f"{hemi}_pial.mat")) coords, faces = surf_['coords'], surf_['faces'] faces -= 1 # make faces zero-indexed self.surf = (coords, faces) - self.atlas = 'MNI152' - - if self.atlas == 'FSAverage': - self.surf_pial = read_geometry(self.surf_file(f"{hemi}.pial")) - else: - # try to find .mat file - surf_ = loadmat(self.surf_file(f"{hemi}_pial.mat")) - coords, faces = surf_['coords'], surf_['faces'] - faces -= 1 # make faces zero-indexed self.surf_pial = (coords, faces) + else: + raise ValueError(f"Argument `coordinate_space`={self.coordinate_space} not implemented.") try: self.cort = np.sort(read_label(self.label_file(f"{hemi}.cortex.label"))) except Exception as e: - logger.warning(f'No {hemi}.cortext.label file found. Assuming the entire surface is cortex.') + logger.warning(f'No {hemi}.cortex.label file found. Assuming the entire surface is cortex.') self.cort = np.arange(self.surf[0].shape[0]) try: @@ -286,23 +294,23 @@ def load_labels(self): self.labels = np.zeros(self.n_verts, dtype=int) annot_file = self.label_file(f"{self.hemi}.aparc.a2009s.annot") annot_file_mni = self.label_file(f"FSL_MNI152.{self.hemi}.aparc.split_STG_MTG.annot") - if self.atlas == 'FSAverage': + if self.coordinate_space == 'FSAverage': for ind, reg in num2region.items(): if reg.startswith("O"): continue self.labels[load_freesurfer_label(annot_file, reg)] = ind - elif self.atlas == 'MNI152': + elif self.coordinate_space == 'MNI152': for ind, reg in num2region_mni.items(): if reg.startswith("O"): continue self.labels[load_freesurfer_label(annot_file_mni, reg)] = ind else: - raise ValueError('Bad atlas') + raise ValueError('Bad coordinate space') self.labels[self.ignore] = 0 - if self.atlas == 'FSAverage': + if self.coordinate_space == 'FSAverage': self.num2label = num2region self.label2num = {v: k for k, v in self.num2label.items()} - elif self.atlas == 'MNI152': + elif self.coordinate_space == 'MNI152': self.num2label = num2region_mni self.label2num = {v: k for k, v in self.num2label.items()} else: @@ -325,7 +333,7 @@ def simplify_labels(self): ------- self : instance of self """ - if self.atlas == 'FSAverage': + if self.coordinate_space == 'FSAverage': conversions = { "Other": [], # Autofill all uncovered vertecies "HG": ["G_temp_sup-G_T_transv"], @@ -353,7 +361,7 @@ def simplify_labels(self): } - elif self.atlas == 'MNI152': + elif self.coordinate_space == 'MNI152': d1 = {k: [k] for k in region2num_mni.keys() if k not in ['O_IFG','parsopercularis','parstriangularis','parsorbitalis']} d2_override = { "Other": [], @@ -502,8 +510,8 @@ def split_hg(self, method="midpoint"): ) self.is_mangled_hg = True - if self.atlas == 'MNI152': - raise ValueError(f'split_hg() is not supported for MNI atlas.') + if self.coordinate_space == 'MNI152': + raise ValueError(f'split_hg() is not supported for MNI coordinate space.') hg = self.filter_labels(["G_temp_sup-G_T_transv", "HG"]) @@ -621,8 +629,8 @@ def remove_tts(self, method="split"): ------- self : instance of self """ - if self.atlas == 'MNI152': - raise ValueError(f'remove_tts() is not supported for MNI atlas.') + if self.coordinate_space == 'MNI152': + raise ValueError(f'remove_tts() is not supported for MNI coordinate space.') if self.is_mangled_tts: raise RuntimeError( @@ -679,8 +687,8 @@ def split_stg(self, method="tts_plane"): ------- self : instance of self """ - if self.atlas == 'MNI152': - raise ValueError(f'split_stg() is not supported for MNI atlas.') + if self.coordinate_space == 'MNI152': + raise ValueError(f'split_stg() is not supported for MNI coordinate space.') if self.is_mangled_stg: raise RuntimeError( @@ -723,7 +731,7 @@ def join_ifg(self): ) self.is_mangled_ifg = True - if self.atlas == 'FSAverage': + if self.coordinate_space == 'FSAverage': ifg = self.filter_labels( [ "G_front_inf-Opercular", @@ -734,7 +742,7 @@ def join_ifg(self): "IFG.orb", ] ) - else: # MNI152 + elif self.coordinate_space == 'MNI152': ifg = self.filter_labels( [ "parsopercularis", @@ -745,6 +753,9 @@ def join_ifg(self): "IFG.orb", ] ) + else: + print('No change for coordinate space', self.coordinate_space) + return self self.labels[ifg] = self.label2num["IFG" if self.simplified else "O_IFG"] @@ -801,7 +812,7 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al if isinstance(roi, str) and roi == 'all': roi_list = self.label_names elif isinstance(roi, str) and roi == 'temporal': - if self.atlas == 'MNI152': + if self.coordinate_space == 'MNI152': raise ValueError("roi='temporal' is not supported for MNI brain. Must specify list of specific region names") roi_list = temporal_regions_superlist else: @@ -894,7 +905,12 @@ def reset_overlay_except(self, labels): class Brain: def __init__( - self, surf_type: str = "pial", subject: str = "fsaverage", subject_dir=None + self, + surf_type: str = "pial", + subject: str = "fsaverage", + coordinate_space: str = 'FSAverage', + atlas: str = '', + subject_dir=None ): """ Brain representation containing a left and right hemisphere. Can be used for plotting, @@ -909,6 +925,12 @@ def __init__( files can be found. subject : str, default='fsaverage' Subject to use, must be a directory within ``subject_dir`` + coordinate_space : str, default='FSAverage' + Coordinate space, used to determine surface geometry + atlas : str, default='' + Atlas labels to use. Defaults to 'Destrieux' for coordinate_space='FSAverage' + and Desikan-Killiany otherwise. Can also be an annotation file name given by + ``{subject_dir}/{subject}/label/?h.{atlas}.annot`` subject_dir : str/path-like, defaults to SUBJECT_DIR environment variable, or the current directory if that does not exist. Path containing the subject's folder. @@ -942,8 +964,8 @@ def __init__( self.surf_type = surf_type self.subject = subject - self.lh = Hemisphere("lh", surf_type, subject, subject_dir=subject_dir) - self.rh = Hemisphere("rh", surf_type, subject, subject_dir=subject_dir) + self.lh = Hemisphere("lh", surf_type, subject, coordinate_space, atlas, subject_dir=subject_dir) + self.rh = Hemisphere("rh", surf_type, subject, coordinate_space, atlas, subject_dir=subject_dir) @property def num2label(self): From b7046fd3ce09a845dcdf578dfca08e52a87f9638 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Sun, 6 Apr 2025 13:17:23 -0400 Subject: [PATCH 02/23] Updated label loader #214 Created structure for loading different labels based on coordinate space and atlas. Still need to construct num2region dictionaries from relevant data --- naplib/localization/freesurfer.py | 46 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index a1ab3b0..11471f4 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -292,29 +292,43 @@ def load_labels(self): self.ignore[load_freesurfer_label(annot_file, reg)] = True self.labels = np.zeros(self.n_verts, dtype=int) - annot_file = self.label_file(f"{self.hemi}.aparc.a2009s.annot") - annot_file_mni = self.label_file(f"FSL_MNI152.{self.hemi}.aparc.split_STG_MTG.annot") - if self.coordinate_space == 'FSAverage': + + if self.coordinate_space == 'MNI152': + if self.atlas == 'Desikan-Killiany': + annot_file = self.label_file(f"FSL_MNI152.{self.hemi}.aparc.split_STG_MTG.annot") + else: + annot_file = self.label_file(f'{self.hemi}.{self.atlas}.annot') + + if os.path.exists(annot_file): + for ind, reg in num2region_mni.items(): + if reg.startswith("O"): + continue + self.labels[load_freesurfer_label(annot_file, reg)] = ind + + self.labels[self.ignore] = 0 + self.num2label = num2region_mni + self.label2num = {v: k for k, v in self.num2label.items()} + else: + ano + if os.path.exists() + raise ValueError('Unknown atlas for MNI152. Try a custom atlas.') + elif self.coordinate_space == "FSAverage": + if self.atlas == 'Desikan-Killiany': + annot_file = self.label_file(f"{self.hemi}.aparc.annot") + elif self.atlas == 'Destrieux': + annot_file = self.label_file(f"{self.hemi}.aparc.a2009s.annot") + else: + annot_file = self.label_file(f'{self.hemi}.{self.atlas}.annot') for ind, reg in num2region.items(): if reg.startswith("O"): continue self.labels[load_freesurfer_label(annot_file, reg)] = ind - elif self.coordinate_space == 'MNI152': - for ind, reg in num2region_mni.items(): - if reg.startswith("O"): - continue - self.labels[load_freesurfer_label(annot_file_mni, reg)] = ind - else: - raise ValueError('Bad coordinate space') - self.labels[self.ignore] = 0 - if self.coordinate_space == 'FSAverage': + + self.labels[self.ignore] = 0 self.num2label = num2region self.label2num = {v: k for k, v in self.num2label.items()} - elif self.coordinate_space == 'MNI152': - self.num2label = num2region_mni - self.label2num = {v: k for k, v in self.num2label.items()} else: - raise ValueError('Bad atlas') + raise ValueError('Bad coordinate space') self.simplified = False From fdc97969099a843277adf95233309a8c496e330e Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:33:01 -0400 Subject: [PATCH 03/23] General atlas loader #214 While retaining custom labels for specific scenarios --- naplib/localization/freesurfer.py | 179 +++++------------------------- 1 file changed, 26 insertions(+), 153 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 11471f4..a53b908 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -20,85 +20,7 @@ HEMIS = ("lh", "rh") SURF_TYPES = ("pial", "inflated") - -num2region = { - # Unknown - 0: "Unknown", - # Destrieux labels - 1: "G_and_S_frontomargin", - 2: "G_and_S_occipital_inf", - 3: "G_and_S_paracentral", - 4: "G_and_S_subcentral", - 5: "G_and_S_transv_frontopol", - 6: "G_and_S_cingul-Ant", - 7: "G_and_S_cingul-Mid-Ant", - 8: "G_and_S_cingul-Mid-Post", - 9: "G_cingul-Post-dorsal", - 10: "G_cingul-Post-ventral", - 11: "G_cuneus", - 12: "G_front_inf-Opercular", - 13: "G_front_inf-Orbital", - 14: "G_front_inf-Triangul", - 15: "G_front_middle", - 16: "G_front_sup", - 17: "G_Ins_lg_and_S_cent_ins", - 18: "G_insular_short", - 19: "G_occipital_middle", - 20: "G_occipital_sup", - 21: "G_oc-temp_lat-fusifor", - 22: "G_oc-temp_med-Lingual", - 23: "G_oc-temp_med-Parahip", - 24: "G_orbital", - 25: "G_pariet_inf-Angular", - 26: "G_pariet_inf-Supramar", - 27: "G_parietal_sup", - 28: "G_postcentral", - 29: "G_precentral", - 30: "G_precuneus", - 31: "G_rectus", - 32: "G_subcallosal", - 33: "G_temp_sup-G_T_transv", - 34: "G_temp_sup-Lateral", - 35: "G_temp_sup-Plan_polar", - 36: "G_temp_sup-Plan_tempo", - 37: "G_temporal_inf", - 38: "G_temporal_middle", - 39: "Lat_Fis-ant-Horizont", - 40: "Lat_Fis-ant-Vertical", - 41: "Lat_Fis-post", - 42: "Pole_occipital", - 43: "Pole_temporal", - 44: "S_calcarine", - 45: "S_central", - 46: "S_cingul-Marginalis", - 47: "S_circular_insula_ant", - 48: "S_circular_insula_inf", - 49: "S_circular_insula_sup", - 50: "S_collat_transv_ant", - 51: "S_collat_transv_post", - 52: "S_front_inf", - 53: "S_front_middle", - 54: "S_front_sup", - 55: "S_interm_prim-Jensen", - 56: "S_intrapariet_and_P_trans", - 57: "S_oc_middle_and_Lunatus", - 58: "S_oc_sup_and_transversal", - 59: "S_occipital_ant", - 60: "S_oc-temp_lat", - 61: "S_oc-temp_med_and_Lingual", - 62: "S_orbital_lateral", - 63: "S_orbital_med-olfact", - 64: "S_orbital-H_Shaped", - 65: "S_parieto_occipital", - 66: "S_pericallosal", - 67: "S_postcentral", - 68: "S_precentral-inf-part", - 69: "S_precentral-sup-part", - 70: "S_suborbital", - 71: "S_subparietal", - 72: "S_temporal_inf", - 73: "S_temporal_sup", - 74: "S_temporal_transverse", +num2region_D_custom = { # My custom labels 75: "O_pmHG", 76: "O_alHG", @@ -109,61 +31,11 @@ 81: "O_pSTG", 82: "O_IFG", } -region2num = {v: k for k, v in num2region.items()} - -temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 75, 76, 77, 78, 79, 80, 81] -temporal_regions_superlist = [num2region[num] for num in temporal_regions_nums] -temporal_regions_superlist += ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] - - -num2region_mni = { - 0: 'unknown', - 1: 'bankssts', - 2: 'caudalanteriorcingulate', - 3: 'caudalmiddlefrontal', - 4: 'corpuscallosum', - 5: 'cuneus', - 6: 'entorhinal', - 7: 'fusiform', - 8: 'inferiorparietal', - 9: 'inferiortemporal', - 10: 'isthmuscingulate', - 11: 'lateraloccipital', - 12: 'lateralorbitofrontal', - 13: 'lingual', - 14: 'medialorbitofrontal', - 15: 'middletemporal', - 16: 'parahippocampal', - 17: 'paracentral', - 18: 'parsopercularis', - 19: 'parsorbitalis', - 20: 'parstriangularis', - 21: 'pericalcarine', - 22: 'postcentral', - 23: 'posteriorcingulate', - 24: 'precentral', - 25: 'precuneus', - 26: 'rostralanteriorcingulate', - 27: 'rostralmiddlefrontal', - 28: 'superiorfrontal', - 29: 'superiorparietal', - 30: 'superiortemporal', - 31: 'supramarginal', - 32: 'frontalpole', - 33: 'temporalpole', - 34: 'transversetemporal', - 35: 'insula', - 36: 'cMTG', - 37: 'mMTG', - 38: 'rMTG', - 39: 'cSTG', - 40: 'mSTG', - 41: 'rSTG', + +num2region_DK_custom = { # My custom labels 42: "O_IFG", } -region2num_mni = {v: k for k, v in num2region_mni.items()} - class Hemisphere: def __init__( @@ -299,19 +171,6 @@ def load_labels(self): else: annot_file = self.label_file(f'{self.hemi}.{self.atlas}.annot') - if os.path.exists(annot_file): - for ind, reg in num2region_mni.items(): - if reg.startswith("O"): - continue - self.labels[load_freesurfer_label(annot_file, reg)] = ind - - self.labels[self.ignore] = 0 - self.num2label = num2region_mni - self.label2num = {v: k for k, v in self.num2label.items()} - else: - ano - if os.path.exists() - raise ValueError('Unknown atlas for MNI152. Try a custom atlas.') elif self.coordinate_space == "FSAverage": if self.atlas == 'Desikan-Killiany': annot_file = self.label_file(f"{self.hemi}.aparc.annot") @@ -319,17 +178,29 @@ def load_labels(self): annot_file = self.label_file(f"{self.hemi}.aparc.a2009s.annot") else: annot_file = self.label_file(f'{self.hemi}.{self.atlas}.annot') - for ind, reg in num2region.items(): - if reg.startswith("O"): - continue - self.labels[load_freesurfer_label(annot_file, reg)] = ind - - self.labels[self.ignore] = 0 - self.num2label = num2region - self.label2num = {v: k for k, v in self.num2label.items()} + else: raise ValueError('Bad coordinate space') + if os.path.exists(annot_file): + _,_,regions = read_annot(annot_file) + regions = [i.decode("utf-8") for i in regions] + num2region = {k:v for k,v in enumerate(regions)} + + for ind, reg in num2region.items(): + self.labels[load_freesurfer_label(annot_file, reg)] = ind + else: + raise ValueError('Unknown atlas. Try "Desikan-Killiany" or "Destrieux".') + + if self.atlas == 'Destrieux': + num2region.update(num2region_D_custom) + elif self.atlas == 'Desikan-Killiany' and self.coordinate_space == 'MNI152': + num2region.update(num2region_DK_custom) + + self.labels[self.ignore] = 0 + self.num2label = num2region + self.label2num = {v: k for k, v in self.num2label.items()} + self.simplified = False self.is_mangled_hg = False @@ -828,7 +699,9 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al elif isinstance(roi, str) and roi == 'temporal': if self.coordinate_space == 'MNI152': raise ValueError("roi='temporal' is not supported for MNI brain. Must specify list of specific region names") - roi_list = temporal_regions_superlist + temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 75, 76, 77, 78, 79, 80, 81] + roi_list = [self.num2label[num] for num in temporal_regions_nums] + roi_list += ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] else: roi_list = roi assert isinstance(roi, list) From 0a9ebe1a43786afad392104301535f6e5969cd38 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:48:04 -0400 Subject: [PATCH 04/23] Refactor more brain functions #214 Making the existing brain functions play nice with the new refactor --- naplib/localization/freesurfer.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index a53b908..5047ed1 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -212,13 +212,13 @@ def load_labels(self): def simplify_labels(self): """ - Simplify Destrieux labels into shortforms. + Simplify Destrieux and Desikan-Killiany labels into shortforms. Returns ------- self : instance of self """ - if self.coordinate_space == 'FSAverage': + if self.atlas == 'Destrieux': conversions = { "Other": [], # Autofill all uncovered vertecies "HG": ["G_temp_sup-G_T_transv"], @@ -246,8 +246,8 @@ def simplify_labels(self): } - elif self.coordinate_space == 'MNI152': - d1 = {k: [k] for k in region2num_mni.keys() if k not in ['O_IFG','parsopercularis','parstriangularis','parsorbitalis']} + elif self.atlas == 'Desikan-Killiany': + d1 = {k: [k] for k in self.label2num.keys() if k not in ['O_IFG','parsopercularis','parstriangularis','parsorbitalis']} d2_override = { "Other": [], "IFG": ["O_IFG"], @@ -395,8 +395,8 @@ def split_hg(self, method="midpoint"): ) self.is_mangled_hg = True - if self.coordinate_space == 'MNI152': - raise ValueError(f'split_hg() is not supported for MNI coordinate space.') + if self.atlas != 'Destrieux': + raise ValueError(f'split_hg() only supported for Destrieux atlas.') hg = self.filter_labels(["G_temp_sup-G_T_transv", "HG"]) @@ -514,8 +514,8 @@ def remove_tts(self, method="split"): ------- self : instance of self """ - if self.coordinate_space == 'MNI152': - raise ValueError(f'remove_tts() is not supported for MNI coordinate space.') + if self.atlas != 'Destrieux': + raise ValueError(f'remove_tts() only supported for Destrieux atlas.') if self.is_mangled_tts: raise RuntimeError( @@ -572,8 +572,8 @@ def split_stg(self, method="tts_plane"): ------- self : instance of self """ - if self.coordinate_space == 'MNI152': - raise ValueError(f'split_stg() is not supported for MNI coordinate space.') + if self.atlas != 'Destrieux': + raise ValueError(f'split_stg() only supported for Destrieux atlas.') if self.is_mangled_stg: raise RuntimeError( @@ -616,7 +616,7 @@ def join_ifg(self): ) self.is_mangled_ifg = True - if self.coordinate_space == 'FSAverage': + if self.atlas == 'Destrieux': ifg = self.filter_labels( [ "G_front_inf-Opercular", @@ -627,7 +627,7 @@ def join_ifg(self): "IFG.orb", ] ) - elif self.coordinate_space == 'MNI152': + elif self.atlas == 'Desikan-Killiany': ifg = self.filter_labels( [ "parsopercularis", @@ -697,8 +697,8 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al if isinstance(roi, str) and roi == 'all': roi_list = self.label_names elif isinstance(roi, str) and roi == 'temporal': - if self.coordinate_space == 'MNI152': - raise ValueError("roi='temporal' is not supported for MNI brain. Must specify list of specific region names") + if self.atlas != 'Destrieux': + raise ValueError("roi='temporal' only supported for Destrieux atlas. Must specify list of specific region names") temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 75, 76, 77, 78, 79, 80, 81] roi_list = [self.num2label[num] for num in temporal_regions_nums] roi_list += ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] From f1756343c9779a979a515fafc5b1cc61d7e02326 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:02:29 -0400 Subject: [PATCH 05/23] Coordinate space, atlas docstrings --- naplib/localization/freesurfer.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 5047ed1..de0b713 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -59,6 +59,12 @@ def __init__( files can be found. subject : str, default='fsaverage' Subject to use, must be a directory within ``subject_dir`` + coordinate_space : str, default='FSAverage' + Coordinate space of brain vertices. Must be 'FSAverage' or 'MNI152' + atlas : str, default='' + Atlas for brain parcellation. Defaults to 'Destrieux' for coordinate_space='FSAverage' + and 'Desikan-Killiany' for 'MNI152'. Can also be an annotation file name given by + ``{subject_dir}/{subject}/label/?h.{atlas}.annot`` subject_dir : str/path-like, defaults to SUBJECT_DIR environment variable, or the current directory if that does not exist. Path containing the subject's folder. @@ -805,18 +811,16 @@ def __init__( Parameters ---------- - hemi : str - Either 'lh' or 'rh'. surf_type : str, default='pial' Cortical surface type, either 'pial' or 'inflated' or another if the corresponding files can be found. subject : str, default='fsaverage' Subject to use, must be a directory within ``subject_dir`` coordinate_space : str, default='FSAverage' - Coordinate space, used to determine surface geometry + Coordinate space of brain vertices. Must be 'FSAverage' or 'MNI152' atlas : str, default='' - Atlas labels to use. Defaults to 'Destrieux' for coordinate_space='FSAverage' - and Desikan-Killiany otherwise. Can also be an annotation file name given by + Atlas for brain parcellation. Defaults to 'Destrieux' for coordinate_space='FSAverage' + and 'Desikan-Killiany' for 'MNI152'. Can also be an annotation file name given by ``{subject_dir}/{subject}/label/?h.{atlas}.annot`` subject_dir : str/path-like, defaults to SUBJECT_DIR environment variable, or the current directory if that does not exist. From c7712355a55d0a0e671f1b551106874b1fd45599 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:08:38 -0400 Subject: [PATCH 06/23] Annotation import --- naplib/localization/freesurfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index de0b713..fc569f9 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -7,7 +7,7 @@ from os.path import join as pjoin import numpy as np -from nibabel.freesurfer.io import read_geometry, read_label, read_morph_data +from nibabel.freesurfer.io import read_geometry, read_label, read_morph_data, read_annot from scipy.spatial.distance import cdist from skspatial.objects import Line, Plane from hdf5storage import loadmat From c2caed48408b7007863188f78734f59b5202965e Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:17:26 -0400 Subject: [PATCH 07/23] Verify coordinate space --- naplib/localization/freesurfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index fc569f9..a5a2b4f 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -107,7 +107,7 @@ def __init__( faces -= 1 # make faces zero-indexed self.surf = (coords, faces) self.surf_pial = (coords, faces) - else: + if self.coordinate_space not in ['FSAverage','MNI152']: raise ValueError(f"Argument `coordinate_space`={self.coordinate_space} not implemented.") try: From 131cfa275d3fcd292389ca7eff7cc7157a06a465 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Mon, 7 Apr 2025 23:11:36 -0400 Subject: [PATCH 08/23] Channel offset --- naplib/localization/freesurfer.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index a5a2b4f..98a7275 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -22,14 +22,14 @@ num2region_D_custom = { # My custom labels - 75: "O_pmHG", - 76: "O_alHG", - 77: "O_Te10", - 78: "O_Te11", - 79: "O_Te12", - 80: "O_mSTG", - 81: "O_pSTG", - 82: "O_IFG", + 76: "O_pmHG", + 77: "O_alHG", + 78: "O_Te10", + 79: "O_Te11", + 80: "O_Te12", + 81: "O_mSTG", + 82: "O_pSTG", + 83: "O_IFG", } num2region_DK_custom = { @@ -196,7 +196,7 @@ def load_labels(self): for ind, reg in num2region.items(): self.labels[load_freesurfer_label(annot_file, reg)] = ind else: - raise ValueError('Unknown atlas. Try "Desikan-Killiany" or "Destrieux".') + raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux".') if self.atlas == 'Destrieux': num2region.update(num2region_D_custom) @@ -705,7 +705,7 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al elif isinstance(roi, str) and roi == 'temporal': if self.atlas != 'Destrieux': raise ValueError("roi='temporal' only supported for Destrieux atlas. Must specify list of specific region names") - temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 75, 76, 77, 78, 79, 80, 81] + temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 76, 77, 78, 79, 80, 81, 82] roi_list = [self.num2label[num] for num in temporal_regions_nums] roi_list += ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] else: From 684b664ebcbbc747a24802c6996f6f7f87fb910f Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:57:05 -0400 Subject: [PATCH 09/23] Handle simplified temporal ROI --- naplib/localization/freesurfer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 98a7275..e9ef399 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -705,9 +705,11 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al elif isinstance(roi, str) and roi == 'temporal': if self.atlas != 'Destrieux': raise ValueError("roi='temporal' only supported for Destrieux atlas. Must specify list of specific region names") - temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 76, 77, 78, 79, 80, 81, 82] - roi_list = [self.num2label[num] for num in temporal_regions_nums] - roi_list += ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] + if self.simplified: + roi_list = ['alHG','pmHG','HG','TTS','PT','PP','MTG','ITG','mSTG','pSTG','STG','STS','T.Pole'] + else: + temporal_regions_nums = [33, 34, 35, 36, 74, 41, 43, 72, 73, 38, 37, 76, 77, 78, 79, 80, 81, 82] + roi_list = [self.num2label[num] for num in temporal_regions_nums] else: roi_list = roi assert isinstance(roi, list) From 5cee5a90a8974c8a445db82adbc1013e71e0c56b Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:21:09 -0400 Subject: [PATCH 10/23] Custom atlas example --- .../brain_plotting/plot_intracranial_electrodes.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/brain_plotting/plot_intracranial_electrodes.py b/examples/brain_plotting/plot_intracranial_electrodes.py index 6d19c9b..ba45dfd 100644 --- a/examples/brain_plotting/plot_intracranial_electrodes.py +++ b/examples/brain_plotting/plot_intracranial_electrodes.py @@ -123,11 +123,21 @@ plt.show() ############################################################################### -# Directly plot Destrieux Atlas region labels overlaid on the brain +# Plot default Destrieux Atlas region labels overlaid on the brain brain = Brain('pial', subject_dir='./fsaverage/') brain.lh.overlay = brain.lh.labels brain.rh.overlay = brain.rh.labels -fig, axes = plot_brain_overlay(brain, cmap='tab20', vmin=1, vmax=75) +fig, axes = plot_brain_overlay(brain, cmap='tab20') +plt.show() + +############################################################################### +# Load and plot Glasser Atlas region labels overlaid on the brain +brain = Brain('pial', atlas='HCPMMP1', subject_dir='./fsaverage/') + +brain.lh.overlay = brain.lh.labels +brain.rh.overlay = brain.rh.labels + +fig, axes = plot_brain_overlay(brain, cmap='prism') plt.show() From cee934c3808f4ab2bf3020e3caa12c741a86fa51 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:25:39 -0400 Subject: [PATCH 11/23] Custom atlas annotation test --- tests/test_brain_object.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_brain_object.py b/tests/test_brain_object.py index c62dada..61b518c 100644 --- a/tests/test_brain_object.py +++ b/tests/test_brain_object.py @@ -47,10 +47,11 @@ def data(): brain_inflated = Brain('inflated', subject_dir='./.fsaverage_tmp/').split_hg('midpoint').split_stg().simplify_labels() brain_pial = Brain('pial', subject_dir='./.fsaverage_tmp/').split_hg('midpoint').split_stg().join_ifg().simplify_labels() - + brain_custom = Brain('pial', atlas='HCPMMP1', subject_dir='./.fsaverage_tmp/') return {'brain_inflated': brain_inflated, 'brain_pial': brain_pial, + 'brain_custom': brain_custom, 'coords': coords, 'isleft': isleft} @@ -95,6 +96,17 @@ def test_annotate_coords(data): assert np.array_equal(annots, expected) +def test_annotate_coords_custom_atlas(data): + annots = data['brain_custom'].annotate_coords(data['coords'], data['isleft']) + expected = array(['L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', + 'L_A5_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_STGa_ROI', + 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', 'L_A5_ROI', + 'L_A4_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_A5_ROI', + 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_A5_ROI', 'L_A5_ROI', + 'L_STSda_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI'], dtype=' Date: Tue, 8 Apr 2025 12:08:38 -0400 Subject: [PATCH 12/23] Add isleft defaults --- naplib/localization/freesurfer.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index e9ef399..d3944b3 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -959,7 +959,7 @@ def annotate(self, verts, is_left, is_surf=None, text=True): return labels def annotate_coords( - self, coords, isleft, distance_cutoff=10, is_surf=None, text=True + self, coords, isleft=None, distance_cutoff=10, is_surf=None, text=True ): """ Get labels (like pmHG, IFG, etc) for coordinates. Note, the coordinates should match the @@ -970,8 +970,9 @@ def annotate_coords( ---------- coords : np.ndarray Array of coordinates, shape (num_elecs, 3). - isleft : np.ndarray - Boolean array whether each electrode belongs to the left hemisphere, shape (num_elecs,). + isleft : np.ndarray (elecs,), optional + If provided, specifies a boolean which is True for each electrode that is in the left hemisphere. + If not given, this will be inferred from the first dimension of the coords (negative is left). distance_cutoff : float, default=10 Electrodes further than this distance (in mm) from the cortical surface will be labeled as "Other" is_surf : boolean np.ndarray @@ -987,6 +988,9 @@ def annotate_coords( Array of labels, either as strings or ints. """ + if isleft is None: + isleft = coords[:,0] < 0 + verts, dists = get_nearest_vert_index( coords, isleft, self.lh.surf, self.rh.surf, verbose=False ) @@ -999,7 +1003,7 @@ def annotate_coords( ) return labels - def distance_from_region(self, coords, isleft, region="pmHG", metric="surf"): + def distance_from_region(self, coords, isleft=None, region="pmHG", metric="surf"): """ Get distance from a certain region for each electrode's coordinates. Can compute distance along the cortical surface or as euclidean distance. For proper results, assuming @@ -1009,8 +1013,9 @@ def distance_from_region(self, coords, isleft, region="pmHG", metric="surf"): ---------- coords : np.ndarray Array of coordinates in pial space for this brain's subject_id, shape (num_elecs, 3). - isleft : np.ndarray - Boolean array whether each electrode belongs to the left hemisphere, shape (num_elecs,). + isleft : np.ndarray (elecs,), optional + If provided, specifies a boolean which is True for each electrode that is in the left hemisphere. + If not given, this will be inferred from the first dimension of the coords (negative is left). region : str, default='pmHG' Anatomical label. Must exist in the labels for the brain. metric : {'surf','euclidean'}, default='surf' @@ -1021,6 +1026,8 @@ def distance_from_region(self, coords, isleft, region="pmHG", metric="surf"): distances : np.ndarray Array of distances, in mm. """ + if isleft is None: + isleft = coords[:,0] < 0 region_label_num = self.label2num[region] From 361d320966ee5be0fc62f36776c57d4cb9ea17f8 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 12:37:51 -0400 Subject: [PATCH 13/23] Fix custom atlas test --- tests/test_brain_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_brain_object.py b/tests/test_brain_object.py index 61b518c..1aca35f 100644 --- a/tests/test_brain_object.py +++ b/tests/test_brain_object.py @@ -98,7 +98,7 @@ def test_annotate_coords(data): def test_annotate_coords_custom_atlas(data): annots = data['brain_custom'].annotate_coords(data['coords'], data['isleft']) - expected = array(['L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', + expected = np.array(['L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_A5_ROI', From a98edffb2929770390cc3607863eec1ce8aa54dd Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:48:51 -0400 Subject: [PATCH 14/23] Validate brain inputs #214 --- naplib/localization/freesurfer.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index d3944b3..52cf4d5 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -44,7 +44,7 @@ def __init__( surf_type: str = "pial", subject: str = "fsaverage", coordinate_space: str = 'FSAverage', - atlas: str = '', + atlas=None, subject_dir=None, ): """ @@ -61,7 +61,7 @@ def __init__( Subject to use, must be a directory within ``subject_dir`` coordinate_space : str, default='FSAverage' Coordinate space of brain vertices. Must be 'FSAverage' or 'MNI152' - atlas : str, default='' + atlas : str, default=None Atlas for brain parcellation. Defaults to 'Destrieux' for coordinate_space='FSAverage' and 'Desikan-Killiany' for 'MNI152'. Can also be an annotation file name given by ``{subject_dir}/{subject}/label/?h.{atlas}.annot`` @@ -84,6 +84,8 @@ def __init__( self.surf_type = surf_type self.subject = subject self.coordinate_space = coordinate_space + if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{self.atlas}.annot')): + raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux"') self.atlas = atlas if subject_dir is None: @@ -327,6 +329,7 @@ def zones(self, labels, min_alpha=0): """ if isinstance(labels, str): labels = (labels,) + labels = [l for l in labels if l in self.label2num] verts = np.zeros(self.n_verts, dtype=bool) zones = np.zeros(self.n_verts, dtype=int) @@ -804,7 +807,7 @@ def __init__( surf_type: str = "pial", subject: str = "fsaverage", coordinate_space: str = 'FSAverage', - atlas: str = '', + atlas=None, subject_dir=None ): """ @@ -820,7 +823,7 @@ def __init__( Subject to use, must be a directory within ``subject_dir`` coordinate_space : str, default='FSAverage' Coordinate space of brain vertices. Must be 'FSAverage' or 'MNI152' - atlas : str, default='' + atlas : str, default=None Atlas for brain parcellation. Defaults to 'Destrieux' for coordinate_space='FSAverage' and 'Desikan-Killiany' for 'MNI152'. Can also be an annotation file name given by ``{subject_dir}/{subject}/label/?h.{atlas}.annot`` @@ -974,7 +977,7 @@ def annotate_coords( If provided, specifies a boolean which is True for each electrode that is in the left hemisphere. If not given, this will be inferred from the first dimension of the coords (negative is left). distance_cutoff : float, default=10 - Electrodes further than this distance (in mm) from the cortical surface will be labeled as "Other" + Electrodes further than this distance (in mm) from the cortical surface will be labeled as None is_surf : boolean np.ndarray Array of the same shape as the number of vertices in the surface (e.g. len(self.lh.surf[0])) indicating whether those points should be included as surface options. If an electrode is closest to a point From 2e3e9aab6089bfb29965088179d16048f1ddc501 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:53:33 -0400 Subject: [PATCH 15/23] Atlas validation --- naplib/localization/freesurfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 52cf4d5..c19be4d 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -84,7 +84,7 @@ def __init__( self.surf_type = surf_type self.subject = subject self.coordinate_space = coordinate_space - if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{self.atlas}.annot')): + if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{atlas}.annot')): raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux"') self.atlas = atlas From 13417dbabe605cccbc292ca26eeedc4389cb5f3e Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:01:29 -0400 Subject: [PATCH 16/23] Brain validation order --- naplib/localization/freesurfer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index c19be4d..ddc5148 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -84,14 +84,15 @@ def __init__( self.surf_type = surf_type self.subject = subject self.coordinate_space = coordinate_space - if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{atlas}.annot')): - raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux"') - self.atlas = atlas if subject_dir is None: subject_dir = os.environ.get("SUBJECTS_DIR", "./") self.subject_dir = subject_dir + + if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{atlas}.annot')): + raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux"') + self.atlas = atlas # Check if fsaverage geometry exists if self.coordinate_space == 'FSAverage': From d0795393bd914dca883159c5f7c0c95f7c2563eb Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:37:01 -0400 Subject: [PATCH 17/23] Separate lh/rh annotation --- naplib/localization/freesurfer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index ddc5148..db68b80 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -89,7 +89,7 @@ def __init__( subject_dir = os.environ.get("SUBJECTS_DIR", "./") self.subject_dir = subject_dir - + if atlas not in ['Desikan-Killiany', 'Destrieux'] and not os.path.exists(self.label_file(f'{self.hemi}.{atlas}.annot')): raise ValueError('Bad atlas. Try "Desikan-Killiany" or "Destrieux"') self.atlas = atlas @@ -959,7 +959,8 @@ def annotate(self, verts, is_left, is_surf=None, text=True): if is_surf is not None: labels[~is_surf] = 0 if text: - labels = np.array([self.num2label[label] for label in labels]) + labels[is_left] = np.array([self.lh.num2label[label] for label in labels[is_left]]) + labels[~is_left] = np.array([self.rh.num2label[label] for label in labels[~is_left]]) return labels def annotate_coords( @@ -994,7 +995,7 @@ def annotate_coords( """ if isleft is None: isleft = coords[:,0] < 0 - + verts, dists = get_nearest_vert_index( coords, isleft, self.lh.surf, self.rh.surf, verbose=False ) From d22f46956b0868f087591d809ec176c5fead7c63 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:55:58 -0400 Subject: [PATCH 18/23] Per hemisphere labeling --- naplib/localization/freesurfer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index db68b80..b162020 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -959,8 +959,8 @@ def annotate(self, verts, is_left, is_surf=None, text=True): if is_surf is not None: labels[~is_surf] = 0 if text: - labels[is_left] = np.array([self.lh.num2label[label] for label in labels[is_left]]) - labels[~is_left] = np.array([self.rh.num2label[label] for label in labels[~is_left]]) + labels = np.array([self.lh.num2label[label] if is_left[i] else self.rh.num2label[label] + for i,label in enumerate(labels)]) return labels def annotate_coords( From 7dcc89e9eb6ba967b4b1af88cf03abb90f1337ba Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:13:59 -0400 Subject: [PATCH 19/23] Fixed test result for bilateral labels --- tests/test_brain_object.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_brain_object.py b/tests/test_brain_object.py index 1aca35f..dfdd085 100644 --- a/tests/test_brain_object.py +++ b/tests/test_brain_object.py @@ -101,10 +101,11 @@ def test_annotate_coords_custom_atlas(data): expected = np.array(['L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_STGa_ROI', 'L_A5_ROI', 'L_A5_ROI', - 'L_A4_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_A5_ROI', - 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'L_A5_ROI', 'L_A5_ROI', + 'L_A4_ROI', 'L_A5_ROI', 'L_A4_ROI', 'L_A4_ROI', 'R_A5_ROI', + 'R_A5_ROI', 'R_A4_ROI', 'R_A4_ROI', 'R_A5_ROI', 'R_A5_ROI', 'L_STSda_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI', 'L_A5_ROI'], dtype=' Date: Wed, 9 Apr 2025 14:42:09 -0400 Subject: [PATCH 20/23] Removing has_overlay --- naplib/localization/freesurfer.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index b162020..91f2f54 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -661,8 +661,6 @@ def reset_overlay(self): self.alpha = np.ones(self.surf[1].shape[0]) self.keep_visible = np.ones_like(self.overlay).astype("bool") self.keep_visible_cells = np.ones_like(self.alpha).astype("bool") - self.has_overlay = np.zeros_like(self.overlay).astype("bool") - self.has_overlay_cells = np.zeros_like(self.alpha).astype("bool") return self def paint_overlay(self, labels, value=1): @@ -672,10 +670,8 @@ def paint_overlay(self, labels, value=1): Returns: self """ - verts, add_overlay, _ = self.zones(labels) - self.overlay[verts] = value - self.has_overlay[verts == 1] = True - self.has_overlay_cells[add_overlay == 1] = True + for label in labels: + self.overlay[self.labels==self.label2num[label]] = value return self def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='all'): @@ -755,8 +751,6 @@ def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='al trigs[i] = np.mean([verts[self.trigs[i, j]] != 0 for j in range(3)]) self.overlay[updated_vertices] = smoothed_values[updated_vertices] - self.has_overlay[updated_vertices] = True - self.has_overlay_cells[trigs == 1] = True return self @@ -964,7 +958,7 @@ def annotate(self, verts, is_left, is_surf=None, text=True): return labels def annotate_coords( - self, coords, isleft=None, distance_cutoff=10, is_surf=None, text=True + self, coords, isleft=None, distance_cutoff=10, is_surf=None, text=True, get_dists=False, ): """ Get labels (like pmHG, IFG, etc) for coordinates. Note, the coordinates should match the @@ -986,11 +980,15 @@ def annotate_coords( with a False indicator in this array, then it will get None as its label. text : bool, default=True Whether to return labels as string names, or integer labels. + get_dists : bool, default=False + Whether to return distances for each electrode to the nearest vertex. Returns ------- labels : np.ndarray Array of labels, either as strings or ints. + dists : np.ndarray, optional + Array of minimum distances as floats """ if isleft is None: @@ -1006,7 +1004,10 @@ def annotate_coords( for lab, dist in zip(labels, dists) ] ) - return labels + if get_dists: + return labels, dists + else: + return labels def distance_from_region(self, coords, isleft=None, region="pmHG", metric="surf"): """ From d4cb2e55f2020e1b854b2b546d048c7816a1e6a5 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:30:49 -0400 Subject: [PATCH 21/23] Check label type --- naplib/localization/freesurfer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 91f2f54..9f8e0da 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -670,8 +670,11 @@ def paint_overlay(self, labels, value=1): Returns: self """ + if isinstance(labels, str): + labels = [labels] for label in labels: - self.overlay[self.labels==self.label2num[label]] = value + if label in self.label2num: + self.overlay[self.labels==self.label2num[label]] = value return self def interpolate_electrodes_onto_brain(self, coords, values, k, max_dist, roi='all'): From 3b8471cd803bc54f4269fb4eb57b3fe358e3a0cb Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Thu, 10 Apr 2025 13:13:02 -0400 Subject: [PATCH 22/23] parcellate_overlay Merge overlay values within each atlas parcel into a single value. Meant to be used after interpolate_electrodes_onto_brain() --- naplib/localization/freesurfer.py | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 9f8e0da..87a1750 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -786,6 +786,28 @@ def mark_overlay(self, verts, value=1, inner_radius=0.8, taper=True): return self + def parcellate_overlay(self, merge_func=np.mean): + """ + Merges overlay values within each parcel for a single hemisphere. + + Parameters + ---------- + merge_func : callable + Function to merge values within each parcel. Should accept a 1D + NumPy array and return a scalar. + """ + # Vectorize label to number conversion for efficiency + label_nums = np.array([self.label2num[label] for label in self.label_names], dtype=self.labels.dtype) + + # Vectorize the core logic. + parcellated_overlay = np.zeros_like(self.overlay) # Create an empty array like self.overlay + for i, label_num in enumerate(label_nums): + inds = self.labels == label_num + if inds.any(): # important check in case a label has no vertices + parcellated_overlay[inds] = merge_func(self.overlay[inds]) + self.overlay = parcellated_overlay + return self + def set_visible(self, labels, min_alpha=0): keep_visible, self.alpha, _ = self.zones(labels, min_alpha=min_alpha) self.keep_visible = keep_visible > min_alpha @@ -1226,6 +1248,32 @@ def interpolate_electrodes_onto_brain(self, coords, values, isleft=None, k=10, m self.rh.interpolate_electrodes_onto_brain(coords[~isleft], values[~isleft], k=k, max_dist=max_dist, roi=roi) return self + def parcellate_overlay(self, merge_func=np.mean): + """Merges brain overlay values within each atlas parcel. + + This method applies a merging function to the overlay values within each + anatomical parcel defined by an atlas. It is typically used after + interpolating electrode data onto the brain surface + (e.g., via `brain.interpolate_electrodes_onto_brain()`) to summarize + the data within each parcel. + + Parameters + ---------- + merge_func : callable, optional + The function used to combine the overlay values within each parcel. + The function should accept an array-like object of values and return a + single value. Common examples include `numpy.mean` (default), + `numpy.median`, and `numpy.max`. + + Returns + ------- + self : instance of self + Returns the instance itself, with the overlay data parcellated. + """ + self.lh.parcellate_overlay(merge_func) + self.rh.parcellate_overlay(merge_func) + return self + def get_nearest_vert_index(coords, isleft, surf_lh, surf_rh, verbose=False): vert_indices = [] From 0c26828ad3365e2dbe3bc9b3c51603fae75fdc68 Mon Sep 17 00:00:00 2001 From: Vinay Raghavan <42253618+vinaysraghavan@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:43:27 -0400 Subject: [PATCH 23/23] Parcellate overlay defaults --- naplib/localization/freesurfer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/naplib/localization/freesurfer.py b/naplib/localization/freesurfer.py index 87a1750..51be56e 100644 --- a/naplib/localization/freesurfer.py +++ b/naplib/localization/freesurfer.py @@ -792,7 +792,7 @@ def parcellate_overlay(self, merge_func=np.mean): Parameters ---------- - merge_func : callable + merge_func : callable, default=numpy.mean Function to merge values within each parcel. Should accept a 1D NumPy array and return a scalar. """ @@ -1259,7 +1259,7 @@ def parcellate_overlay(self, merge_func=np.mean): Parameters ---------- - merge_func : callable, optional + merge_func : callable, default=numpy.mean The function used to combine the overlay values within each parcel. The function should accept an array-like object of values and return a single value. Common examples include `numpy.mean` (default),