def test_cable_length(): skel = PrecomputedSkeleton([ (0,0,0), (1,0,0), (2,0,0), (3,0,0), (4,0,0), (5,0,0) ], edges=[ (1,0), (1,2), (2,3), (3,4), (5,4) ], radii=[ 1, 2, 3, 4, 5, 6 ], vertex_types=[1, 2, 3, 4, 5, 6] ) assert skel.cable_length() == (skel.vertices.shape[0] - 1) skel = PrecomputedSkeleton([ (2,0,0), (1,0,0), (0,0,0), (0,5,0), (0,6,0), (0,7,0) ], edges=[ (1,0), (1,2), (2,3), (3,4), (5,4) ], radii=[ 1, 2, 3, 4, 5, 6 ], vertex_types=[1, 2, 3, 4, 5, 6] ) assert skel.cable_length() == 9 skel = PrecomputedSkeleton([ (1,1,1), (0,0,0), (1,0,0) ], edges=[ (1,0), (1,2) ], radii=[ 1, 2, 3], vertex_types=[1, 2, 3] ) assert abs(skel.cable_length() - (math.sqrt(3) + 1)) < 1e-6
def test_components(): skel = PrecomputedSkeleton( [ (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (0, 2, 0), (0, 3, 0), ], edges=[(0, 1), (1, 2), (3, 4), (4, 5), (3, 5)], segid=666, ) components = skel.components() assert len(components) == 2 assert components[0].vertices.shape[0] == 3 assert components[1].vertices.shape[0] == 3 assert components[0].edges.shape[0] == 2 assert components[1].edges.shape[0] == 3 skel1_gt = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0), (2, 0, 0)], [(0, 1), (1, 2)]) skel2_gt = PrecomputedSkeleton([(0, 1, 0), (0, 2, 0), (0, 3, 0)], [(0, 1), (0, 2), (1, 2)]) assert PrecomputedSkeleton.equivalent(components[0], skel1_gt) assert PrecomputedSkeleton.equivalent(components[1], skel2_gt)
def test_consolidate(): skel = PrecomputedSkeleton( vertices=np.array([ (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 0, 0), (2, 1, 0), (2, 2, 0), (2, 2, 1), (2, 2, 2), ], dtype=np.float32), edges=np.array([ [0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], ], dtype=np.uint32), radii=np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.float32), vertex_types=np.array([0, 1, 2, 3, 4, 5, 6, 7], dtype=np.uint8), ) correct_skel = PrecomputedSkeleton( vertices=np.array([ (0, 0, 0), (1, 0, 0), (2, 0, 0), (2, 1, 0), (2, 2, 0), (2, 2, 1), (2, 2, 2), ], dtype=np.float32), edges=np.array([ [0, 1], [0, 2], [0, 3], [1, 2], [3, 4], [4, 5], [5, 6], ], dtype=np.uint32), radii=np.array([0, 1, 2, 4, 5, 6, 7], dtype=np.float32), vertex_types=np.array([0, 1, 2, 4, 5, 6, 7], dtype=np.uint8), ) consolidated = skel.consolidate() assert np.all(consolidated.vertices == correct_skel.vertices) assert np.all(consolidated.edges == correct_skel.edges) assert np.all(consolidated.radii == correct_skel.radii) assert np.all(consolidated.vertex_types == correct_skel.vertex_types)
def test_downsample(): skel = PrecomputedSkeleton([ (0,0,0), (1,0,0), (1,1,0), (1,1,3), (2,1,3), (2,2,3) ], edges=[ (1,0), (1,2), (2,3), (3,4), (5,4) ], radii=[ 1, 2, 3, 4, 5, 6 ], vertex_types=[1, 2, 3, 4, 5, 6], segid=1337, ) def should_error(x): try: skel.downsample(x) assert False except ValueError: pass should_error(-1) should_error(0) should_error(.5) should_error(2.00000000000001) dskel = skel.downsample(1) assert PrecomputedSkeleton.equivalent(dskel, skel) assert dskel.id == skel.id assert dskel.id == 1337 dskel = skel.downsample(2) dskel_gt = PrecomputedSkeleton( [ (0,0,0), (1,1,0), (2,1,3), (2,2,3) ], edges=[ (1,0), (1,2), (2,3) ], radii=[1,3,5,6], vertex_types=[1,3,5,6] ) assert PrecomputedSkeleton.equivalent(dskel, dskel_gt) dskel = skel.downsample(3) dskel_gt = PrecomputedSkeleton( [ (0,0,0), (1,1,3), (2,2,3) ], edges=[ (1,0), (1,2) ], radii=[1,4,6], vertex_types=[1,4,6], ) assert PrecomputedSkeleton.equivalent(dskel, dskel_gt) skel = PrecomputedSkeleton([ (0,0,0), (1,0,0), (1,1,0), (1,1,3), (2,1,3), (2,2,3) ], edges=[ (1,0), (1,2), (3,4), (5,4) ], radii=[ 1, 2, 3, 4, 5, 6 ], vertex_types=[1, 2, 3, 4, 5, 6] ) dskel = skel.downsample(2) dskel_gt = PrecomputedSkeleton( [ (0,0,0), (1,1,0), (1,1,3), (2,2,3) ], edges=[ (1,0), (2,3) ], radii=[1,3,4,6], vertex_types=[1,3,4,6] ) assert PrecomputedSkeleton.equivalent(dskel, dskel_gt)
def test_downsample_joints(): skel = PrecomputedSkeleton([ (2, 3,0), # 0 (2, 2,0), # 1 (2, 1,0), # 2 (0,0,0), (1,0,0), (2, 0,0), (3,0,0), (4,0,0), # 3, 4, 5, 6, 7 (2,-1,0), # 8 (2,-2,0), # 9 (2,-3,0), # 10 ], edges=[ (0, 1), (1, 2), (2, 5), (3,4), (4,5), (5, 6), (6,7), (5, 8), (8, 9), (9,10) ], radii=[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], vertex_types=[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ], segid=1337, ) ds_skel = skel.downsample(2) ds_skel_gt = PrecomputedSkeleton([ (2, 3,0), # 0 (2, 2,0), # 1 (0,0,0), (2, 0,0), (4,0,0), # 2, 3, 4 (2,-2,0), # 5 (2,-3,0), # 6 ], edges=[ (0,1), (1,3), (2,3), (3,4), (3,5), (5,6) ], radii=[ 0, 1, 3, 5, 7, 9, 10 ], vertex_types=[ 0, 1, 3, 5, 7, 9, 10 ], segid=1337, ) assert PrecomputedSkeleton.equivalent(ds_skel, ds_skel_gt)
def fuse_skeletons(self, skels): if len(skels) == 0: return PrecomputedSkeleton() bbxs = [ item[0] for item in skels ] skeletons = [ item[1] for item in skels ] skeletons = self.crop_skels(bbxs, skeletons) skeletons = [ s for s in skeletons if not s.empty() ] if len(skeletons) == 0: return PrecomputedSkeleton() return PrecomputedSkeleton.simple_merge(skeletons).consolidate()
def test_caching(): vol = CloudVolume('file:///tmp/cloudvolume/test-skeletons', info=info, cache=True) vol.cache.flush() skel = PrecomputedSkeleton( [ (0, 0, 0), (1, 0, 0), (2, 0, 0), (0, 1, 0), (0, 2, 0), (0, 3, 0), ], edges=[(0, 1), (1, 2), (3, 4), (4, 5), (3, 5)], segid=666, ) vol.skeleton.upload(skel) assert vol.cache.list_skeletons() == ['666.gz'] skel.id = 1 with open(os.path.join(vol.cache.path, 'skeletons/1'), 'wb') as f: f.write(skel.encode()) cached_skel = vol.skeleton.get(1) assert cached_skel == skel vol.cache.flush()
def test_read_swc(): # From http://research.mssm.edu/cnic/swc.html test_file = """# ORIGINAL_SOURCE NeuronStudio 0.8.80 # CREATURE # REGION # FIELD/LAYER # TYPE # CONTRIBUTOR # REFERENCE # RAW # EXTRAS # SOMA_AREA # SHINKAGE_CORRECTION 1.0 1.0 1.0 # VERSION_NUMBER 1.0 # VERSION_DATE 2007-07-24 # SCALE 1.0 1.0 1.0 1 1 14.566132 34.873772 7.857000 0.717830 -1 2 0 16.022520 33.760513 7.047000 0.463378 1 3 5 17.542000 32.604973 6.885001 0.638007 2 4 0 19.163984 32.022469 5.913000 0.602284 3 5 0 20.448090 30.822802 4.860000 0.436025 4 6 6 21.897903 28.881084 3.402000 0.471886 5 7 0 18.461960 30.289471 8.586000 0.447463 3 8 6 19.420759 28.730757 9.558000 0.496217 7""" skel = PrecomputedSkeleton.from_swc(test_file) assert skel.vertices.shape[0] == 8 assert skel.edges.shape[0] == 7 skel_gt = PrecomputedSkeleton( vertices=[ [14.566132, 34.873772, 7.857000], [16.022520, 33.760513, 7.047000], [17.542000, 32.604973, 6.885001], [19.163984, 32.022469, 5.913000], [20.448090, 30.822802, 4.860000], [21.897903, 28.881084, 3.402000], [18.461960, 30.289471, 8.586000], [19.420759, 28.730757, 9.558000] ], edges=[ (0,1), (1,2), (2,3), (3,4), (4,5), (2,6), (7,6) ], radii=[ 0.717830, 0.463378, 0.638007, 0.602284, 0.436025, 0.471886, 0.447463, 0.496217 ], vertex_types=[ 1, 0, 5, 0, 0, 6, 0, 6 ], ) assert PrecomputedSkeleton.equivalent(skel, skel_gt)
def test_remove_disconnected_vertices(): skel = PrecomputedSkeleton( [ (0,0,0), (1,0,0), (2,0,0), (0,1,0), (0,2,0), (0,3,0), (-1, -1, -1) ], edges=[ (0,1), (1,2), (3,4), (4,5), (3,5) ], segid=666, ) res = skel.remove_disconnected_vertices() assert res.vertices.shape[0] == 6 assert res.edges.shape[0] == 5 assert res.radii.shape[0] == 6 assert res.vertex_types.shape[0] == 6 assert res.id == 666
def trace( labels, DBF, scale=10, const=10, anisotropy=(1, 1, 1), soma_detection_threshold=1100, soma_acceptance_threshold=4000, pdrf_scale=5000, pdrf_exponent=16, soma_invalidation_scale=0.5, soma_invalidation_const=0, fix_branching=True, manual_targets_before=[], manual_targets_after=[], root=None, max_paths=None, voxel_graph=None, ): """ Given the euclidean distance transform of a label ("Distance to Boundary Function"), convert it into a skeleton using an algorithm based on TEASAR. DBF: Result of the euclidean distance transform. Must represent a single label, assumed to be expressed in chosen physical units (i.e. nm) scale: during the "rolling ball" invalidation phase, multiply the DBF value by this. const: during the "rolling ball" invalidation phase, this is the minimum radius in chosen physical units (i.e. nm). anisotropy: (x,y,z) conversion factor for voxels to chosen physical units (i.e. nm) soma_detection_threshold: if object has a DBF value larger than this, root will be placed at largest DBF value and special one time invalidation will be run over that root location (see soma_invalidation scale) expressed in chosen physical units (i.e. nm) pdrf_scale: scale factor in front of dbf, used to weight dbf over euclidean distance (higher to pay more attention to dbf) (default 5000) pdrf_exponent: exponent in dbf formula on distance from edge, faster if factor of 2 (default 16) soma_invalidation_scale: the 'scale' factor used in the one time soma root invalidation (default .5) soma_invalidation_const: the 'const' factor used in the one time soma root invalidation (default 0) (units in chosen physical units (i.e. nm)) fix_branching: When enabled, zero out the graph edge weights traversed by of previously found paths. This causes branch points to occur closer to the actual path divergence. However, there is a large performance penalty associated with this as dijkstra's algorithm is computed once per a path rather than once per a skeleton. manual_targets_before: list of (x,y,z) that correspond to locations that must have paths drawn to. Used for specifying root and border targets for merging adjacent chunks out-of-core. Targets are applied before ordinary target selection. manual_targets_after: Same as manual_targets_before but the additional targets are applied after the usual algorithm runs. The current invalidation status of the shape makes no difference. max_paths: If a label requires drawing this number of paths or more, abort and move onto the next label. root: If you want to force the root to be a particular voxel, you can specify it here. voxel_graph: a connection graph that defines permissible directions of motion between voxels. This is useful for dealing with self-touches. The graph is defined by the conventions used in cc3d.voxel_connectivity_graph (https://github.com/seung-lab/connected-components-3d/blob/3.2.0/cc3d_graphs.hpp#L73-L92) Based on the algorithm by: M. Sato, I. Bitter, M. Bender, A. Kaufman, and M. Nakajima. "TEASAR: tree-structure extraction algorithm for accurate and robust skeletons" Proc. the Eighth Pacific Conference on Computer Graphics and Applications. Oct. 2000. doi:10.1109/PCCGA.2000.883951 (https://ieeexplore.ieee.org/document/883951/) Returns: Skeleton object """ dbf_max = np.max(DBF) labels = np.asfortranarray(labels) DBF = np.asfortranarray(DBF) soma_mode = False # > 5000 nm, gonna be a soma or blood vessel # For somata: specially handle the root by # placing it at the approximate center of the soma if dbf_max > soma_detection_threshold: labels, num_voxels_filled = fill_voids.fill(labels, in_place=True, return_fill_count=True) if num_voxels_filled > 0: del DBF DBF = edt.edt(labels, anisotropy=anisotropy, order='F', black_border=np.all(labels)) dbf_max = np.max(DBF) soma_mode = dbf_max > soma_acceptance_threshold soma_radius = 0.0 if soma_mode: if root is not None: manual_targets_before.insert(0, root) root = find_soma_root(DBF, dbf_max) soma_radius = dbf_max * soma_invalidation_scale + soma_invalidation_const elif root is None: root = find_root(labels, anisotropy) if root is None: return PrecomputedSkeleton() free_space_radius = 0 if not soma_mode else DBF[root] # DBF: Distance to Boundary Field # DAF: Distance from any voxel Field (distance from root field) # PDRF: Penalized Distance from Root Field DBF = kimimaro.skeletontricks.zero2inf(DBF) # DBF[ DBF == 0 ] = np.inf DAF, target = dijkstra3d.euclidean_distance_field( labels, root, anisotropy=anisotropy, free_space_radius=free_space_radius, voxel_graph=voxel_graph, return_max_location=True, ) DAF = kimimaro.skeletontricks.inf2zero(DAF) # DAF[ DAF == np.inf ] = 0 PDRF = compute_pdrf(dbf_max, pdrf_scale, pdrf_exponent, DBF, DAF) # Use dijkstra propogation w/o a target to generate a field of # pointers from each voxel to its parent. Then we can rapidly # compute multiple paths by simply hopping pointers using path_from_parents if not fix_branching: parents = dijkstra3d.parental_field(PDRF, root, voxel_graph) del PDRF else: parents = PDRF if soma_mode: invalidated, labels = kimimaro.skeletontricks.roll_invalidation_ball( labels, DBF, np.array([root], dtype=np.uint32), scale=soma_invalidation_scale, const=soma_invalidation_const, anisotropy=anisotropy) # This target is only valid if no # invalidations have occured yet. elif len(manual_targets_before) == 0: manual_targets_before.append(target) # delete reference to DAF and place it in # a list where we can delete it later and # free that memory. DAF = [DAF] paths = compute_paths(root, labels, DBF, DAF, parents, scale, const, anisotropy, soma_mode, soma_radius, fix_branching, manual_targets_before, manual_targets_after, max_paths, voxel_graph) skel = PrecomputedSkeleton.simple_merge([ PrecomputedSkeleton.from_path(path) for path in paths if len(path) > 0 ]).consolidate() verts = skel.vertices.flatten().astype(np.uint32) skel.radii = DBF[verts[::3], verts[1::3], verts[2::3]] return skel
def test_equivalent(): assert PrecomputedSkeleton.equivalent(PrecomputedSkeleton(), PrecomputedSkeleton()) identity = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0)], [(0, 1)]) assert PrecomputedSkeleton.equivalent(identity, identity) diffvertex = PrecomputedSkeleton([(0, 0, 0), (0, 1, 0)], [(0, 1)]) assert not PrecomputedSkeleton.equivalent(identity, diffvertex) single1 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0)], edges=[(1, 0)]) single2 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0)], edges=[(0, 1)]) assert PrecomputedSkeleton.equivalent(single1, single2) double1 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0)], edges=[(1, 0)]) double2 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0)], edges=[(0, 1)]) assert PrecomputedSkeleton.equivalent(double1, double2) double1 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0), (1, 1, 0)], edges=[(1, 0), (1, 2)]) double2 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0), (1, 1, 0)], edges=[(2, 1), (0, 1)]) assert PrecomputedSkeleton.equivalent(double1, double2) double1 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 3)], edges=[(1, 0), (1, 2), (1, 3)]) double2 = PrecomputedSkeleton([(0, 0, 0), (1, 0, 0), (1, 1, 0), (1, 1, 3)], edges=[(3, 1), (2, 1), (0, 1)]) assert PrecomputedSkeleton.equivalent(double1, double2)
def trace( labels, DBF, scale=10, const=10, anisotropy=(1, 1, 1), soma_detection_threshold=1100, soma_acceptance_threshold=4000, pdrf_scale=5000, pdrf_exponent=16, soma_invalidation_scale=0.5, soma_invalidation_const=0, fix_branching=True, ): """ Given the euclidean distance transform of a label ("Distance to Boundary Function"), convert it into a skeleton using an algorithm based on TEASAR. DBF: Result of the euclidean distance transform. Must represent a single label, assumed to be expressed in chosen physical units (i.e. nm) scale: during the "rolling ball" invalidation phase, multiply the DBF value by this. const: during the "rolling ball" invalidation phase, this is the minimum radius in chosen physical units (i.e. nm). anisotropy: (x,y,z) conversion factor for voxels to chosen physical units (i.e. nm) soma_detection_threshold: if object has a DBF value larger than this, root will be placed at largest DBF value and special one time invalidation will be run over that root location (see soma_invalidation scale) expressed in chosen physical units (i.e. nm) pdrf_scale: scale factor in front of dbf, used to weight dbf over euclidean distance (higher to pay more attention to dbf) (default 5000) pdrf_exponent: exponent in dbf formula on distance from edge, faster if factor of 2 (default 16) soma_invalidation_scale: the 'scale' factor used in the one time soma root invalidation (default .5) soma_invalidation_const: the 'const' factor used in the one time soma root invalidation (default 0) (units in chosen physical units (i.e. nm)) fix_branching: When enabled, zero out the graph edge weights traversed by of previously found paths. This causes branch points to occur closer to the actual path divergence. However, there is a large performance penalty associated with this as dijkstra's algorithm is computed once per a path rather than once per a skeleton. Based on the algorithm by: M. Sato, I. Bitter, M. Bender, A. Kaufman, and M. Nakajima. "TEASAR: tree-structure extraction algorithm for accurate and robust skeletons" Proc. the Eighth Pacific Conference on Computer Graphics and Applications. Oct. 2000. doi:10.1109/PCCGA.2000.883951 (https://ieeexplore.ieee.org/document/883951/) Returns: Skeleton object """ dbf_max = np.max(DBF) labels = np.asfortranarray(labels) DBF = np.asfortranarray(DBF) soma_mode = False # > 5000 nm, gonna be a soma or blood vessel # For somata: specially handle the root by # placing it at the approximate center of the soma if dbf_max > soma_detection_threshold: del DBF labels = ndimage.binary_fill_holes(labels) labels = np.asfortranarray(labels) DBF = edt.edt(labels, anisotropy=anisotropy, order='F') dbf_max = np.max(DBF) soma_mode = dbf_max > soma_acceptance_threshold if soma_mode: root = np.unravel_index(np.argmax(DBF), DBF.shape) soma_radius = dbf_max * soma_invalidation_scale + soma_invalidation_const else: root = find_root(labels, anisotropy) soma_radius = 0.0 if root is None: return PrecomputedSkeleton() # DBF: Distance to Boundary Field # DAF: Distance from any voxel Field (distance from root field) # PDRF: Penalized Distance from Root Field DBF = kimimaro.skeletontricks.zero2inf(DBF) # DBF[ DBF == 0 ] = np.inf DAF = dijkstra3d.euclidean_distance_field(labels, root, anisotropy=anisotropy) DAF = kimimaro.skeletontricks.inf2zero(DAF) # DAF[ DAF == np.inf ] = 0 PDRF = compute_pdrf(dbf_max, pdrf_scale, pdrf_exponent, DBF, DAF) # Use dijkstra propogation w/o a target to generate a field of # pointers from each voxel to its parent. Then we can rapidly # compute multiple paths by simply hopping pointers using path_from_parents if not fix_branching: parents = dijkstra3d.parental_field(PDRF, root) del PDRF else: parents = PDRF if soma_mode: invalidated, labels = kimimaro.skeletontricks.roll_invalidation_ball( labels, DBF, np.array([root], dtype=np.uint32), scale=soma_invalidation_scale, const=soma_invalidation_const, anisotropy=anisotropy) paths = compute_paths(root, labels, DBF, DAF, parents, scale, const, anisotropy, soma_mode, soma_radius, fix_branching) skel = PrecomputedSkeleton.simple_merge( [PrecomputedSkeleton.from_path(path) for path in paths]).consolidate() verts = skel.vertices.flatten().astype(np.uint32) skel.radii = DBF[verts[::3], verts[1::3], verts[2::3]] return skel