def calculate_skeleton_paths_on_mesh(mesh, soma_pt=None, soma_thresh=7500, invalidation_d=10000, smooth_neighborhood=5, large_skel_path_threshold=5000, cc_vertex_thresh=100, return_map=False): """ function to turn a trimesh object of a neuron into a skeleton, without running soma collapse, or recasting result into a Skeleton. Used by :func:`meshparty.skeletonize.skeletonize_mesh` and makes use of :func:`meshparty.skeletonize.skeletonize_components` 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_thresh: 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)) 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 (10000 (nm)) smooth_neighborhood: int the neighborhood in edge hopes over which to smooth skeleton locations. This controls the smoothing of the skeleton (default 5) large_skel_path_threshold: int the threshold in terms of skeleton vertices that skeletons will be nominated for tip merging. Smaller skeleton fragments will not be merged at their tips (default 5000) cc_vertex_thresh: int the threshold in terms of vertex numbers that connected components of the mesh will be considered for skeletonization. mesh connected components with fewer than these number of vertices will be ignored by skeletonization algorithm. (default 100) return_map: bool whether to return a map of how each mesh vertex maps onto each skeleton vertex based upon how it was invalidated. Returns ------- skel_verts: np.array a Nx3 matrix of skeleton vertex positions skel_edges: np.array a Kx2 matrix of skeleton edge indices into skel_verts smooth_verts: np.array a Nx3 matrix of vertex positions after smoothing skel_verts_orig: np.array a N long index of skeleton vertices in the original mesh vertex index (mesh_to_skeleton_map): np.array a Mx2 map of mesh vertex indices to skeleton vertex indices """ skeletonize_output = skeletonize_components( mesh, soma_pt=soma_pt, soma_thresh=soma_thresh, invalidation_d=invalidation_d, cc_vertex_thresh=cc_vertex_thresh, return_map=return_map) if return_map is True: all_paths, roots, tot_path_lengths, mesh_to_skeleton_map = skeletonize_output else: all_paths, roots, tot_path_lengths = skeletonize_output all_edges = [] for comp_paths in all_paths: all_edges.append(utils.paths_to_edges(comp_paths)) tot_edges = np.vstack(all_edges) skel_verts, skel_edges, skel_verts_orig = reduce_verts( mesh.vertices, tot_edges) smooth_verts = smooth_graph(skel_verts, skel_edges, neighborhood=smooth_neighborhood) if return_map: mesh_to_skeleton_map = utils.nanfilter_shapes( np.unique(tot_edges.ravel()), mesh_to_skeleton_map) else: mesh_to_skeleton_map = None output_tuple = (skel_verts, skel_edges, smooth_verts, skel_verts_orig) if return_map: output_tuple = output_tuple + (mesh_to_skeleton_map, ) return output_tuple
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 merge_tips(mesh, all_paths, roots, tot_path_lengths, large_skel_path_threshold=5000, max_tip_d=2000): # collect all the tips of the skeletons (including roots) skel_tips = [] all_tip_indices = [] for paths, root in zip(all_paths, roots): tips = [] tip_indices = [] for path in paths: tip_ind = path[0] tip = mesh.vertices[tip_ind, :] tips.append(tip) tip_indices.append(tip_ind) root_tip = mesh.vertices[root, :] tips.append(root_tip) tip_indices.append(root) skel_tips.append(np.vstack(tips)) all_tip_indices.append(np.array(tip_indices)) # this is our overall tip matrix merged together all_tips = np.vstack(skel_tips) # and the vertex index of those tips in the original mesh all_tip_indices = np.concatenate(all_tip_indices) # variable to keep track of what component each tip was from tip_component = np.zeros(all_tips.shape[0]) # counter to keep track of an overall tip index as we go through # the components with different numbers of tips ind_counter = 0 # setup the prize collection steiner forest problem variables # prizes will be related to path length of the tip components tip_prizes = [] # where to collect all the tip<>tip edges all_edges = [] # where to collect all the tip<>tip edge weights all_edge_weights = [] # loop over all the components and their tips for k, tips, path_lengths in zip(range(len(tot_path_lengths)), skel_tips, tot_path_lengths): # how many tips in this component ntips = tips.shape[0] # calculate the total path length in this component path_len = np.sum(np.array(path_lengths)) # the prize is 0 if this is small, and the path length if big prize = path_len if path_len > large_skel_path_threshold else 0 # the cost of traveling within a skeleton is 0 if big, and the path_len if small cost = path_len if path_len <= large_skel_path_threshold else 0 # add a block of prizes to the tip prizes for this component tip_prizes.append(prize * np.ones(ntips)) # make an array of overall tip index for this component comp_tips = np.arange(ind_counter, ind_counter + ntips, dtype=np.int64) # add edges between this components root and each of the tips root_tips = (ind_counter + ntips - 1) * np.ones(ntips, dtype=np.int64) in_tip_edges = np.hstack( [root_tips[:, np.newaxis], comp_tips[:, np.newaxis]]) all_edges.append(in_tip_edges) # add a block for the cost of these edges all_edge_weights.append(cost * np.ones(ntips)) # note what component each of these tips is from tip_component[comp_tips] = k # increment our overall index counter ind_counter += ntips # gather all the prizes into a single block tip_prizes = np.concatenate(tip_prizes) # make a kdtree with all the tips tip_tree = KDTree(all_tips) # find the tips near one another close_tips = tip_tree.query_pairs(max_tip_d, output_type='ndarray') # filter out close tips from the same component diff_comp = ~(tip_component[close_tips[:, 0]] == tip_component[close_tips[:, 1]]) filt_close_tips = close_tips[diff_comp] # add these as edges all_edges.append(filt_close_tips) # with weights equal to their euclidean distance dv = np.linalg.norm(all_tips[filt_close_tips[:, 0], :] - all_tips[filt_close_tips[:, 1]], axis=1) all_edge_weights.append(dv) # consolidate the edges and weights into a single array inter_tip_weights = np.concatenate(all_edge_weights) inter_tip_edges = np.concatenate(all_edges) # run the prize collecting steiner forest optimization mst_verts, mst_edges = pcst_fast.pcst_fast(inter_tip_edges, tip_prizes, inter_tip_weights, -1, 1, 'gw', 1) # # find the set of mst edges that are between connected components new_mst_edges = mst_edges[tip_component[inter_tip_edges[mst_edges, 0]] != tip_component[inter_tip_edges[mst_edges, 1]]] good_inter_tip_edges = inter_tip_edges[new_mst_edges, :] # get these in the original index new_edges_orig_ind = all_tip_indices[good_inter_tip_edges] # # collect all the edges for all the paths into a single list # # with the original indices of the mesh orig_edges = [] for paths, root in zip(all_paths, roots): edges = utils.paths_to_edges(paths) orig_edges.append(edges) orig_edges = np.vstack(orig_edges) # and add our new mst edges tot_edges = np.vstack([orig_edges, new_edges_orig_ind]) return tot_edges