def path_length(streamlines, aoi, affine, fill_value=-1): """ Computes the shortest path, along any streamline, between aoi and each voxel. Parameters ---------- streamlines : seq of (N, 3) arrays A sequence of streamlines, path length is given in mm along the curve of the streamline. aoi : array, 3d A mask (binary array) of voxels from which to start computing distance. affine : array (4, 4) The mapping from voxel indices to streamline points. fill_value : float The value of voxel in the path length map that are not connected to the aoi. Returns ------- plm : array Same shape as aoi. The minimum distance between every point and aoi along the path of a streamline. """ aoi = np.asarray(aoi, dtype=bool) # path length map plm = np.empty(aoi.shape, dtype=float) plm[:] = np.inf lin_T, offset = _mapping_to_voxel(affine, None) for sl in streamlines: seg_ind = _to_voxel_coordinates(sl, lin_T, offset) i, j, k = seg_ind.T # Get where streamlines passes through aoi breaks = aoi[i, j, k] # Where streamline passes aoi, dist is zero i, j, k = seg_ind[breaks].T plm[i, j, k] = 0 # If a streamline crosses aoi >1, re-start counting distance for each for seg in _as_segments(sl, breaks): i, j, k = _to_voxel_coordinates(seg[1:], lin_T, offset).T # Get the distance, in mm, between streamline points segment_length = np.sqrt(((seg[1:] - seg[:-1]) ** 2).sum(1)) dist = segment_length.cumsum() # Updates path length map with shorter distances minimum_at(plm, (i, j, k), dist) if fill_value != np.inf: plm = np.where(plm == np.inf, fill_value, plm) return plm
def convert_to_indices(streamline, papaya_aff, aff, img): #print(streamline) topoints = lambda x : np.array([[m["x"], m["y"], m["z"]] for m in x["world_coor"]]) points_orig = topoints(streamline) points_nifti_space = list(utils.move_streamlines([points_orig], aff, input_space=papaya_aff))[0] from dipy.tracking._utils import _to_voxel_coordinates, _mapping_to_voxel lin_T, offset = _mapping_to_voxel(aff, None) idx = _to_voxel_coordinates(points_orig, lin_T, offset) return points_nifti_space, idx
def target(streamlines, target_mask, affine, include=True): """Filters streamlines based on whether or not they pass through an ROI. Parameters ---------- streamlines : iterable A sequence of streamlines. Each streamline should be a (N, 3) array, where N is the length of the streamline. target_mask : array-like A mask used as a target. Non-zero values are considered to be within the target region. affine : array (4, 4) The affine transform from voxel indices to streamline points. include : bool, default True If True, streamlines passing through `target_mask` are kept. If False, the streamlines not passing through `target_mask` are kept. Returns ------- streamlines : generator A sequence of streamlines that pass through `target_mask`. Raises ------ ValueError When the points of the streamlines lie outside of the `target_mask`. See Also -------- density_map """ target_mask = np.array(target_mask, dtype=bool, copy=True) lin_T, offset = _mapping_to_voxel(affine, voxel_size=None) yield # End of initialization for sl in streamlines: try: ind = _to_voxel_coordinates(sl, lin_T, offset) i, j, k = ind.T state = target_mask[i, j, k] except IndexError: raise ValueError("streamlines points are outside of target_mask") if state.any() == include: yield sl
def density_map(streamlines, vol_dims, voxel_size=None, affine=None): """Counts the number of unique streamlines that pass through each voxel. Parameters ---------- streamlines : iterable A sequence of streamlines. vol_dims : 3 ints The shape of the volume to be returned containing the streamlines counts voxel_size : This argument is deprecated. affine : array_like (4, 4) The mapping from voxel coordinates to streamline points. Returns ------- image_volume : ndarray, shape=vol_dims The number of streamline points in each voxel of volume. Raises ------ IndexError When the points of the streamlines lie outside of the return volume. Notes ----- A streamline can pass through a voxel even if one of the points of the streamline does not lie in the voxel. For example a step from [0,0,0] to [0,0,2] passes through [0,0,1]. Consider subsegmenting the streamlines when the edges of the voxels are smaller than the steps of the streamlines. """ lin_T, offset = _mapping_to_voxel(affine, voxel_size) counts = np.zeros(vol_dims, 'int') for sl in streamlines: inds = _to_voxel_coordinates(sl, lin_T, offset) i, j, k = inds.T # this takes advantage of the fact that numpy's += operator only # acts once even if there are repeats in inds counts[i, j, k] += 1 return counts
def test_to_voxel_coordinates_precision(): # To simplify tests, use an identity affine. This would be the result of # a call to _mapping_to_voxel with another identity affine. transfo = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]) # Offset is computed by _mapping_to_voxel. With a 1x1x1 dataset # having no translation, the offset is half the voxel size, i.e. 0.5. offset = np.array([0.5, 0.5, 0.5]) # Without the added tolerance in _to_voxel_coordinates, this streamline # should raise an Error in the call to _to_voxel_coordinates. failing_strl = [np.array([[-0.5000001, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=np.float32)] indices = _to_voxel_coordinates(failing_strl, transfo, offset) expected_indices = np.array([[[0, 0, 0], [0, 1, 0]]]) assert_array_equal(indices, expected_indices)
def connectivity_matrix(streamlines, label_volume, voxel_size=None, affine=None, symmetric=True, return_mapping=False, mapping_as_streamlines=False): """Counts the streamlines that start and end at each label pair. Parameters ---------- streamlines : sequence A sequence of streamlines. label_volume : ndarray An image volume with an integer data type, where the intensities in the volume map to anatomical structures. voxel_size : This argument is deprecated. affine : array_like (4, 4) The mapping from voxel coordinates to streamline coordinates. symmetric : bool, True by default Symmetric means we don't distinguish between start and end points. If symmetric is True, ``matrix[i, j] == matrix[j, i]``. return_mapping : bool, False by default If True, a mapping is returned which maps matrix indices to streamlines. mapping_as_streamlines : bool, False by default If True voxel indices map to lists of streamline objects. Otherwise voxel indices map to lists of integers. Returns ------- matrix : ndarray The number of connection between each pair of regions in `label_volume`. mapping : defaultdict(list) ``mapping[i, j]`` returns all the streamlines that connect region `i` to region `j`. If `symmetric` is True mapping will only have one key for each start end pair such that if ``i < j`` mapping will have key ``(i, j)`` but not key ``(j, i)``. """ # Error checking on label_volume kind = label_volume.dtype.kind labels_positive = ((kind == 'u') or ((kind == 'i') and (label_volume.min() >= 0))) valid_label_volume = (labels_positive and label_volume.ndim == 3) if not valid_label_volume: raise ValueError("label_volume must be a 3d integer array with" "non-negative label values") # If streamlines is an iterators if return_mapping and mapping_as_streamlines: streamlines = list(streamlines) # take the first and last point of each streamline endpoints = [sl[0::len(sl)-1] for sl in streamlines] # Map the streamlines coordinates to voxel coordinates lin_T, offset = _mapping_to_voxel(affine, voxel_size) endpoints = _to_voxel_coordinates(endpoints, lin_T, offset) # get labels for label_volume i, j, k = endpoints.T endlabels = label_volume[i, j, k] if symmetric: endlabels.sort(0) mx = label_volume.max() + 1 matrix = ndbincount(endlabels, shape=(mx, mx)) if symmetric: matrix = np.maximum(matrix, matrix.T) if return_mapping: mapping = defaultdict(list) for i, (a, b) in enumerate(endlabels.T): mapping[a, b].append(i) # Replace each list of indices with the streamlines they index if mapping_as_streamlines: for key in mapping: mapping[key] = [streamlines[i] for i in mapping[key]] # Return the mapping matrix and the mapping return matrix, mapping else: return matrix
def connectivity_selection_getsl(streamlines, affine, label_volume, symmetric=True, return_mapping=True, mapping_as_streamlines=False): """Counts the streamlines that start and end at each label pair. Parameters ---------- streamlines : sequence A sequence of streamlines. affine : array_like (4, 4) The mapping from voxel coordinates to streamline coordinates. The voxel_to_rasmm matrix, typically from a NIFTI file. label_volume : ndarray An image volume with an integer data type, where the intensities in the volume map to anatomical structures. labels : tuple (2,1) The labels that are to be isolated inclusive: bool Whether to analyze the entire streamline, as opposed to just the endpoints. Allowing this will increase calculation time and mapping size, especially if mapping_as_streamlines is True. False by default. symmetric : bool, True by default Symmetric means we don't distinguish between start and end points. If symmetric is True, ``matrix[i, j] == matrix[j, i]``. return_mapping : bool, False by default If True, a mapping is returned which maps matrix indices to streamlines. mapping_as_streamlines : bool, False by default If True voxel indices map to lists of streamline objects. Otherwise voxel indices map to lists of integers. Returns ------- matrix : ndarray The number of connection between each pair of regions in `label_volume`. mapping : defaultdict(list) ``mapping[i, j]`` returns all the streamlines that connect region `i` to region `j`. If `symmetric` is True mapping will only have one key for each start end pair such that if ``i < j`` mapping will have key ``(i, j)`` but not key ``(j, i)``. """ # Error checking on label_volume kind = label_volume.dtype.kind labels_positive = ((kind == 'u') or ((kind == 'i') and (label_volume.min() >= 0))) valid_label_volume = (labels_positive and label_volume.ndim == 3) if not valid_label_volume: raise ValueError("label_volume must be a 3d integer array with" "non-negative label values") # If streamlines is an iterator if return_mapping and mapping_as_streamlines: streamlines = list(streamlines) label_dict = {} #singlecase = np.size(np.shape(label_vals)) == 1 matrix_sl = np.empty((3, ), dtype=object) for i, v in enumerate(matrix_sl): matrix_sl[i] = [v, i] for v in matrix_sl: v.append(34) if inclusive: # Create ndarray to store streamline connections edges = np.ndarray(shape=(3, 0), dtype=int) lin_T, offset = _mapping_to_voxel(affine) for sl, _ in enumerate(streamlines): # Convert streamline to voxel coordinates entire = _to_voxel_coordinates(streamlines[sl], lin_T, offset) i, j, k = entire.T if symmetric: # Create list of all labels streamline passes through entirelabels = list(OrderedDict.fromkeys(label_volume[i, j, k])) # Append all connection combinations with streamline number for comb in combinations(entirelabels, 2): if singlecase: if (comb == label_vals).all(): label_dict[tuple(label_vals)].append(sl) else: for label in label_vals: if (comb == label).all(): label_dict[tuple(label)].append(sl) edges = np.append(edges, [[comb[0]], [comb[1]], [sl]], axis=1) else: # Create list of all labels streamline passes through, keeping # order and whether a label was entered multiple times entirelabels = list(groupby(label_volume[i, j, k])) # Append connection combinations along with streamline number, # removing duplicates and connections from a label to itself combs = set(combinations([z[0] for z in entirelabels], 2)) for comb in combs: if comb[0] == comb[1]: pass else: edges = np.append(edges, [[comb[0]], [comb[1]], [sl]], axis=1) if symmetric: edges[0:2].sort(0) mx = label_volume.max() + 1 matrix = ndbincount(edges[0:2], shape=(mx, mx)) if symmetric: matrix = np.maximum(matrix, matrix.T) if return_mapping: mapping = defaultdict(list) for i, (a, b, c) in enumerate(edges.T): mapping[a, b].append(c) # Replace each list of indices with the streamlines they index if mapping_as_streamlines: for key in mapping: mapping[key] = [streamlines[i] for i in mapping[key]] return matrix, mapping return matrix else: # take the first and last point of each streamline endpoints = [sl[0::len(sl) - 1] for sl in streamlines] # Map the streamlines coordinates to voxel coordinates lin_T, offset = _mapping_to_voxel(affine) endpoints = _to_voxel_coordinates(endpoints, lin_T, offset) # get labels for label_volume i, j, k = endpoints.T endlabels = label_volume[i, j, k] if symmetric: endlabels.sort(0) mx = label_volume.max() + 1 matrix = ndbincount(endlabels, shape=(mx, mx)) if symmetric: matrix = np.maximum(matrix, matrix.T) if return_mapping: mapping = defaultdict(list) for i, (a, b) in enumerate(endlabels.T): mapping[a, b].append(i) # Replace each list of indices with the streamlines they index if mapping_as_streamlines: for key in mapping: mapping[key] = [streamlines[i] for i in mapping[key]] # Return the mapping matrix and the mapping return matrix, mapping return matrix
def streams2graph(atlas_for_streams, 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_for_streams : str File path to atlas parcellation Nifti1Image in T1w-conformed 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_for_streams : str File path to atlas parcellation Nifti1Image in T1w-conformed 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 os 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.utils import generate_sl from dipy.io.streamline import load_tractogram from dipy.io.stateful_tractogram import Space, Origin from pynets.core.utils import load_runconfig hardcoded_params = load_runconfig() 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] start = time.time() if float(roi_neighborhood_tol) <= float(error_margin): raise ValueError('roi_neighborhood_tol preset cannot be less than ' 'the value of the structural connectome error' '_margin parameter.') 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_for_streams) atlas_data = np.around(np.asarray(roi_img.dataobj)) roi_zooms = roi_img.header.get_zooms() roi_shape = roi_img.shape # Read Streamlines if streams is not None: 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 " f"parcellation 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("Redefining edges on the basis of 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("Re-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] else: print( UserWarning('No valid streamlines detected. ' 'Proceeding with an empty graph...')) mx = len(np.unique(atlas_data.astype("uint16"))) - 1 conn_matrix = np.zeros((mx, mx)) assert len(coords) == len(labels) == conn_matrix.shape[0] if network is not None: atlas_name = f"{atlas}_{network}_stage-rawgraph" else: atlas_name = f"{atlas}_stage-rawgraph" utils.save_coords_and_labels_to_json(coords, labels, dir_path, atlas_name, indices=None) coords = np.array(coords) labels = np.array(labels) if parc is True: node_size = "parc" # Save unthresholded utils.save_mat( conn_matrix, utils.create_raw_path_diff(ID, network, conn_model, roi, dir_path, node_size, target_samples, track_type, parc, directget, min_length, error_margin), ) return (atlas_for_streams, 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 connectivity_matrix(streamlines, label_volume, voxel_size=None, affine=None, symmetric=True, return_mapping=False, mapping_as_streamlines=False): """Counts the streamlines that start and end at each label pair. Parameters ---------- streamlines : sequence A sequence of streamlines. label_volume : ndarray An image volume with an integer data type, where the intensities in the volume map to anatomical structures. voxel_size : This argument is deprecated. affine : array_like (4, 4) The mapping from voxel coordinates to streamline coordinates. symmetric : bool, False by default Symmetric means we don't distinguish between start and end points. If symmetric is True, ``matrix[i, j] == matrix[j, i]``. return_mapping : bool, False by default If True, a mapping is returned which maps matrix indices to streamlines. mapping_as_streamlines : bool, False by default If True voxel indices map to lists of streamline objects. Otherwise voxel indices map to lists of integers. Returns ------- matrix : ndarray The number of connection between each pair of regions in `label_volume`. mapping : defaultdict(list) ``mapping[i, j]`` returns all the streamlines that connect region `i` to region `j`. If `symmetric` is True mapping will only have one key for each start end pair such that if ``i < j`` mapping will have key ``(i, j)`` but not key ``(j, i)``. """ # Error checking on label_volume kind = label_volume.dtype.kind labels_positive = ((kind == 'u') or ((kind == 'i') and (label_volume.min() >= 0))) valid_label_volume = (labels_positive and label_volume.ndim == 3) if not valid_label_volume: raise ValueError("label_volume must be a 3d integer array with" "non-negative label values") # If streamlines is an iterators if return_mapping and mapping_as_streamlines: streamlines = list(streamlines) # take the first and last point of each streamline endpoints = [sl[0::len(sl) - 1] for sl in streamlines] # Map the streamlines coordinates to voxel coordinates lin_T, offset = _mapping_to_voxel(affine, voxel_size) endpoints = _to_voxel_coordinates(endpoints, lin_T, offset) # get labels for label_volume i, j, k = endpoints.T endlabels = label_volume[i, j, k] if symmetric: endlabels.sort(0) mx = label_volume.max() + 1 matrix = ndbincount(endlabels, shape=(mx, mx)) if symmetric: matrix = np.maximum(matrix, matrix.T) if return_mapping: mapping = defaultdict(list) for i, (a, b) in enumerate(endlabels.T): mapping[a, b].append(i) # Replace each list of indices with the streamlines they index if mapping_as_streamlines: for key in mapping: mapping[key] = [streamlines[i] for i in mapping[key]] # Return the mapping matrix and the mapping return matrix, mapping else: return matrix
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) 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)
if ref != 'ln': ref_img_path = get_diff_ref(ref_MDT_folder, subject, ref) ref_data, ref_affine = load_nifti(ref_img_path) from dipy.tracking._utils import (_mapping_to_voxel, _to_voxel_coordinates) from collections import defaultdict, OrderedDict from itertools import combinations, groupby edges = np.ndarray(shape=(3, 0), dtype=int) lin_T, offset = _mapping_to_voxel(trkdata.space_attributes[0]) stream_ref = [] stream_point_ref = [] for sl, _ in enumerate(target_streamlines_set): # Convert streamline to voxel coordinates entire = _to_voxel_coordinates(target_streamlines_set[sl], lin_T, offset) i, j, k = entire.T ref_values = ref_data[i, j, k] stream_point_ref.append(ref_values) stream_ref.append(np.mean(ref_values)) else: stream_ref = list(length(target_streamlines)) """ from dipy.viz import window, actor from tract_visualize import show_bundles, setup_view import nibabel as nib lut_cmap = actor.colormap_lookup_table( scale_range=(0.05, 0.3)) scene = setup_view(nib.streamlines.ArraySequence(target_streamlines[33:34]), colors=lut_cmap,
def nconnectivity_matrix(streamlines, label_img, fiberlen_range, npoints, voxel_size=None, affine=None, symmetric=True, return_mapping=False, mapping_as_streamlines=False, keepfiberinroi=False): """return streamlines that start and end at each label pair. Parameters ---------- streamlines : sequence A sequence of streamlines. label_img : ndarray An image volume with an integer data type, where the intensities in the volume map to anatomical structures. voxel_size : This argument is deprecated. affine : array_like (4, 4) The mapping from voxel coordinates to streamline coordinates. symmetric : bool, False by default Symmetric means we don't distinguish between start and end points. If symmetric is True, ``matrix[i, j] == matrix[j, i]``. return_mapping : bool, False by default If True, a mapping is returned which maps matrix indices to streamlines. mapping_as_streamlines : bool, False by default If True voxel indices map to lists of streamline objects. Otherwise voxel indices map to lists of integers. keepfiberinroi: bool, False by default If True, we keep fiber curves inside each ROI. Otherwise, we only keep fibers between two ROIs Returns ------- matrix : ndarray The number of connection between each pair of regions in `label_volume`. mapping : defaultdict(list) ``mapping[i, j]`` returns all the streamlines that connect region `i` to region `j`. If `symmetric` is True mapping will only have one key for each start end pair such that if ``i < j`` mapping will have key ``(i, j)`` but not key ``(j, i)``. """ # Error checking on label_volume kind = label_img.dtype.kind labels_positive = ((kind == 'u') or ((kind == 'i') and (label_img.min() >= 0))) valid_label_volume = (labels_positive and label_img.ndim == 3) if not valid_label_volume: raise ValueError("label_volume must be a 3d integer array with" "non-negative label values") lin_T, offset = _mapping_to_voxel(affine, voxel_size) # If streamlines is an iterators if return_mapping and mapping_as_streamlines: streamlines = list(streamlines) group = defaultdict(list) group_ma = defaultdict(list) mx = label_img.max() + 1 matrix = np.zeros(shape=(mx, mx)) # check the growth step of the streamlines tmpsl = streamlines[0] interval_dist = LA.norm(tmpsl[1:-1, :] - tmpsl[0:-2, :], axis=1) if ((interval_dist.min() < 0.18) | (interval_dist.max() > 0.22)): print " the step size is not 0.2mm, this program does not work for current data " return matrix, group # for each streamline, we cut it and find how many pairs of rois it connects for sl in streamlines: slpoints = _to_voxel_coordinates(sl, lin_T, offset) ii, jj, kk = slpoints.T newlabel_img = label_img[ii, jj, kk] if keepfiberinroi: new_streamlines, num_sl, new_streamlines_startlabel, new_streamlines_endlabel = streamline_connectcut_returnfulllength( sl, newlabel_img, npoints, fiberlen_range) else: new_streamlines, num_sl, new_streamlines_startlabel, new_streamlines_endlabel = streamline_connectcut( sl, newlabel_img, npoints, fiberlen_range) if (num_sl == 1): startroi = new_streamlines_startlabel[0] endroi = new_streamlines_endlabel[0] curr_streamline = np.squeeze(new_streamlines) # get the up triangular matrix if (startroi > endroi): matrix[endroi, startroi] = matrix[endroi, startroi] + 1 group[endroi, startroi].append(curr_streamline[::-1]) else: matrix[startroi, endroi] = matrix[startroi, endroi] + 1 group[startroi, endroi].append(curr_streamline) else: for i in range(0, num_sl): startroi = new_streamlines_startlabel[i] endroi = new_streamlines_endlabel[i] curr_streamline = new_streamlines[i] # get the up triangular matrix if (startroi > endroi): matrix[endroi, startroi] = matrix[endroi, startroi] + 1 group[endroi, startroi].append(curr_streamline[::-1]) else: matrix[startroi, endroi] = matrix[startroi, endroi] + 1 group[startroi, endroi].append(curr_streamline) if return_mapping: # Return the mapping matrix and the mapping return matrix, group else: return matrix