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 test_get_sphere(): """ Test get_sphere functionality """ base_dir = str(Path(__file__).parent / "examples") img_file = f"{base_dir}/BIDS/sub-0025427/ses-1/func/sub-0025427_ses-1_task-rest_space-MNI152NLin2009cAsym_desc-brain_mask.nii.gz" img = nib.load(img_file) r = 4 vox_dims = (2.0, 2.0, 2.0) coords = [[0, 0, 0], [-5, -5, -5], [5, 5, 5], [-10, -10, -10], [10, 10, 10]] neighbors = [] for coord in coords: neighbors.append( nodemaker.get_sphere(coord, r, vox_dims, img.shape[0:3])) neighbors = [i for i in neighbors if len(i) > 0] assert len(neighbors) == 3
def test_get_sphere(): """ Test get_sphere functionality """ img_file = pkg_resources.resource_filename( "pynets", f"templates/standard/MNI152_T1_brain_2mm.nii.gz") img = nib.load(img_file) r = 4 vox_dims = (2.0, 2.0, 2.0) coords = [[0, 0, 0], [-5, -5, -5], [5, 5, 5], [-10, -10, -10], [10, 10, 10]] neighbors = [] for coord in coords: neighbors.append( nodemaker.get_sphere(coord, r, vox_dims, img.shape[0:3])) neighbors = [i for i in neighbors if len(i) > 0] assert len(neighbors) == 3
def test_get_sphere(): """ Test get_sphere functionality """ base_dir = str(Path(__file__).parent/"examples") dir_path = base_dir + '/002/dmri' img_file = dir_path + '/nodif_b0_bet.nii.gz' img = nib.load(img_file) r = 4 vox_dims = (2.0, 2.0, 2.0) coords_file = dir_path + '/DesikanKlein2012/Default_coords_rsn.pkl' with open(coords_file, 'rb') as file_: coords = pickle.load(file_) neighbors = [] for coord in coords: neighbors.append(nodemaker.get_sphere(coord, r, vox_dims, img.shape[0:3])) neighbors = [i for i in neighbors if len(i)>0] assert len(neighbors) == 4
def streams2graph(atlas_mni, streams, dir_path, track_type, target_samples, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, warped_fa, min_length, error_margin): """ Use tracked streamlines as a basis for estimating a structural connectome. Parameters ---------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. dir_path : str Path to directory containing subject derivative data for a given pynets run. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). warped_fa : str File path to MNI-space warped FA Nifti1Image. min_length : int Minimum fiber length threshold in mm to restrict tracking. error_margin : int Euclidean margin of error for classifying a streamline as a connection to an ROI. Default is 2 voxels. Returns ------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. conn_matrix : array Adjacency matrix stored as an m x n array of nodes and edges. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. dir_path : str Path to directory containing subject derivative data for given run. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). min_length : int Minimum fiber length threshold in mm to restrict tracking. error_margin : int Euclidean margin of error for classifying a streamline as a connection to an ROI. Default is 2 voxels. References ---------- .. [1] Sporns, O., Tononi, G., & Kötter, R. (2005). The human connectome: A structural description of the human brain. PLoS Computational Biology. https://doi.org/10.1371/journal.pcbi.0010042 .. [2] Sotiropoulos, S. N., & Zalesky, A. (2019). Building connectomes using diffusion MRI: why, how and but. NMR in Biomedicine. https://doi.org/10.1002/nbm.3752 .. [3] Chung, M. K., Hanson, J. L., Adluru, N., Alexander, A. L., Davidson, R. J., & Pollak, S. D. (2017). Integrative Structural Brain Network Analysis in Diffusion Tensor Imaging. Brain Connectivity. https://doi.org/10.1089/brain.2016.0481 """ import gc import time import pkg_resources import sys import yaml from dipy.tracking.streamline import Streamlines, values_from_volume from dipy.tracking._utils import _mapping_to_voxel, _to_voxel_coordinates import networkx as nx from itertools import combinations from collections import defaultdict from pynets.core import utils, nodemaker from pynets.dmri.dmri_utils import generate_sl from dipy.io.streamline import load_tractogram from dipy.io.stateful_tractogram import Space, Origin with open(pkg_resources.resource_filename("pynets", "runconfig.yaml"), "r") as stream: hardcoded_params = yaml.load(stream) fa_wei = hardcoded_params["StructuralNetworkWeighting"][ "fa_weighting"][0] fiber_density = hardcoded_params["StructuralNetworkWeighting"][ "fiber_density"][0] overlap_thr = hardcoded_params["StructuralNetworkWeighting"][ "overlap_thr"][0] roi_neighborhood_tol = \ hardcoded_params['tracking']["roi_neighborhood_tol"][0] stream.close() start = time.time() if float(roi_neighborhood_tol) <= float(error_margin): try: raise ValueError('roi_neighborhood_tol preset cannot be less than ' 'the value of the structural connectome error' '_margin parameter.') except ValueError: import sys sys.exit(1) else: print(f"Using fiber-roi intersection tolerance: {error_margin}...") # Load FA fa_img = nib.load(warped_fa) # Load parcellation roi_img = nib.load(atlas_mni) atlas_data = np.around(np.asarray(roi_img.dataobj)) roi_zooms = roi_img.header.get_zooms() roi_shape = roi_img.shape # Read Streamlines streamlines = [ i.astype(np.float32) for i in Streamlines( load_tractogram( streams, fa_img, to_origin=Origin.NIFTI, to_space=Space.VOXMM).streamlines) ] # from fury import actor, window # renderer = window.Renderer() # template_actor = actor.contour_from_roi(roi_img.get_fdata(), # color=(50, 50, 50), opacity=0.05) # renderer.add(template_actor) # lines_actor = actor.streamtube(streamlines, window.colors.orange, # linewidth=0.3) # renderer.add(lines_actor) # window.show(renderer) roi_img.uncache() if fa_wei is True: fa_weights = values_from_volume( np.asarray(fa_img.dataobj, dtype=np.float32), streamlines, np.eye(4)) global_fa_weights = list(utils.flatten(fa_weights)) min_global_fa_wei = min([i for i in global_fa_weights if i > 0]) max_global_fa_wei = max(global_fa_weights) fa_weights_norm = [] # Here we normalize by global FA for val_list in fa_weights: fa_weights_norm.append( np.nanmean((val_list - min_global_fa_wei) / (max_global_fa_wei - min_global_fa_wei))) # Make streamlines into generators to keep memory at a minimum total_streamlines = len(streamlines) sl = [generate_sl(i) for i in streamlines] del streamlines # Instantiate empty networkX graph object & dictionary and create # voxel-affine mapping lin_T, offset = _mapping_to_voxel(np.eye(4)) mx = len(np.unique(atlas_data.astype("uint16"))) - 1 g = nx.Graph(ecount=0, vcount=mx) edge_dict = defaultdict(int) node_dict = dict( zip(np.unique(atlas_data.astype("uint16"))[1:], np.arange(mx) + 1)) # Add empty vertices with label volume attributes for node in range(1, mx + 1): g.add_node(node, roi_volume=np.sum(atlas_data.astype("uint16") == node)) # Build graph pc = 0 bad_idxs = [] fiberlengths = {} fa_weights_dict = {} print(f"Quantifying fiber-ROI intersection for {atlas}:") for ix, s in enumerate(sl): # Percent counter pcN = int(round(100 * float(ix / total_streamlines))) if pcN % 10 == 0 and ix > 0 and pcN > pc: pc = pcN print(f"{pcN}%") # Map the streamlines coordinates to voxel coordinates and get labels # for label_volume vox_coords = _to_voxel_coordinates(Streamlines(s), lin_T, offset) lab_coords = [ nodemaker.get_sphere(coord, error_margin, roi_zooms, roi_shape) for coord in vox_coords ] [i, j, k] = np.vstack(np.array(lab_coords)).T # get labels for label_volume lab_arr = atlas_data[i, j, k] # print(lab_arr) endlabels = [] for jx, lab in enumerate(np.unique(lab_arr).astype("uint32")): if (lab > 0) and (np.sum(lab_arr == lab) >= overlap_thr): try: endlabels.append(node_dict[lab]) except BaseException: bad_idxs.append(jx) print(f"Label {lab} missing from parcellation. Check " f"registration and ensure valid input parcellation " f"file.") edges = combinations(endlabels, 2) for edge in edges: # Get fiber lengths along edge if fiber_density is True: if not (edge[0], edge[1]) in fiberlengths.keys(): fiberlengths[(edge[0], edge[1])] = [len(vox_coords)] else: fiberlengths[(edge[0], edge[1])].append(len(vox_coords)) # Get FA values along edge if fa_wei is True: if not (edge[0], edge[1]) in fa_weights_dict.keys(): fa_weights_dict[(edge[0], edge[1])] = [fa_weights_norm[ix]] else: fa_weights_dict[(edge[0], edge[1])].append(fa_weights_norm[ix]) lst = tuple([int(node) for node in edge]) edge_dict[tuple(sorted(lst))] += 1 edge_list = [(k[0], k[1], count) for k, count in edge_dict.items()] g.add_weighted_edges_from(edge_list) del lab_coords, lab_arr, endlabels, edges, edge_list gc.collect() # Add fiber density attributes for each edge # Adapted from the nnormalized fiber-density estimation routines of # Sebastian Tourbier. if fiber_density is True: print("Weighting edges by fiber density...") # Summarize total fibers and total label volumes total_fibers = 0 total_volume = 0 u_start = -1 for u, v, d in g.edges(data=True): total_fibers += len(d) if u != u_start: total_volume += g.nodes[int(u)]['roi_volume'] u_start = u ix = 0 for u, v, d in g.edges(data=True): if d['weight'] > 0: edge_fiberlength_mean = np.nanmean(fiberlengths[(u, v)]) fiber_density = (float( ((float(d['weight']) / float(total_fibers)) / float(edge_fiberlength_mean)) * ((2.0 * float(total_volume)) / (g.nodes[int(u)]['roi_volume'] + g.nodes[int(v)]['roi_volume'])))) * 1000 else: fiber_density = 0 g.edges[u, v].update({"fiber_density": fiber_density}) ix += 1 if fa_wei is True: print("Weighting edges by FA...") # Add FA attributes for each edge ix = 0 for u, v, d in g.edges(data=True): if d['weight'] > 0: edge_average_fa = np.nanmean(fa_weights_dict[(u, v)]) else: edge_average_fa = np.nan g.edges[u, v].update({"fa_weight": edge_average_fa}) ix += 1 # Summarize weights if fa_wei is True and fiber_density is True: for u, v, d in g.edges(data=True): g.edges[u, v].update( {"final_weight": (d['fa_weight']) * d['fiber_density']}) elif fiber_density is True and fa_wei is False: for u, v, d in g.edges(data=True): g.edges[u, v].update({"final_weight": d['fiber_density']}) elif fa_wei is True and fiber_density is False: for u, v, d in g.edges(data=True): g.edges[u, v].update({"final_weight": d['fa_weight'] * d['weight']}) else: for u, v, d in g.edges(data=True): g.edges[u, v].update({"final_weight": d['weight']}) # Convert weighted graph to numpy matrix conn_matrix_raw = nx.to_numpy_array(g, weight='final_weight') # Enforce symmetry conn_matrix = np.maximum(conn_matrix_raw, conn_matrix_raw.T) print("Structural graph completed:\n", str(time.time() - start)) if len(bad_idxs) > 0: bad_idxs = sorted(list(set(bad_idxs)), reverse=True) for j in bad_idxs: del labels[j], coords[j] coords = np.array(coords) labels = np.array(labels) assert len(coords) == len(labels) == conn_matrix.shape[0] return (atlas_mni, streams, conn_matrix, track_type, target_samples, dir_path, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, min_length, error_margin)
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
def streams2graph(atlas_mni, streams, overlap_thr, dir_path, track_type, target_samples, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, warped_fa, error_margin, min_length, fa_wei=True): ''' Use tracked streamlines as a basis for estimating a structural connectome. Parameters ---------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. overlap_thr : int Number of voxels for which a given streamline must intersect with an ROI for an edge to be counted. dir_path : str Path to directory containing subject derivative data for a given pynets run. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). warped_fa : str File path to MNI-space warped FA Nifti1Image. error_margin : int Euclidean margin of error for classifying a streamline as a connection to an ROI. Default is 2 voxels. min_length : int Minimum fiber length threshold in mm to restrict tracking. fa_wei : bool Scale streamline count edges by fractional anistropy (FA). Default is False. Returns ------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. conn_matrix : array Adjacency matrix stored as an m x n array of nodes and edges. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. dir_path : str Path to directory containing subject derivative data for given run. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). min_length : int Minimum fiber length threshold in mm to restrict tracking. References ---------- .. [1] Sporns, O., Tononi, G., & Kötter, R. (2005). The human connectome: A structural description of the human brain. PLoS Computational Biology. https://doi.org/10.1371/journal.pcbi.0010042 .. [2] Sotiropoulos, S. N., & Zalesky, A. (2019). Building connectomes using diffusion MRI: why, how and but. NMR in Biomedicine. https://doi.org/10.1002/nbm.3752 .. [3] Chung, M. K., Hanson, J. L., Adluru, N., Alexander, A. L., Davidson, R. J., & Pollak, S. D. (2017). Integrative Structural Brain Network Analysis in Diffusion Tensor Imaging. Brain Connectivity. https://doi.org/10.1089/brain.2016.0481 ''' import gc import time from dipy.tracking.streamline import Streamlines, values_from_volume from dipy.tracking._utils import (_mapping_to_voxel, _to_voxel_coordinates) import networkx as nx from itertools import combinations from collections import defaultdict from pynets.core import utils, nodemaker from pynets.dmri.dmri_utils import generate_sl from dipy.io.streamline import load_tractogram from dipy.io.stateful_tractogram import Space, Origin start = time.time() # Load parcellation roi_img = nib.load(atlas_mni) atlas_data = np.around(np.asarray(roi_img.dataobj)) roi_zooms = roi_img.header.get_zooms() roi_shape = roi_img.shape # Read Streamlines streamlines = [ i.astype(np.float32) for i in Streamlines( load_tractogram(streams, roi_img, to_space=Space.RASMM, to_origin=Origin.TRACKVIS, bbox_valid_check=False).streamlines) ] roi_img.uncache() if fa_wei is True: fa_weights = values_from_volume( np.asarray(nib.load(warped_fa).dataobj), streamlines, np.eye(4)) global_fa_weights = list(utils.flatten(fa_weights)) min_global_fa_wei = min(i for i in global_fa_weights if i > 0) max_global_fa_wei = max(global_fa_weights) fa_weights_norm = [] # Here we normalize by global FA for val_list in fa_weights: fa_weights_norm.append( np.nanmean((val_list - min_global_fa_wei) / (max_global_fa_wei - min_global_fa_wei))) # Make streamlines into generators to keep memory at a minimum sl = [generate_sl(i) for i in streamlines] del streamlines # Instantiate empty networkX graph object & dictionary and create voxel-affine mapping lin_T, offset = _mapping_to_voxel(np.eye(4)) mx = len(np.unique(atlas_data.astype('uint16'))) - 1 g = nx.Graph(ecount=0, vcount=mx) edge_dict = defaultdict(int) node_dict = dict( zip(np.unique(atlas_data.astype('uint16'))[1:], np.arange(mx) + 1)) # Add empty vertices for node in range(1, mx + 1): g.add_node(node) # Build graph ix = 0 bad_idxs = [] for s in sl: # Map the streamlines coordinates to voxel coordinates and get labels for label_volume vox_coords = _to_voxel_coordinates(Streamlines(s), lin_T, offset) lab_coords = [ nodemaker.get_sphere(coord, error_margin, roi_zooms, roi_shape) for coord in vox_coords ] [i, j, k] = np.vstack(np.array(lab_coords)).T # get labels for label_volume lab_arr = atlas_data[i, j, k] endlabels = [] for ix, lab in enumerate(np.unique(lab_arr).astype('uint32')): if (lab > 0) and (np.sum(lab_arr == lab) >= overlap_thr): try: endlabels.append(node_dict[lab]) except: bad_idxs.append(ix) print( f"Label {lab} missing from parcellation. Check registration and ensure valid input " f"parcellation file.") edges = combinations(endlabels, 2) for edge in edges: lst = tuple([int(node) for node in edge]) edge_dict[tuple(sorted(lst))] += 1 edge_list = [(k[0], k[1], v) for k, v in edge_dict.items()] if fa_wei is True: # Add edgelist to g, weighted by average fa of the streamline g.add_weighted_edges_from(edge_list, weight=fa_weights_norm[ix]) else: g.add_weighted_edges_from(edge_list) ix = ix + 1 del lab_coords, lab_arr, endlabels, edges, edge_list gc.collect() if fa_wei is True: # Add average fa weights to streamline counts for u, v in list(g.edges): h = g.get_edge_data(u, v) edge_att_dict = {} for e, w in h.items(): if w not in edge_att_dict.keys(): edge_att_dict[w] = [] else: edge_att_dict[w].append(e) for key in edge_att_dict.keys(): edge_att_dict[key] = np.nanmean(edge_att_dict[key]) vals = [] for e2, w2 in edge_att_dict.items(): vals.append(float(e2) * float(w2)) g.edges[u, v].update({'weight': np.nanmean(vals)}) # Convert to numpy matrix conn_matrix_raw = nx.to_numpy_array(g) # Impose symmetry conn_matrix = np.maximum(conn_matrix_raw, conn_matrix_raw.T) print('Graph Building Complete:\n', str(time.time() - start)) if len(bad_idxs) > 0: bad_idxs = sorted(list(set(bad_idxs)), reverse=True) for j in bad_idxs: del labels[j], coords[j] coords = np.array(coords) labels = np.array(labels) return (atlas_mni, streams, conn_matrix, track_type, target_samples, dir_path, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, min_length)
def streams2graph(atlas_mni, streams, overlap_thr, dir_path, track_type, target_samples, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, warped_fa, error_margin, max_length, fa_wei=True): ''' Use tracked streamlines as a basis for estimating a structural connectome. Parameters ---------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. overlap_thr : int Number of voxels for which a given streamline must intersect with an ROI for an edge to be counted. dir_path : str Path to directory containing subject derivative data for a given pynets run. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). warped_fa : str File path to MNI-space warped FA Nifti1Image. error_margin : int Euclidean margin of error for classifying a streamline as a connection to an ROI. Default is 2 voxels. max_length : int Maximum fiber length threshold in mm to restrict tracking. fa_wei : bool Scale streamline count edges by fractional anistropy (FA). Default is False. Returns ------- atlas_mni : str File path to atlas parcellation Nifti1Image in T1w-warped MNI space. streams : str File path to streamline array sequence in .trk format. conn_matrix : array Adjacency matrix stored as an m x n array of nodes and edges. track_type : str Tracking algorithm used (e.g. 'local' or 'particle'). target_samples : int Total number of streamline samples specified to generate streams. dir_path : str Path to directory containing subject derivative data for given run. conn_model : str Connectivity reconstruction method (e.g. 'csa', 'tensor', 'csd'). 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. node_size : int Spherical centroid node size in the case that coordinate-based centroids are used as ROI's for tracking. dens_thresh : bool Indicates whether a target graph density is to be used as the basis for thresholding. ID : str A subject id or other unique identifier. roi : str File path to binarized/boolean region-of-interest Nifti1Image file. min_span_tree : bool Indicates whether local thresholding from the Minimum Spanning Tree should be used. disp_filt : bool Indicates whether local thresholding using a disparity filter and 'backbone network' should be used. parc : bool Indicates whether to use parcels instead of coordinates as ROI nodes. prune : bool Indicates whether to prune final graph of disconnected nodes/isolates. atlas : str Name of atlas parcellation used. uatlas : str File path to atlas parcellation Nifti1Image in MNI template space. labels : list List of string labels corresponding to graph nodes. coords : list List of (x, y, z) tuples corresponding to a coordinate atlas used or which represent the center-of-mass of each parcellation node. norm : int Indicates method of normalizing resulting graph. binary : bool Indicates whether to binarize resulting graph edges to form an unweighted graph. directget : str The statistical approach to tracking. Options are: det (deterministic), closest (clos), boot (bootstrapped), and prob (probabilistic). max_length : int Maximum fiber length threshold in mm to restrict tracking. ''' from dipy.tracking.streamline import Streamlines, values_from_volume from dipy.tracking._utils import (_mapping_to_voxel, _to_voxel_coordinates) import networkx as nx from itertools import combinations from collections import defaultdict from pynets.core import utils, nodemaker from dipy.io.streamline import load_tractogram from dipy.io.stateful_tractogram import Space, Origin import time # Load parcellation roi_img = nib.load(atlas_mni) atlas_data = np.around(roi_img.get_fdata()) roi_zooms = roi_img.header.get_zooms() roi_shape = roi_img.shape # Read Streamlines streamlines = Streamlines( load_tractogram(streams, roi_img, to_space=Space.RASMM, to_origin=Origin.TRACKVIS, bbox_valid_check=False).streamlines) roi_img.uncache() fa_weights = values_from_volume( nib.load(warped_fa).get_fdata(), streamlines, np.eye(4)) global_fa_weights = list(utils.flatten(fa_weights)) min_global_fa_wei = min(global_fa_weights) max_global_fa_wei = max(global_fa_weights) fa_weights_norm = [] for val_list in fa_weights: fa_weights_norm.append((val_list - min_global_fa_wei) / (max_global_fa_wei - min_global_fa_wei)) # Instantiate empty networkX graph object & dictionary and create voxel-affine mapping lin_T, offset = _mapping_to_voxel(np.eye(4)) mx = len(np.unique(atlas_data.astype('uint16'))) - 1 g = nx.Graph(ecount=0, vcount=mx) edge_dict = defaultdict(int) node_dict = dict( zip(np.unique(atlas_data.astype('uint16')) + 1, np.arange(mx) + 1)) # Add empty vertices for node in range(1, mx + 1): g.add_node(node) # Build graph start_time = time.time() ix = 0 for s in streamlines: # Map the streamlines coordinates to voxel coordinates and get labels for label_volume i, j, k = np.vstack( np.array([ nodemaker.get_sphere(coord, error_margin, roi_zooms, roi_shape) for coord in _to_voxel_coordinates(s, lin_T, offset) ])).T # get labels for label_volume lab_arr = atlas_data[i, j, k] endlabels = [] for lab in np.unique(lab_arr).astype('uint32'): if (lab > 0) and (np.sum(lab_arr == lab) >= overlap_thr): try: endlabels.append(node_dict[lab]) except UserWarning: print("%s%s%s" % ( 'Label ', lab, ' missing from parcellation. Check registration and ensure valid ' 'input parcellation file.')) edges = combinations(endlabels, 2) for edge in edges: lst = tuple([int(node) for node in edge]) edge_dict[tuple(sorted(lst))] += 1 edge_list = [(k[0], k[1], v) for k, v in edge_dict.items()] if fa_wei is True: # Add edgelist to g, weighted by average fa of the streamline g.add_weighted_edges_from(edge_list, weight=np.nanmean(fa_weights_norm[ix])) else: g.add_weighted_edges_from(edge_list) ix = ix + 1 print("%s%s%s" % ('Graph construction runtime: ', np.round(time.time() - start_time, 1), 's')) del streamlines if fa_wei is True: # Add average fa weights to streamline counts for u, v in list(g.edges): h = g.get_edge_data(u, v) edge_att_dict = {} for e, w in h.items(): if w not in edge_att_dict.keys(): edge_att_dict[w] = [] else: edge_att_dict[w].append(e) for key in edge_att_dict.keys(): edge_att_dict[key] = np.nanmean(edge_att_dict[key]) vals = [] for e2, w2 in edge_att_dict.items(): vals.append(float(e2) * float(w2)) g.edges[u, v].update({'weight': np.nanmean(vals)}) # Convert to numpy matrix conn_matrix_raw = nx.to_numpy_matrix(g) # Enforce symmetry conn_matrix = np.maximum(conn_matrix_raw, conn_matrix_raw.T) return atlas_mni, streams, conn_matrix, track_type, target_samples, dir_path, conn_model, network, node_size, dens_thresh, ID, roi, min_span_tree, disp_filt, parc, prune, atlas, uatlas, labels, coords, norm, binary, directget, max_length