def bench_suite(): times = OrderedDict() skeleton = np.load(os.path.join(rundir, 'infected3.npz'))['skeleton'] with timer() as t_build_graph: g, indices = csr.skeleton_to_csgraph(skeleton, spacing=2.24826) times['build graph'] = t_build_graph[0] with timer() as t_build_graph2: g, indices = csr.skeleton_to_csgraph(skeleton, spacing=2.24826) times['build graph again'] = t_build_graph2[0] with timer() as t_stats: stats = csr.branch_statistics(g) times['compute statistics'] = t_stats[0] with timer() as t_stats2: stats = csr.branch_statistics(g) times['compute statistics again'] = t_stats2[0] with timer() as t_skeleton: skel_obj = csr.Skeleton(skeleton) times['skeleton object'] = t_skeleton[0] with timer() as t_skeleton2: skel_obj = csr.Skeleton(skeleton) times['skeleton object again'] = t_skeleton2[0] with timer() as t_summary: summary = csr.summarize(skel_obj) times['compute per-skeleton statistics'] = t_summary[0] return times
def test_junction_multiplicity(): """Test correct distances when a junction has more than one pixel.""" g, idxs = csr.skeleton_to_csgraph(skeleton0) assert_almost_equal(g[3, 5], 2.0155644) g, idxs = csr.skeleton_to_csgraph(skeleton0, unique_junctions=False) assert_almost_equal(g[2, 3], 1.0) assert_almost_equal(g[3, 6], np.sqrt(2))
def test_mst_junctions(): g, _ = csr.skeleton_to_csgraph(skeleton0) h = csr._mst_junctions(g) hprime, _ = csr.skeleton_to_csgraph(skeleton0) G = g.todense() G[G > 1.1] = 0 np.testing.assert_equal(G, h.todense()) np.testing.assert_equal(G, hprime.todense())
def test_networkx_plot(): g0, c0 = csr.skeleton_to_csgraph(_testdata.skeleton0) g1, c1 = csr.skeleton_to_csgraph(_testdata.skeleton1) fig, axes = plt.subplots(1, 2) draw.overlay_skeleton_networkx(g0, c0, image=_testdata.skeleton0, axis=axes[0]) draw.overlay_skeleton_networkx(g1, c1, image=_testdata.skeleton1, axis=axes[1]) # test axis=None and additional coordinates c2 = np.concatenate((c1, np.random.random(c1[:1].shape)), axis=0) draw.overlay_skeleton_networkx(g1, c2, image=_testdata.skeleton1)
def test_skeletons_swc(self): config = SkeletonWorkflow.get_config()['skeletonize'] config.update({'chunk_len': 50}) with open(os.path.join(self.config_folder, 'skeletonize.config'), 'w') as f: json.dump(config, f) self._run_skel_wf(format_='swc', max_jobs=8) # check output for correctness seg, ids = self.ids_and_seg() out_folder = os.path.join(self.output_path, self.output_prefix, 's0') for seg_id in ids: # read the result from file out_file = os.path.join(out_folder, '%i.swc' % seg_id) skel_ids, coords, parents = su.read_swc(out_file) coords = np.array(coords, dtype='float') # compute the expected result mask = seg == seg_id skel_vol = skeletonize_3d(mask) try: pix_graph, coords_exp, _ = csr.skeleton_to_csgraph(skel_vol) except ValueError: continue # check coordinates coords_exp = coords_exp[1:] self.assertEqual(coords.shape, coords_exp.shape) self.assertTrue(np.allclose(coords, coords_exp))
def thinning(obj, resolution, *args, **kwargs): """ Skeletonize object with thinning based method. Wrapper around implementation from https://scikit-image.org/docs/dev/api/skimage.morphology.html#skimage.morphology.skeletonize_3d Arguments: obj [np.ndarray] - binary object mask resolution [list] - size of the voxels in physical unit """ # skeletonize with thinning vol = skeletonize_3d(obj) # use skan to extact skeleon node coordinates and edges adj_mat, nodes, _ = csr.skeleton_to_csgraph(vol, spacing=resolution) graph = csr.csr_to_nbgraph(adj_mat) # I think we need to substract 1 here, beacuse skan uses 1-based indexing n_nodes = len(nodes) edges = np.array([[u - 1, v - 1] for u in range(1, n_nodes + 1) for v in graph.neighbors(u) if u < v], dtype='uint64') # retunr node coordinate list and edges return nodes.astype('uint64'), edges
def write_swc(output_path, skel_vol, resolution=None, invert_coords=False): """ Write skeleton to .swc For details on the swc format for skeletons, see http://research.mssm.edu/cnic/swc.html. This writes the swc catmaid flavor. Arguments: output_path [str]: output_path for swc file skel_vol [np.ndarray]: binary volume containing the skeleton resolution [list or float]: pixel resolution (default: None) invert_coords [bool]: whether to invert the coordinates This may be useful because swc expects xyz, but input is zyx (default: False) """ # extract the skeleton graph # NOTE looks like skan function names are about to change in 0.8: # csr.numba_csgraph -> csr.csr_to_nbgraph # this may fail for small skeletons with a value-error try: pix_graph, coords, _ = csr.skeleton_to_csgraph(skel_vol) except ValueError: return graph = csr.numba_csgraph(pix_graph) # map coords to resolution and invert if necessary if resolution is not None: if isinstance(resolution, float): resolution = 3 * [resolution] assert len(resolution) == 3, str(len(resolution)) coords *= resolution if invert_coords: coords = coords[:, ::-1] # TODO if this becomes a bottle-neck think about moving to numba, cython or c++ n_points = pix_graph.shape[0] with open(output_path, 'w') as f: for node_id in range(1, n_points): # swc: node-id # type (hard-coded to 0 = undefined) # coordinates # radius (hard-coded to 0.0) # parent id ngbs = graph.neighbors(node_id) # only a single neighbor -> terminal node and no parent # also, for some reasons ngbs can be empty if len(ngbs) in (0, 1): parent = -1 # two neighbors -> path node # more than two neighbors -> junction else: # TODO can we just assume that we get consistent output if we set parent to min ??? parent = np.min(ngbs) coord = coords[node_id] line = '%i 0 %f %f %f 0.0 %i \n' % (node_id, coord[0], coord[1], coord[2], parent) f.write(line)
def test_junctions(unique_junctions=True): x = np.zeros((7, 7), dtype='uint8') x[3, :] = 1 x[:, 3] = 1 print("input image") print(x) _, coords, _ = csr.skeleton_to_csgraph(x, unique_junctions=unique_junctions) print("coord for skel node 3, expected [2., 3.]") print(coords[3])
def test_tiny_cycle(): g, idxs = csr.skeleton_to_csgraph(tinycycle) expected_indptr = [0, 2, 4, 6, 8] expected_indices = [1, 2, 0, 3, 0, 3, 1, 2] expected_data = np.sqrt(2) assert_equal(g.indptr, expected_indptr) assert_equal(g.indices, expected_indices) assert_almost_equal(g.data, expected_data) assert_equal(np.ravel_multi_index(idxs, tinycycle.shape), [1, 3, 5, 7])
def test_tiny_cycle(): g, idxs, degimg = csr.skeleton_to_csgraph(tinycycle) expected_indptr = [0, 0, 2, 4, 6, 8] expected_indices = [2, 3, 1, 4, 1, 4, 2, 3] expected_data = np.sqrt(2) assert_equal(g.indptr, expected_indptr) assert_equal(g.indices, expected_indices) assert_almost_equal(g.data, expected_data) expected_degrees = np.array([[0, 2, 0], [2, 0, 2], [0, 2, 0]]) assert_equal(degimg, expected_degrees) assert_equal(np.ravel_multi_index(idxs.astype(int).T, tinycycle.shape), [0, 1, 3, 5, 7])
def test_skeleton1_stats(): g, idxs, degimg = csr.skeleton_to_csgraph(skeleton1) stats = csr.branch_statistics(g) assert_equal(stats.shape, (4, 4)) keys = map(tuple, stats[:, :2].astype(int)) dists = stats[:, 2] types = stats[:, 3].astype(int) ids2dist = dict(zip(keys, dists)) assert (13, 8) in ids2dist assert (8, 13) in ids2dist d0, d1 = sorted((ids2dist[(13, 8)], ids2dist[(8, 13)])) assert_almost_equal(d0, 1 + np.sqrt(2)) assert_almost_equal(d1, 5 * d0) assert_equal(np.bincount(types), [0, 2, 2]) assert_almost_equal(np.unique(dists), [d0, 2 + np.sqrt(2), d1])
def write_n5(ds, skel_id, skel_vol, coordinate_offset=None): """ Write skeleton to custom n5-based format The skeleton data is stored via varlen chunks: each chunk contains the data for one skeleton and stores: [n_skel_points, coord_z_0, coord_y_0, coord_x_0, ..., coord_z_n, coord_y_n, coord_x_n, n_edges, edge_0_u, edge_0_v, ..., edge_n_u, edge_n_v] Arguments: ds [z5py.Dataset]: output dataset skel_id [int]: id of the object corresponding to the skeleton skel_vol [np.ndarray]: binary volume containing the skeleton coordinate_offset [listlike]: offset to coordinate (default: None) """ # NOTE looks like skan function names are about to change in 0.8: # csr.numba_csgraph -> csr.csr_to_nbgraph # extract the skeleton graph # this may fail for small skeletons with a value-error try: pix_graph, coords, _ = csr.skeleton_to_csgraph(skel_vol) except ValueError: return graph = csr.numba_csgraph(pix_graph) # skan-indexing is 1 based, so we need to get rid of first coordinate row coords = coords[1:] # check if we have offset and add up if we do if coordinate_offset is not None: assert len(coordinate_offset) == 3 coords += coordinate_offset # make serialization for number of points and coordinates n_points = coords.shape[0] data = [np.array([n_points]), coords.flatten()] # make edges edges = [[u, v] for u in range(1, n_points + 1) for v in graph.neighbors(u) if u < v] edges = np.array(edges) # substract 1 to change to zero-based indexing edges -= 1 # add number of edges and edges to the serialization n_edges = len(edges) data.extend([np.array([n_edges]), edges.flatten()]) data = np.concatenate(data, axis=0) ds.write_chunk((skel_id, ), data.astype('uint64'), True)
def test_skeletons_n5(self): config = SkeletonWorkflow.get_config()['skeletonize'] config.update({'chunk_len': 50}) with open(os.path.join(self.config_folder, 'skeletonize.config'), 'w') as f: json.dump(config, f) self._run_skel_wf(format_='n5', max_jobs=8) # check output for correctness seg, ids = self.ids_and_seg() out_key = os.path.join(self.output_prefix, 's0') ds = z5py.File(self.output_path)[out_key] for seg_id in ids: # read the result from file coords, edges = su.read_n5(ds, seg_id) # compute the expected result mask = seg == seg_id skel_vol = skeletonize_3d(mask) try: pix_graph, coords_exp, _ = csr.skeleton_to_csgraph(skel_vol) except ValueError: continue # check coordinates coords_exp = coords_exp[1:].astype('uint64') self.assertEqual(coords.shape, coords_exp.shape) self.assertTrue(np.allclose(coords, coords_exp)) # check edges graph = csr.numba_csgraph(pix_graph) n_points = len(coords) edges_exp = [[u, v] for u in range(1, n_points + 1) for v in graph.neighbors(u) if u < v] edges_exp = np.array(edges_exp) edges_exp -= 1 self.assertEqual(edges.shape, edges_exp.shape) self.assertTrue(np.allclose(edges, edges_exp))
def skeletonize3D(cube): """ Return the 3D skeleton dkel2 and converted it in form of graph and return pixel graph as a SciPy CSR matrix in which entry (i,j) is 0 if pixels i and j are not connected, and otherwise is equal to the distance between pixels i and j in the skeleton. coordinates (in pixel units) of the points in the pixel graph. Finally, degrees is an image of the skeleton, with each skeleton pixel containing the number of neighboring pixels. Parameters ---------- cube : 3d numpy array Returns ------- dskel2 :3D skeletonized binary image(numpy array) Pixel Graph, coordinates, degree """ selem = ball(3) ddilate = dilation(cube, selem) dclose = closing(ddilate) dskel2 = skeletonize_3d(dclose) pixel_graph0, coordinates0, degrees0 = csr.skeleton_to_csgraph(dskel2) return dskel2, pixel_graph0, coordinates0, degrees0
def test_junction_multiplicity(): """Test correct distances when a junction has more than one pixel.""" g, _ = csr.skeleton_to_csgraph(skeleton0) assert_equal(g.data, 1.0) assert_equal(g[2, 5], 0.0)
counts, bins = np.histogram(values, bins='auto') counts.shape bins.shape counts, bins = np.histogram(values, bins=100) fig, ax = plt.subplots() ax.plot(bins[:-1]/2 + bins[1:]/2, counts) ax.clear() ax.hist(values, bins=bins); np.sqrt(9**2 + 3**2 + 3**2) ax.set_xlabel('distance from skan point to nearest Fiji point (µm)') ax.set_ylabel('count') fig.savefig('OP1-point-distance-histogram-new.png') np.argmax(np.argmin(dmx, axis=1)) np.argmin(dmx[79]) all_points_skan[79] all_points_fiji[99] np.argmax(dmx[np.arange(dmx.shape[0]), np.argmin(dmx, axis=1)]) np.argmin(dmx[19]) all_points_skan[19] all_points_fiji[27] all_points_fiji[27] / spacing all_points_skan[19] / spacing get_ipython().magic('pinfo csr.skeleton_to_csgraph') g, idx, deg = csr.skeleton_to_csgraph(skel1, spacing=spacing) deg[22, 146, 410] deg[20:25, 144:149, 408:413] deg[20:26, 144:149, 408:413] deg[21:28, 144:149, 408:413] # above: clear difference in the branch point location due to # cluster of junction points
#cube = fits.getdata('ngc3627_co21_12m+7m+tp_mask.fits') #Parameter : parameter = 5 cutnodes = [] selem = ball(3) ddilate = dilation(cube, selem) dclose = closing(ddilate) dskel2 = skeletonize_3d(dclose) #subcube = remove_holes[15:, 15:45, 15:45] pixel_graph0, coordinates0, degrees0 = csr.skeleton_to_csgraph(dskel2) subcube = dskel2[125:175, 300:500, 50:200] #Graph import networkx as nx pixel_graph1, coordinates1, degrees1 = csr.skeleton_to_csgraph(subcube) #print(pixel_graph1.paths_list()) nodes = range(15, 75) G = nx.from_scipy_sparse_matrix(pixel_graph1) #H = G.subgraph(nodes) #graphs = list(nx.connected_component_subgraphs(G))
def test_line(): g, idxs = csr.skeleton_to_csgraph(tinyline) assert_equal(np.ravel(idxs), [1, 2, 3]) assert_equal(g.shape, (3, 3)) # source, dest, length, type assert_equal(_old_branch_statistics(tinyline), [[0, 2, 2, 0]])
def branch_parameters_extration(distmap, skelbranch, physicspacing): """ Extract branches data from skeleton such as primary branche lengths, paths, initial and final pixels. :param skelBranches: :param physicspacing: :return: """ skeldict = { 'protusion_id': [], 'initial_node-id': [], 'initial_node-coord-0': [], 'initial_node-coord-1': [], 'final_node-id': [], 'final_node-coord-0': [], 'final_node-coord-1': [], 'euclidean-length': [], 'primary-path': [], 'secondary-paths': [], 'total-protlength': [], 'physical-space': physicspacing, } # Intermediate objects to measure the length of skeleton branches, see the skan module _, c0, _ = csr.skeleton_to_csgraph(skelbranch) # c0 : An array of shape (Nnz + 1, skel.ndim), mapping indices in graph to pixel coordinates # in degree_image or skel. branch_data = csr.summarise(skelbranch, spacing=physicspacing) # total protusion length totprotlength = branch_data['euclidean-distance'].sum() skeldict['total-protlength'] = totprotlength skeletonEdges = edgepoint_detect(distmap > 0) branchEdges = edgepoint_detect(skelbranch) removeInd = np.where((branchEdges == skeletonEdges[:, None]).all(-1))[1] endbodycoord = np.delete(branchEdges, removeInd, 0) skeldict['initial_node-coord-0'] = endbodycoord[:, 0].tolist() skeldict['initial_node-coord-1'] = endbodycoord[:, 1].tolist() # Matrix that relates nodeid in the branch_data dataframe and pixel coordinates nodeIDallPixels = c0.astype(int) # Node id of endpoints, , by using NumPy broadcasting nodeIDendBody = np.where( (nodeIDallPixels == endbodycoord[:, None]).all(-1))[1] skeldict['initial_node-id'] = nodeIDendBody.tolist() # label separate branches starting from the cell body structure = np.ones( (3, 3), dtype=np.int) # in this case we allow any kind of connection labeled, ncomponents = label(skelbranch, structure) labelValue = labeled[endbodycoord[:, 0], endbodycoord[:, 1]] skeldict['protusion_id'] = labelValue.tolist() skeldict['protusion_id'] = np.arange(ncomponents) + 1 for i in range(ncomponents): # skeleton of single protusion skelProt = labeled == labelValue[i] # protusion edges in black background endprotCoord = edgepoint_detect(skelProt) # # remove cellbody endpoint from protusion endpoint list # endpointProt[endbodycoord[i - 1, 0], endbodycoord[i - 1, 1]] = 0 removeInd = np.where((endprotCoord == endbodycoord[:, None]).all(-1))[1] endprotCoord = np.delete(endprotCoord, removeInd, 0) # node id of protusion endpoints, by using NumPy broadcasting endprotNodeID = np.where( (nodeIDallPixels == endprotCoord[:, None]).all(-1))[1] protPaths = [] protLengths = [] for coord in endprotCoord: Path, _ = route_through_array(np.invert(skelProt), start=endbodycoord[i], end=coord) Path = np.array(Path) protPaths.append(Path) Length = path_length(Path, skelProt, physicspacing) protLengths.append(Length) maxLength = np.array(protLengths).max() skeldict['euclidean-length'].append(maxLength) primaryPathID = np.array(protLengths).argmax() primaryPath = protPaths[primaryPathID] skeldict['primary-path'].append(primaryPath) # get only secondary paths del (protPaths[primaryPathID]) skeldict['secondary-paths'].append(protPaths) skeldict['final_node-id'].append(endprotNodeID[primaryPathID]) skeldict['final_node-coord-0'].append(endprotCoord[primaryPathID, 0]) skeldict['final_node-coord-1'].append(endprotCoord[primaryPathID, 1]) return skeldict
def test_line(): g, idxs, degimg = csr.skeleton_to_csgraph(tinyline) assert_equal(np.ravel(idxs), [0, 1, 2, 3]) assert_equal(degimg, [0, 1, 2, 1, 0]) assert_equal(g.shape, (4, 4)) assert_equal(csr.branch_statistics(g), [[1, 3, 2, 0]])
def test_topograph(): g, idxs, degimg = csr.skeleton_to_csgraph(topograph1d, value_is_height=True) stats = csr.branch_statistics(g) assert stats.shape == (1, 4) assert_almost_equal(stats[0], [1, 3, 2 * np.sqrt(2), 0])
def test_3d_spacing(): g, idxs, degimg = csr.skeleton_to_csgraph(skeleton3d, spacing=[5, 1, 1]) stats = csr.branch_statistics(g) assert_equal(stats.shape, (5, 4)) assert_almost_equal(stats[0], [1, 5, 10.467, 1], decimal=3) assert_equal(np.unique(stats[:, 3].astype(int)), [1, 2, 3])
def test_cycle_stats(): stats = csr.branch_statistics(csr.skeleton_to_csgraph(tinycycle)[0], buffer_size_offset=1) assert_almost_equal(stats, [[1, 1, 4 * np.sqrt(2), 3]])