def create_spherical_roi_volumes(node_size, coords, template_mask): """ Create volume ROI mask of spheres from a given set of coordinates and radius. Parameters ---------- node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. coords : list List of (x, y, z) tuples in mm-space corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. template_mask : str Path to binarized version of standard (MNI)-space template Nifti1Image file. Returns ------- parcel_list : list List of 3D boolean numpy arrays or binarized Nifti1Images corresponding to ROI masks. par_max : int The maximum label intensity in the parcellation image. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. parc : bool Indicates whether to use the raw parcels as ROI nodes instead of coordinates at their center-of-mass. """ from pynets.core.nodemaker import get_sphere, mmToVox mask_img = nib.load(template_mask) mask_aff = mask_img.affine mask_shape = mask_img.shape mask_img.uncache() print("%s%s" % ('Creating spherical ROI atlas with radius: ', node_size)) coords_vox = [] for i in coords: coords_vox.append(mmToVox(mask_aff, i)) coords_vox = list(set(list(tuple(x) for x in coords_vox))) x_vox = np.diagonal(mask_aff[:3, 0:3])[0] y_vox = np.diagonal(mask_aff[:3, 0:3])[1] z_vox = np.diagonal(mask_aff[:3, 0:3])[2] sphere_vol = np.zeros(mask_shape, dtype=bool) parcel_list = [] i = 0 for coord in coords_vox: sphere_vol[tuple(get_sphere(coord, node_size, (np.abs(x_vox), y_vox, z_vox), mask_shape).T)] = i * 1 parcel_list.append(nib.Nifti1Image(sphere_vol.astype('uint16'), affine=mask_aff)) i = i + 1 par_max = len(coords) if par_max > 0: parc = True else: raise ValueError('Number of nodes is zero.') return parcel_list, par_max, node_size, parc
def coords_masker(roi, coords, labels, error): """ Evaluate the affinity of any arbitrary list of coordinate nodes for a user-specified ROI mask. Parameters ---------- roi : str File path to binarized/boolean region-of-interest Nifti1Image file. coords : list List of (x, y, z) tuples in mm-space corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. labels : list List of string labels corresponding to ROI nodes. error : int Rounded euclidean distance, in units of voxel number, to use as a spatial error cushion in the case of evaluating the spatial affinity of a given list of coordinates to the given ROI mask. Returns ------- coords : list Filtered list of (x, y, z) tuples in mm-space with a spatial affinity for the specified ROI mask. labels : list Filtered list of string labels corresponding to ROI nodes with a spatial affinity for the specified ROI mask. """ from nilearn import masking from pynets.core.nodemaker import mmToVox mask_data, mask_aff = masking._load_mask_img(roi) x_vox = np.diagonal(mask_aff[:3, 0:3])[0] y_vox = np.diagonal(mask_aff[:3, 0:3])[1] z_vox = np.diagonal(mask_aff[:3, 0:3])[2] # mask_coords = list(zip(*np.where(mask_data == True))) coords_vox = [] for i in coords: coords_vox.append(mmToVox(mask_aff, i)) coords_vox = list( tuple(map(lambda y: isinstance(y, float) and int(round(y, 0)), x)) for x in coords_vox) #coords_vox = list(set(list(tuple(x) for x in coords_vox))) bad_coords = [] for coord_vox in coords_vox: sphere_vol = np.zeros(mask_data.shape, dtype=bool) sphere_vol[tuple(coord_vox)] = 1 if (mask_data & sphere_vol).any(): print(f"{coord_vox}{' falls within mask...'}") continue inds = get_sphere(coord_vox, error, (np.abs(x_vox), y_vox, z_vox), mask_data.shape) sphere_vol[tuple(inds.T)] = 1 if (mask_data & sphere_vol).any(): print( f"{coord_vox}{' is within a + or - '}{float(error):.2f}{' mm neighborhood...'}" ) continue bad_coords.append(coord_vox) bad_coords = [x for x in bad_coords if x is not None] indices = [] for bad_coords in bad_coords: indices.append(coords_vox.index(bad_coords)) labels = list(labels) coords = list(tuple(x) for x in coords) try: for ix in sorted(indices, reverse=True): print(f"{'Removing: '}{labels[ix]}{' at '}{coords[ix]}") del labels[ix], coords[ix] except RuntimeError: print( 'ERROR: Restrictive masking. No coords remain after masking with brain mask/roi...' ) if len(coords) <= 1: raise ValueError( '\nERROR: ROI mask was likely too restrictive and yielded < 2 remaining coords' ) return coords, labels
def get_node_membership(network, infile, coords, labels, parc, parcel_list, perc_overlap=0.75, error=4): """ Evaluate the affinity of any arbitrary list of coordinate or parcel nodes for a user-specified RSN based on Yeo-7 or Yeo-17 definitions. Parameters ---------- network : str Resting-state network based on Yeo-7 and Yeo-17 naming (e.g. 'Default') used to filter nodes in the study of brain subgraphs. infile : str File path to Nifti1Image object whose affine will provide sampling reference for evaluation spatial proximity. coords : list List of (x, y, z) tuples in mm-space corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. labels : list List of string labels corresponding to ROI nodes. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. parcel_list : list List of 3D boolean numpy arrays or binarized Nifti1Images corresponding to ROI masks. perc_overlap : float Value 0-1 indicating a threshold of spatial overlap to use as a spatial error cushion in the case of evaluating RSN membership from a given list of parcel masks. Default is 0.75. error : int Rounded euclidean distance, in units of voxel number, to use as a spatial error cushion in the case of evaluating RSN membership from a given list of coordinates. Default is 4. Returns ------- coords_mm : list Filtered list of (x, y, z) tuples in mm-space with a spatial affinity for the specified RSN. RSN_parcels : list Filtered list of 3D boolean numpy arrays or binarized Nifti1Images corresponding to ROI masks with a spatial affinity for the specified RSN. net_labels : list Filtered list of string labels corresponding to ROI nodes with a spatial affinity for the specified RSN. network : str Resting-state network based on Yeo-7 and Yeo-17 naming (e.g. 'Default') used to filter nodes in the study of brain subgraphs. References ---------- .. [1] Thomas Yeo, B. T., Krienen, F. M., Sepulcre, J., Sabuncu, M. R., Lashkari, D., Hollinshead, M., … Buckner, R. L. (2011). The organization of the human cerebral cortex estimated by intrinsic functional connectivity. Journal of Neurophysiology. https://doi.org/10.1152/jn.00338.2011 .. [2] Schaefer A, Kong R, Gordon EM, Laumann TO, Zuo XN, Holmes AJ, Eickhoff SB, Yeo BTT. Local-Global parcellation of the human cerebral cortex from intrinsic functional connectivity MRI, Cerebral Cortex, 29:3095-3114, 2018. """ from nilearn.image import resample_img from pynets.core.nodemaker import get_sphere, mmToVox, VoxTomm import pkg_resources import pandas as pd # Determine whether input is from 17-networks or 7-networks seven_nets = [ 'Vis', 'SomMot', 'DorsAttn', 'SalVentAttn', 'Limbic', 'Cont', 'Default' ] seventeen_nets = [ 'VisCent', 'VisPeri', 'SomMotA', 'SomMotB', 'DorsAttnA', 'DorsAttnB', 'SalVentAttnA', 'SalVentAttnB', 'LimbicOFC', 'LimbicTempPole', 'ContA', 'ContB', 'ContC', 'DefaultA', 'DefaultB', 'DefaultC', 'TempPar' ] # Load subject func data bna_img = nib.load(infile) bna_aff = bna_img.affine bna_img.uncache() x_vox = np.diagonal(bna_aff[:3, 0:3])[0] y_vox = np.diagonal(bna_aff[:3, 0:3])[1] z_vox = np.diagonal(bna_aff[:3, 0:3])[2] if network in seventeen_nets: if x_vox <= 1 and y_vox <= 1 and z_vox <= 1: par_file = pkg_resources.resource_filename( "pynets", "rsnrefs/BIGREF1mm.nii.gz") else: par_file = pkg_resources.resource_filename( "pynets", "rsnrefs/BIGREF2mm.nii.gz") # Grab RSN reference file nets_ref_txt = pkg_resources.resource_filename( "pynets", "rsnrefs/Schaefer2018_1000_17nets_ref.txt") elif network in seven_nets: if x_vox <= 1 and y_vox <= 1 and z_vox <= 1: par_file = pkg_resources.resource_filename( "pynets", "rsnrefs/SMALLREF1mm.nii.gz") else: par_file = pkg_resources.resource_filename( "pynets", "rsnrefs/SMALLREF2mm.nii.gz") # Grab RSN reference file nets_ref_txt = pkg_resources.resource_filename( "pynets", "rsnrefs/Schaefer2018_1000_7nets_ref.txt") else: nets_ref_txt = None if not nets_ref_txt: raise ValueError( f"Network: {str(network)} not found!\nSee valid network names using the `--help` flag with " f"`pynets`") # Create membership dictionary dict_df = pd.read_csv(nets_ref_txt, sep="\t", header=None, names=["Index", "Region", "X", "Y", "Z"]) dict_df.Region.unique().tolist() ref_dict = {v: k for v, k in enumerate(dict_df.Region.unique().tolist())} par_img = nib.load(par_file) RSN_ix = list(ref_dict.keys())[list(ref_dict.values()).index(network)] RSNmask = np.asarray(par_img.dataobj)[:, :, :, RSN_ix] coords_vox = [] for i in coords: coords_vox.append(mmToVox(bna_aff, i)) coords_vox = list( tuple(map(lambda y: isinstance(y, float) and int(round(y, 0)), x)) for x in coords_vox) #coords_vox = list(set(list(tuple(x) for x in coords_vox))) if parc is False: i = -1 RSN_parcels = None RSN_coords_vox = [] net_labels = [] for coords in coords_vox: sphere_vol = np.zeros(RSNmask.shape, dtype=bool) sphere_vol[tuple(coords)] = 1 i = i + 1 if (RSNmask.astype('bool') & sphere_vol).any(): print(f"{coords}{' coords falls within '}{network}{'...'}") RSN_coords_vox.append(coords) net_labels.append(labels[i]) continue else: inds = get_sphere(coords, error, (np.abs(x_vox), y_vox, z_vox), RSNmask.shape) sphere_vol[tuple(inds.T)] = 1 if (RSNmask.astype('bool') & sphere_vol).any(): print( f"{coords} coords is within a + or - {float(error):.2f} mm neighborhood of {network}..." ) RSN_coords_vox.append(coords) net_labels.append(labels[i]) coords_mm = [] for i in RSN_coords_vox: coords_mm.append(VoxTomm(bna_aff, i)) coords_mm = list(set(list(tuple(x) for x in coords_mm))) else: i = 0 RSN_parcels = [] coords_with_parc = [] net_labels = [] for parcel in parcel_list: parcel_vol = np.zeros(RSNmask.shape, dtype=bool) parcel_vol[np.asarray( resample_img(parcel, target_affine=par_img.affine, target_shape=RSNmask.shape).dataobj) == 1] = 1 # Count number of unique voxels where overlap of parcel and mask occurs overlap_count = len( np.unique( np.where((RSNmask.astype('uint16') == 1) & (parcel_vol.astype('uint16') == 1)))) # Count number of total unique voxels within the parcel total_count = len( np.unique(np.where((parcel_vol.astype('uint16') == 1)))) # Calculate % overlap try: overlap = float(overlap_count / total_count) except RuntimeWarning: print('\nWarning: No overlap with roi mask!\n') overlap = float(0) if overlap >= perc_overlap: print( f"{100 * overlap:.2f}% of parcel {labels[i]} falls within {str(network)} mask..." ) RSN_parcels.append(parcel) coords_with_parc.append(coords[i]) net_labels.append(labels[i]) i = i + 1 coords_mm = list(set(list(tuple(x) for x in coords_with_parc))) par_img.uncache() if len(coords_mm) <= 1: raise ValueError( f"\nERROR: No coords from the specified atlas found within {network} network." ) return coords_mm, RSN_parcels, net_labels, network