def csgraph_binary(self): if self._csgraph_binary is None: self._csgraph_binary = utils.create_csgraph(self.vertices, self.edges, euclidean_weight=False, directed=True) return self._csgraph_binary
def _create_default_root(self): temp_graph = utils.create_csgraph(self.vertices, self.edges, euclidean_weight=True, directed=False) r = utils.find_far_points_graph(temp_graph) self.reroot(int(r[0]), reset_other_components=True)
def csgraph(self): if self._csgraph is None: self._csgraph = utils.create_csgraph(self.vertices, self.edges, euclidean_weight=True, directed=True) return self._csgraph
def _create_csgraph(self, directed=True, euclidean_weight=True): """Create the csgraph for the skeleton. """ return utils.create_csgraph(self.vertices, self.edges, euclidean_weight=euclidean_weight, directed=directed)
def soma_via_sphere(soma_pt, verts, edges, soma_d_thresh): """Get indices within soma_d_thresh of a soma_pt. Exclude vertices that left and come back.""" closest_soma_ind = np.argmin(np.linalg.norm(verts - soma_pt, axis=1)) close_inds = np.linalg.norm(verts - soma_pt, axis=1) < soma_d_thresh orig_graph = utils.create_csgraph(verts, edges, euclidean_weight=False) speye = sparse.diags(close_inds.astype(int)) _, compids = sparse.csgraph.connected_components(orig_graph * speye) return np.flatnonzero(compids[closest_soma_ind] == compids), soma_d_thresh
def _create_default_root(self): temp_graph = utils.create_csgraph( self._rooted.vertices, self._rooted.edges, euclidean_weight=True, directed=False, ) r = utils.find_far_points_graph(temp_graph) self._rooted.reroot(int(r[0]))
def _create_csgraph(self): """ Computes csgraph """ if self.mesh_edges is not None: edges = self.mesh_edges else: edges = self.edges return utils.create_csgraph(self.vertices, edges, euclidean_weight=True, directed=False)
def __init__(self, vertices, edges, vertex_properties={}, edge_properties={}, root=None): self._vertex_components = np.full(len(vertices), None) self._edge_components = np.full(len(edges), None) self._skeletons = [] self._kdtree = None self._csgraph = None self._csgraph_binary = None vertices = np.array(vertices) edges = np.array(edges) bin_csgraph = utils.create_csgraph(vertices, edges, euclidean_weight=False) nc, v_lbls = sparse.csgraph.connected_components(bin_csgraph) lbls, count = np.unique(v_lbls, return_counts=True) lbl_order = np.argsort(count)[::-1] for lbl in lbls[lbl_order]: v_filter = np.where(v_lbls == lbl)[0] vertices_f, edges_f, filters = utils.reduce_vertices( vertices, edges, v_filter=v_filter, return_filter_inds=True) vertex_properties_f = { vp_n: np.array(vp_v)[filters[0]] for vp_n, vp_v in vertex_properties.items() } edge_properties_f = { ep_n: np.array(ep_v)[filters[1]] for ep_n, ep_v in edge_properties.items() } self._vertex_components[filters[0]] = lbl self._edge_components[filters[1]] = lbl if root in v_filter: root_f = np.where(root == v_filter)[0][0] else: root_f = None self._skeletons.append( Skeleton(vertices_f, edges_f, vertex_properties_f, edge_properties_f, root=root_f))
def cut_graph(self, vinds, directed=True, euclidean_weight=True): """Return a csgraph for the skeleton with specified vertices cut off from their parent vertex. Parameters ---------- vinds : Collection of indices to cut off from their parent. directed : bool, optional Return the graph as directed, by default True euclidean_weight : bool, optional Return the graph with euclidean weights, by default True. If false, the unweighted. Returns ------- scipy.sparse.csr.csr_matrix Graph with vertices in vinds cutt off from parents. """ e_keep = ~np.isin(self.edges[:,0], vinds) es_new = self.edges[e_keep] return utils.create_csgraph(self.vertices, es_new, euclidean_weight=euclidean_weight, directed=directed)
def find_edges_to_link(mesh, vert_ind_a, vert_ind_b, distance_upper_bound=2500, verbose=False): '''Given a mesh and two points on that mesh find a way to add edges to the mesh graph so that those indices are on the same connected component Parameters ---------- mesh: trimesh_io.Mesh a mesh to find edges on vert_ind_a: int one index into mesh.vertices, the first point vert_ind_b: int a second index into mesh.vertices, the second point distance_upper_bound: float a maximum distance to (default 2500 in units of mesh.vertices) verbose: bool whether to print debug info Returns ------- np.array a Kx2 array of mesh indices that represent edges to add to the mesh to link the two points in a way that creates the shortest path between the points across mututally closest vertices from connected components.. not adding edges if they are larger than distance_upper_bound TODO: distance_upper_bound not presently implemented ''' timings = {} start_time = time.time() # find the distance between the merge points and their center d = np.linalg.norm(mesh.vertices[vert_ind_a, :] - mesh.vertices[vert_ind_b, :]) c = np.mean(mesh.vertices[[vert_ind_a, vert_ind_b], :], axis=0) # cut down the mesh to only include mesh vertices near the center of this # merge edge and within 2x the euclidean length of the edge inds = mesh.kdtree.query_ball_point(c, d * 2) # convert this to a mask mask = np.zeros(len(mesh.vertices), dtype=np.bool) mask[inds] = True timings['create_mask'] = time.time() - start_time start_time = time.time() # create a masked version of the mesh mask_mesh = mesh.apply_mask(mask) timings['apply_mask'] = time.time() - start_time start_time = time.time() ccs, labels = sparse.csgraph.connected_components(mask_mesh.csgraph, return_labels=True) # map the original indices into this masked space mask_inds = mask_mesh.filter_unmasked_indices( np.array([vert_ind_a, vert_ind_b])) timings['masked_ccs'] = time.time() - start_time start_time = time.time() # find all the multually closest edges between the linked components new_edges = find_close_edges_sym(mask_mesh.vertices, labels, labels[mask_inds[0]], labels[mask_inds[1]]) timings['find_close_edges_sym'] = time.time() - start_time start_time = time.time() # if there is now way to do this, fall back to adding all # edges that are close if len(new_edges) == 0: if verbose: print('finding all close edges') new_edges = find_all_close_edges(mask_mesh.vertices, labels, ccs) if verbose: print(f'new_edges shape {new_edges.shape}') # if there are still not edges we have a problem if len(new_edges) == 0: raise Exception('no close edges found') # create a new mesh that has these added edges #new_mesh = make_new_mesh_with_added_edges(mask_mesh, new_edges) total_edges = np.vstack([mask_mesh.graph_edges, new_edges]) graph = utils.create_csgraph(mask_mesh.vertices, total_edges) timings['make_new_mesh'] = time.time() - start_time start_time = time.time() # find the shortest path to one of the linking spots in this new mesh d_ais_to_all, pred = sparse.csgraph.dijkstra(graph, indices=mask_inds[0], unweighted=False, directed=False, return_predecessors=True) timings['find_close_edges_sym'] = time.time() - start_time start_time = time.time() # make sure we found a good path if np.isinf(d_ais_to_all[mask_inds[1]]): raise Exception( f"cannot find link between {vert_ind_a} and {vert_ind_b}") # turn this path back into a original mesh index edge list path = utils.get_path(mask_inds[0], mask_inds[1], pred) path_as_edges = utils.paths_to_edges([path]) good_edges = np_shared_rows(path_as_edges, new_edges) good_edges = np.sort(path_as_edges[good_edges], axis=1) timings['remap answers'] = time.time() - start_time if verbose: print(timings) return mask_mesh.map_indices_to_unmasked(good_edges)
def collapse_soma_skeleton(soma_pt, verts, edges, soma_d_thresh=12000, mesh_to_skeleton_map=None, soma_mesh_indices=None, return_filter=False, only_soma_component=True, return_soma_ind=False): """function to adjust skeleton result to move root to soma_pt Parameters ---------- soma_pt : numpy.array a 3 long vector of xyz locations of the soma (None to just remove duplicate ) verts : numpy.array a Nx3 array of xyz vertex locations edges : numpy.array a Kx2 array of edges of the skeleton soma_d_thresh : float distance from soma_pt to collapse skeleton nodes mesh_to_skeleton_map : np.array a M long array of how each mesh index maps to a skeleton vertex (default None). The function will update this as it collapses vertices to root. soma_mesh_indices : np.array a K long array of indices in the mesh that should be considered soma Any skeleton vertex on these vertices will all be collapsed to root. return_filter : bool whether to return a list of which skeleton vertices were used in the end for the reduced set of skeleton vertices only_soma_component : bool whether to collapse only the skeleton connected component which is closest to the soma_pt (default True) return_soma_ind : bool whether to return which skeleton index that is the soma_pt Returns ------- np.array verts, Px3 array of xyz skeleton vertices np.array edges, Qx2 array of skeleton edges (np.array) new_mesh_to_skeleton_map, returned if mesh_to_skeleton_map and soma_pt passed (np.array) used_vertices, if return_filter this contains the indices into the passed verts which the return verts is using int an index into the returned verts that is the root of the skeleton node, only returned if return_soma_ind is True """ if soma_pt is not None: if only_soma_component: closest_soma_ind = np.argmin( np.linalg.norm(verts - soma_pt, axis=1)) close_inds = np.linalg.norm(verts - soma_pt, axis=1) < soma_d_thresh orig_graph = utils.create_csgraph(verts, edges, euclidean_weight=False) speye = sparse.diags(close_inds.astype(int)) _, compids = sparse.csgraph.connected_components(orig_graph * speye) soma_verts = np.flatnonzero(compids[closest_soma_ind] == compids) else: dv = np.linalg.norm(verts - soma_pt_m, axis=1) soma_verts = np.where(dv < soma_d_thresh)[0] soma_pt_m = soma_pt[np.newaxis, :] new_verts = np.vstack((verts, soma_pt_m)) soma_i = verts.shape[0] edges_m = edges.copy() edges_m[np.isin(edges, soma_verts)] = soma_i simple_verts, simple_edges = trimesh_vtk.remove_unused_verts( new_verts, edges_m) good_edges = ~(simple_edges[:, 0] == simple_edges[:, 1]) if mesh_to_skeleton_map is not None: new_mesh_to_skeleton_map = mesh_to_skeleton_map.copy() remap_rows = np.isin(mesh_to_skeleton_map, soma_verts) new_mesh_to_skeleton_map[remap_rows] = soma_i new_mesh_to_skeleton_map = utils.nanfilter_shapes( np.unique(edges_m.ravel()), new_mesh_to_skeleton_map) if soma_mesh_indices is not None: new_mesh_to_skeleton_map[soma_mesh_indices] = len( simple_verts) - 1 output = [simple_verts, simple_edges[good_edges]] if mesh_to_skeleton_map is not None: output.append(new_mesh_to_skeleton_map) if return_filter: used_vertices = np.unique( edges_m.ravel())[: -1] #Remove the largest value which is soma_i output.append(used_vertices) if return_soma_ind: output.append(len(simple_verts) - 1) return output else: simple_verts, simple_edges = trimesh_vtk.remove_unused_verts( verts, edges) return simple_verts, simple_edges
def skeletonize_mesh(mesh, soma_pt=None, soma_radius=7500, collapse_soma=True, invalidation_d=12000, smooth_vertices=False, compute_radius=True, compute_original_index=True, verbose=True): ''' Build skeleton object from mesh skeletonization Parameters ---------- mesh: meshparty.trimesh_io.Mesh the mesh to skeletonize, defaults assume vertices in nm soma_pt: np.array a length 3 array specifying to soma location to make the root default=None, in which case a heuristic root will be chosen in units of mesh vertices. soma_radius: float distance in mesh vertex units over which to consider mesh vertices close to soma_pt to belong to soma these vertices will automatically be invalidated and no skeleton branches will attempt to reach them. This distance will also be used to collapse all skeleton points within this distance to the soma_pt root if collpase_soma is true. (default=7500 (nm)) collapse_soma: bool whether to collapse the skeleton around the soma point (default True) invalidation_d: float the distance along the mesh to invalidate when applying TEASAR like algorithm. Controls how detailed a structure the skeleton algorithm reaches. default (12000 (nm)) smooth_vertices: bool whether to smooth the vertices of the skeleton compute_radius: bool whether to calculate the radius of the skeleton at each point on the skeleton (default True) compute_original_index: bool whether to calculate how each of the mesh nodes maps onto the skeleton (default True) verbose: bool whether to print verbose logging Returns ------- :obj:`meshparty.skeleton.Skeleton` a Skeleton object for this mesh ''' skel_verts, skel_edges, smooth_verts, orig_skel_index, skel_map = calculate_skeleton_paths_on_mesh( mesh, soma_pt=soma_pt, soma_thresh=soma_radius, invalidation_d=invalidation_d, return_map=True) if smooth_vertices is True: skel_verts = smooth_verts if collapse_soma is True and soma_pt is not None: soma_verts = mesh_filters.filter_spatial_distance_from_points( mesh, [soma_pt], soma_radius) new_v, new_e, new_skel_map, vert_filter, root_ind = collapse_soma_skeleton( soma_pt, skel_verts, skel_edges, soma_d_thresh=soma_radius, mesh_to_skeleton_map=skel_map, soma_mesh_indices=soma_verts, return_filter=True, return_soma_ind=True) else: new_v, new_e, new_skel_map = skel_verts, skel_edges, skel_map vert_filter = np.arange(len(orig_skel_index)) if soma_pt is None: sk_graph = utils.create_csgraph(new_v, new_e) root_ind = utils.find_far_points_graph(sk_graph)[0] else: _, qry_inds = pyKDTree(new_v).query( soma_pt[np.newaxis, :]) # Still try to root close to the soma root_ind = qry_inds[0] skel_map_full_mesh = np.full(mesh.node_mask.shape, -1, dtype=np.int64) skel_map_full_mesh[mesh.node_mask] = new_skel_map ind_to_fix = mesh.map_boolean_to_unmasked(np.isnan(new_skel_map)) skel_map_full_mesh[ind_to_fix] = -1 props = {} if compute_original_index is True: props['mesh_index'] = np.append( mesh.map_indices_to_unmasked(orig_skel_index[vert_filter]), -1) if compute_radius is True: rs = ray_trace_distance(orig_skel_index[vert_filter], mesh) rs = np.append(rs, soma_radius) props['rs'] = rs sk = Skeleton(new_v, new_e, mesh_to_skel_map=skel_map_full_mesh, vertex_properties=props, root=root_ind) return sk
def skeletonize_mesh( mesh, soma_pt=None, soma_radius=7500, collapse_soma=True, collapse_function="sphere", invalidation_d=12000, smooth_vertices=False, compute_radius=True, shape_function="single", compute_original_index=True, verbose=True, smooth_iterations=12, smooth_neighborhood=2, smooth_r=0.1, cc_vertex_thresh=100, root_index=None, remove_zero_length_edges=True, collapse_params={}, meta={}, ): """ Build skeleton object from mesh skeletonization Parameters ---------- mesh: meshparty.trimesh_io.Mesh the mesh to skeletonize, defaults assume vertices in nm soma_pt: np.array a length 3 array specifying to soma location to make the root default=None, in which case a heuristic root will be chosen in units of mesh vertices. soma_radius: float distance in mesh vertex units over which to consider mesh vertices close to soma_pt to belong to soma these vertices will automatically be invalidated and no skeleton branches will attempt to reach them. This distance will also be used to collapse all skeleton points within this distance to the soma_pt root if collpase_soma is true. (default=7500 (nm)) collapse_soma: bool whether to collapse the skeleton around the soma point (default True) collapse_function: 'sphere' or 'branch' Determines which soma collapse function to use. Sphere uses the soma_radius and collapses all vertices within that radius to the soma. Branch is an experimental approach that tries to compute the right boundary for each branch into soma. invalidation_d: float the distance along the mesh to invalidate when applying TEASAR like algorithm. Controls how detailed a structure the skeleton algorithm reaches. default (12000 (nm)) smooth_vertices: bool whether to smooth the vertices of the skeleton compute_radius: bool whether to calculate the radius of the skeleton at each point on the skeleton (default True) shape_function: 'single' or 'cone' Selects how to compute the radius, either with a single ray or a cone of rays. Default is 'single'. compute_original_index: bool whether to calculate how each of the mesh nodes maps onto the skeleton (default True) smooth_iterations: int, optional Number of iterations to smooth (default is 12) smooth_neighborhood: int, optional Size of neighborhood to look at for smoothing smooth_r: float, optional Weight of update step in smoothing algorithm, default is 0.2 root_index: int or None, optional A precise mesh vertex to use as the skeleton root. If provided, the vertex location overrides soma_pt. By default, None. remove_zero_length_edges: bool If True, removes vertices involved in zero length edges, which can disrupt graph computations. Default True. collapse_params: dict Extra keyword arguments for the collapse function. See soma_via_sphere and soma_via_branch_starts for specifics. cc_vertex_thresh : int, optional Smallest number of vertices in a connected component to skeletonize. verbose: bool whether to print verbose logging meta: dict Skeletonization metadata to add to the skeleton. See skeleton.SkeletonMetadata for keys. Returns ------- :obj:`meshparty.skeleton.Skeleton` a Skeleton object for this mesh """ ( skel_verts, skel_edges, orig_skel_index, skel_map, ) = calculate_skeleton_paths_on_mesh( mesh, invalidation_d=invalidation_d, cc_vertex_thresh=cc_vertex_thresh, root_index=root_index, return_map=True, ) if smooth_vertices is True: smooth_verts = smooth_graph( skel_verts, skel_edges, neighborhood=smooth_neighborhood, iterations=smooth_iterations, r=smooth_r, ) skel_verts = smooth_verts if root_index is not None and soma_pt is None: soma_pt = mesh.vertices[root_index] if soma_pt is not None: soma_pt = np.array(soma_pt).reshape(1, 3) rs = None if collapse_soma is True and soma_pt is not None: temp_sk = Skeleton( skel_verts, skel_edges, mesh_index=mesh.map_indices_to_unmasked(orig_skel_index), mesh_to_skel_map=skel_map, ) _, close_ind = temp_sk.kdtree.query(soma_pt) temp_sk.reroot(close_ind[0]) if collapse_function == "sphere": soma_verts, soma_r = soma_via_sphere( soma_pt, temp_sk.vertices, temp_sk.edges, soma_radius ) elif collapse_function == "branch": if shape_function == "single": rs = ray_trace_distance( mesh.filter_unmasked_indices_padded(temp_sk.mesh_index), mesh ) elif shape_function == "cone": rs = shape_diameter_function( mesh.filter_unmasked_indices_padded(temp_sk.mesh_index), mesh, num_points=30, cone_angle=np.pi / 3, ) soma_verts, soma_r = soma_via_branch_starts( temp_sk, mesh, soma_pt, rs, search_radius=collapse_params.get("search_radius", 25000), fallback_radius=collapse_params.get("fallback_radius", soma_radius), cutoff_threshold=collapse_params.get("cutoff_threshold", 0.4), min_cutoff=collapse_params.get("min_cutoff", 0.1), dynamic_range=collapse_params.get("dynamic_range", 1), dynamic_threshold=collapse_params.get("dynamic_threshold", False), ) if root_index is not None: collapse_index = np.flatnonzero(orig_skel_index == root_index)[0] else: collapse_index = None new_v, new_e, new_skel_map, vert_filter, root_ind = collapse_soma_skeleton( soma_verts, soma_pt, temp_sk.vertices, temp_sk.edges, mesh_to_skeleton_map=temp_sk.mesh_to_skel_map, collapse_index=collapse_index, return_filter=True, return_soma_ind=True, ) else: new_v, new_e, new_skel_map = skel_verts, skel_edges, skel_map vert_filter = np.arange(len(orig_skel_index)) if root_index is not None: root_ind = np.flatnonzero(orig_skel_index == root_index)[0] elif soma_pt is None: sk_graph = utils.create_csgraph(new_v, new_e) root_ind = utils.find_far_points_graph(sk_graph)[0] else: # Still try to root close to the soma _, qry_inds = spatial.cKDTree(new_v, balanced_tree=False).query( soma_pt[np.newaxis, :] ) root_ind = qry_inds[0] skel_map_full_mesh = np.full(mesh.node_mask.shape, -1, dtype=np.int64) skel_map_full_mesh[mesh.node_mask] = new_skel_map ind_to_fix = mesh.map_boolean_to_unmasked(np.isnan(new_skel_map)) skel_map_full_mesh[ind_to_fix] = -1 props = {} if compute_original_index is True: if collapse_soma is True and soma_pt is not None: mesh_index = temp_sk.mesh_index[vert_filter] if root_index is None: mesh_index = np.append(mesh_index, -1) else: mesh_index = orig_skel_index[vert_filter] props["mesh_index"] = mesh_index if compute_radius is True: if rs is None: if shape_function == "single": rs = ray_trace_distance(orig_skel_index[vert_filter], mesh) elif shape_function == "cone": rs = shape_diameter_function(orig_skel_index[vert_filter], mesh) else: rs = rs[vert_filter] if collapse_soma is True and soma_pt is not None: if root_index is None: rs = np.append(rs, soma_r) else: rs[root_ind] = soma_r props["rs"] = rs sk_params = { "soma_pt_x": soma_pt[0, 0] if soma_pt is not None else None, "soma_pt_y": soma_pt[0, 1] if soma_pt is not None else None, "soma_pt_z": soma_pt[0, 2] if soma_pt is not None else None, "soma_radius": soma_radius, "collapse_soma": collapse_soma, "collapse_function": collapse_function, "invalidation_d": invalidation_d, "smooth_vertices": smooth_vertices, "compute_radius": compute_radius, "shape_function": shape_function, "smooth_iterations": smooth_iterations, "smooth_neighborhood": smooth_neighborhood, "smooth_r": smooth_r, "cc_vertex_thresh": cc_vertex_thresh, "remove_zero_length_edges": remove_zero_length_edges, "collapse_params": collapse_params, "timestamp": time.time(), } sk_params.update(meta) sk = Skeleton( new_v, new_e, mesh_to_skel_map=skel_map_full_mesh, mesh_index=props.get("mesh_index", None), radius=props.get("rs", None), root=root_ind, remove_zero_length_edges=remove_zero_length_edges, meta=sk_params, ) if compute_radius is True: _remove_nan_radius(sk) return sk
def _create_csgraph(self): """ Computes csgraph """ return utils.create_csgraph(self.vertices, self.graph_edges, euclidean_weight=True, directed=False)
def _create_csgraph(self, directed=True, euclidean_weight=True): return utils.create_csgraph(self.vertices, self.edges, euclidean_weight=euclidean_weight, directed=directed)