def test_euclidean_distance_field_2d(free_space_radius): values = np.ones((2, 2), dtype=bool) sq2 = sqrt(2) sq3 = sqrt(3) answer = np.array([ [0, 1], [1, sq2], ], dtype=np.float32) field = dijkstra3d.euclidean_distance_field( values, (0, 0), free_space_radius=free_space_radius) assert np.all(np.abs(field - answer) < 0.00001) values = np.ones((5, 5), dtype=bool) answer = np.array( [[0, 1., 2., 3., 4.], [1, 1.4142135, 2.4142137, 3.4142137, 4.4142137], [2, 2.4142137, 2.828427, 3.828427, 4.8284273], [3, 3.4142137, 3.828427, 4.2426405, 5.2426405], [4, 4.4142137, 4.8284273, 5.2426405, 5.656854]], dtype=np.float32) field = dijkstra3d.euclidean_distance_field( values, (0, 0, 0), (1, 1, 1), free_space_radius=free_space_radius) assert np.all(np.abs(field - answer) < 0.00001) answer = np.array([[ [0, 1], [1, sq2], ], [ [1, sq2], [sq2, sq3], ]], dtype=np.float32) values = np.ones((2, 2, 2), dtype=bool) field = dijkstra3d.euclidean_distance_field( values, (0, 0, 0), (1, 1, 1), free_space_radius=free_space_radius) assert np.all(np.abs(field - answer) < 0.00001) values = np.ones((2, 2, 2), dtype=bool) field = dijkstra3d.euclidean_distance_field( values, (1, 1, 1), (1, 1, 1), free_space_radius=free_space_radius) answer = np.array([[ [sq3, sq2], [sq2, 1], ], [ [sq2, 1], [1, 0], ]], dtype=np.float32) assert np.all(np.abs(field - answer) < 0.00001)
def test_euclidean_distance_field_3d_free_space_eqn(point): point = tuple(point) print(point) values = np.ones((256, 256, 256), dtype=bool) field_dijk = dijkstra3d.euclidean_distance_field( values, point, free_space_radius=0) # free space off field_free = dijkstra3d.euclidean_distance_field( values, point, free_space_radius=10000) # free space 100% on assert np.all(np.abs(field_free - field_dijk) < 0.001) # there's some difference below this
def edf(): print("Running edf.") N = 1 sx, sy, sz = 256, 256, 256 values = np.ones((sx,sy,sz), dtype=np.bool) for i in range(5): s = time.time() dijkstra3d.euclidean_distance_field(values, (100,100,100)) e = time.time() accum = e-s mvx = N * sx * sy * sz / accum / 1000000 print(f"{mvx:.3f} MVx/sec ({accum:.3f} sec)")
def find_root(labels, anisotropy): """ "4.4 DAF: Compute distance from any voxel field" Compute DAF, but we immediately convert to the PDRF The extremal point of the PDRF is a valid root node even if the DAF is computed from an arbitrary pixel. """ any_voxel = kimimaro.skeletontricks.first_label(labels) if any_voxel is None: return None DAF = dijkstra3d.euclidean_distance_field(np.asfortranarray(labels), any_voxel, anisotropy=anisotropy) return kimimaro.skeletontricks.find_target(labels, DAF)
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 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
def test_euclidean_distance_field_2d(free_space_radius): values = np.ones((2, 2), dtype=bool) sq2 = sqrt(2) sq3 = sqrt(3) answer = np.array([ [0, 1], [1, sq2], ], dtype=np.float32) field = dijkstra3d.euclidean_distance_field( values, (0, 0), free_space_radius=free_space_radius) assert np.all(np.abs(field - answer) < 0.00001) values = np.ones((5, 5), dtype=bool) answer = np.array( [[0, 1., 2., 3., 4.], [1, 1.4142135, 2.4142137, 3.4142137, 4.4142137], [2, 2.4142137, 2.828427, 3.828427, 4.8284273], [3, 3.4142137, 3.828427, 4.2426405, 5.2426405], [4, 4.4142137, 4.8284273, 5.2426405, 5.656854]], dtype=np.float32) field, max_loc = dijkstra3d.euclidean_distance_field( values, (0, 0, 0), (1, 1, 1), free_space_radius=free_space_radius, return_max_location=True) assert np.all(np.abs(field - answer) < 0.00001) assert max_loc == (4, 4) answer = np.array([[ [0, 1], [1, sq2], ], [ [1, sq2], [sq2, sq3], ]], dtype=np.float32) values = np.ones((2, 2, 2), dtype=bool) field, max_loc = dijkstra3d.euclidean_distance_field( values, (0, 0, 0), (1, 1, 1), free_space_radius=free_space_radius, return_max_location=True) assert np.all(np.abs(field - answer) < 0.00001) assert max_loc == (1, 1, 1) values = np.ones((2, 2, 2), dtype=bool) field = dijkstra3d.euclidean_distance_field( values, (1, 1, 1), (1, 1, 1), free_space_radius=free_space_radius) answer = np.array([[ [sq3, sq2], [sq2, 1], ], [ [sq2, 1], [1, 0], ]], dtype=np.float32) assert np.all(np.abs(field - answer) < 0.00001) # Multi-source values = np.ones((4, 4), dtype=bool) field = dijkstra3d.euclidean_distance_field( values, [(0, 0), (3, 3)], free_space_radius=free_space_radius) answer = np.array([ [0, 1, 2, 3], [1, sq2, (1 + sq2), 2], [2, (1 + sq2), sq2, 1], [3, 2, 1, 0], ]) assert np.all(np.isclose(field, answer))