def test_return_count(): labels = np.ones((10, 10, 10), dtype=bool) labels[3:6, 3:6, 3:6] = False filled = fill_voids.fill(labels) assert np.all(filled == 1) filled, ct = fill_voids.fill(labels, return_fill_count=True) assert np.any(labels == False) assert ct == 27
def postrocessing(label_image, min_area): '''some post-processing mapping small label patches to the neighbout whith which they share the largest border. All connected components smaller than min_area will be removed ''' # cleaning overall connected components and fill holes regionmask = skimage.measure.label(label_image > 0) regions = skimage.measure.regionprops(regionmask) resizes = np.asarray([x.area for x in regions]) m = len(resizes) ix = np.zeros((m, ), dtype=np.uint8) ix[resizes > min_area] = 1 ix = np.concatenate([[ 0, ], ix]) cleaned = ix[regionmask] outmask = np.zeros(cleaned.shape, dtype=np.uint8) for i in np.unique(label_image)[1:]: outmask[fill_voids.fill((label_image == i) & (cleaned > 0))] = i # merge small components to neighbours regionmask = skimage.measure.label(outmask) origlabels = np.unique(outmask) origlabels_maxsub = np.zeros( (max(origlabels) + 1, ), dtype=np.uint32) # will hold the largest component for a label regions = skimage.measure.regionprops(regionmask, outmask) regions.sort(key=lambda x: x.area) regionlabels = [x.label for x in regions] # will hold mapping from regionlabels to original labels region_to_lobemap = np.zeros((len(regionlabels) + 1, ), dtype=np.uint8) for r in regions: if r.area > origlabels_maxsub[r.max_intensity]: origlabels_maxsub[r.max_intensity] = r.area region_to_lobemap[r.label] = r.max_intensity for r in regions: if r.area < origlabels_maxsub[r.max_intensity]: bb = bbox_3D(regionmask == r.label) sub = regionmask[bb[0]:bb[1], bb[2]:bb[3], bb[4]:bb[5]] dil = ndimage.binary_dilation(sub == r.label) neighbours, counts = np.unique(sub[dil], return_counts=True) mapto = r.label maxmap = 0 myarea = 0 for ix, n in enumerate(neighbours): if n != 0 and n != r.label and counts[ix] > maxmap: maxmap = counts[ix] mapto = n myarea = r.area regionmask[regionmask == r.label] = mapto if regions[regionlabels.index(mapto)].area == origlabels_maxsub[ regions[regionlabels.index(mapto)].max_intensity]: origlabels_maxsub[regions[regionlabels.index( mapto)].max_intensity] += myarea regions[regionlabels.index( mapto)].__dict__['_cache']['area'] += myarea return region_to_lobemap[regionmask]
def execute(seg: Chunk): properties = seg.properties array = fill_voids.fill(seg.array) seg2 = Chunk(array) seg2.set_properties(properties) return [ seg2, ]
def engage_avocado_protection_single_pass(cc_labels, all_dbf, candidates=None, progress=False): """ For each candidate, check if there's a fruit around the avocado pit roughly from the center (the max EDT). """ if candidates is None: candidates = fastremap.unique(cc_labels) candidates = [label for label in candidates if label != 0] unchanged = set() changed = set() if len(candidates) == 0: return cc_labels, unchanged, changed def paint_walls(binimg): """ Ensure that inclusions that touch the wall are handled by performing a 2D fill on each wall. """ binimg[:, :, 0] = fill_voids.fill(binimg[:, :, 0]) binimg[:, :, -1] = fill_voids.fill(binimg[:, :, -1]) binimg[:, 0, :] = fill_voids.fill(binimg[:, 0, :]) binimg[:, -1, :] = fill_voids.fill(binimg[:, -1, :]) binimg[0, :, :] = fill_voids.fill(binimg[0, :, :]) binimg[-1, :, :] = fill_voids.fill(binimg[-1, :, :]) return binimg remap = {} for label in tqdm(candidates, disable=(not progress), desc="Fixing Avocados"): binimg = paint_walls(cc_labels == label) # image of the pit coord = argmax(binimg * all_dbf) (pit, fruit) = kimimaro.skeletontricks.find_avocado_fruit( cc_labels, coord[0], coord[1], coord[2]) if pit == fruit and pit not in changed: unchanged.add(pit) else: unchanged.discard(pit) unchanged.discard(fruit) changed.add(pit) changed.add(fruit) binimg |= (cc_labels == fruit) binimg, N = fill_voids.fill(binimg, in_place=True, return_fill_count=True) cc_labels *= ~binimg cc_labels += label * binimg return cc_labels, unchanged, changed
def test_scipy_comparison2d(): segids = np.copy(SEGIDS) np.random.shuffle(segids) for segid in tqdm(segids[:10]): print(segid) for z in tqdm(range(img.shape[2])): binimg = img[:, :, z] == segid orig_binimg = np.copy(binimg, order='F') fv = fill_voids.fill(binimg, in_place=False) fvip = fill_voids.fill(binimg, in_place=True) assert np.all(fv == fvip) spy = binary_fill_holes(binimg) assert np.all(fv == spy)
def test_scipy_comparison3d(): segids = np.copy(SEGIDS) np.random.shuffle(segids) for segid in tqdm(segids[:10]): print(segid) binimg = img == segid slices = scipy.ndimage.find_objects(binimg)[0] binimg = binimg[slices] orig_binimg = np.copy(binimg, order='F') fv = fill_voids.fill(binimg, in_place=False) fvip = fill_voids.fill(binimg, in_place=True) assert np.all(fv == fvip) spy = binary_fill_holes(binimg) assert np.all(fv == spy)
def test_dimensions(dimension): size = [5] * dimension for i in range(3, dimension): size[i] = 1 labels = np.ones(size, dtype=np.uint8) labels = fill_voids.fill(labels) assert labels.ndim == dimension if dimension <= 3: return size[dimension - 1] = 2 labels = np.ones(size, dtype=np.uint8) try: labels = fill_voids.fill(labels) assert False except fill_voids.DimensionError: pass
def test_2d_3d_differ(): labels = np.zeros((10, 10), dtype=np.bool) labels[1:9, 1:9] = True labels[4:8, 4:8] = False expected_result2d = np.zeros((10, 10), dtype=np.bool) expected_result2d[1:9, 1:9] = True expected_result3d = np.copy(labels).reshape(10, 10, 1) filled_labels, N = fill_voids.fill(labels, in_place=False, return_fill_count=True) assert N == 16 assert np.all(filled_labels == expected_result2d) labels = labels[..., np.newaxis] filled_labels, N = fill_voids.fill(labels, in_place=False, return_fill_count=True) assert N == 0 assert np.all(filled_labels == expected_result3d)
def test_zero_array(): labels = np.zeros((0, ), dtype=np.uint8) # just don't throw an exception fill_voids.fill(labels, in_place=False) fill_voids.fill(labels, in_place=True) labels = np.zeros((128, 128, 128), dtype=np.uint8) fill_voids.fill(labels, in_place=True) assert not np.any(labels)
def fill_all_holes(cc_labels, progress=False, return_fill_count=False): """ Fills the holes in each connected component and removes components that get filled in. The idea is that holes (entirely contained labels or background) are artifacts in cell segmentations. A common example is a nucleus segmented separately from the rest of the cell or errors in a manual segmentation leaving a void in a dendrite. cc_labels: an image containing connected components with labels smaller than the number of voxels in the image. progress: Display a progress bar or not. return_fill_count: if specified, return a tuple (filled_image, N) where N is the number of voxels that were filled in. Returns: filled_in_labels """ labels = fastremap.unique(cc_labels) labels_set = set(labels) labels_set.discard(0) all_slices = find_objects(cc_labels) pixels_filled = 0 for label in tqdm(labels, disable=(not progress), desc="Filling Holes"): if label not in labels_set: continue slices = all_slices[label - 1] if slices is None: continue binary_image = (cc_labels[slices] == label) binary_image, N = fill_voids.fill(binary_image, in_place=True, return_fill_count=True) pixels_filled += N if N == 0: continue sub_labels = set(fastremap.unique(cc_labels[slices] * binary_image)) sub_labels.remove(label) labels_set -= sub_labels cc_labels[ slices] = cc_labels[slices] * ~binary_image + label * binary_image if return_fill_count: return cc_labels, pixels_filled return cc_labels
def paint_walls(binimg): """ Ensure that inclusions that touch the wall are handled by performing a 2D fill on each wall. """ binimg[:, :, 0] = fill_voids.fill(binimg[:, :, 0]) binimg[:, :, -1] = fill_voids.fill(binimg[:, :, -1]) binimg[:, 0, :] = fill_voids.fill(binimg[:, 0, :]) binimg[:, -1, :] = fill_voids.fill(binimg[:, -1, :]) binimg[0, :, :] = fill_voids.fill(binimg[0, :, :]) binimg[-1, :, :] = fill_voids.fill(binimg[-1, :, :]) return binimg
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 mask_to_cells_edge(prediction_mask, im, config, r_min, frame_data, edge_dist=15, return_mask=False): r_min_pix = r_min / config["pixel_size_m"] / 1e6 edge_dist_pix = edge_dist / config["pixel_size_m"] / 1e6 cells = [] # TDOD: consider first applying binary closing operations to avoid impact of very small gaps in the cell border filled = fill_voids.fill(prediction_mask) # iterate over all detected regions for region in regionprops( label(filled)): # region props are based on the original image # checking if the anything was filled up by extracting the region form the original image # if no significant region was filled, we skip this object yc, xc = np.split(region.coords, 2, axis=1) if np.sum(~prediction_mask[yc.flatten(), xc.flatten()]) < 10: if return_mask: prediction_mask[yc.flatten(), xc.flatten()] = False continue elif return_mask: prediction_mask[yc.flatten(), xc.flatten()] = True a = region.major_axis_length / 2 b = region.minor_axis_length / 2 r = np.sqrt(a * b) if region.orientation > 0: ellipse_angle = np.pi / 2 - region.orientation else: ellipse_angle = -np.pi / 2 - region.orientation Amin_pixels = np.pi * ( r_min_pix)**2 # minimum region area based on minimum radius # filtering cells close to left and right image edge # usually cells do not come close to upper and lower image edge x_pos = region.centroid[1] dist_to_edge = np.min([x_pos, prediction_mask.shape[1] - x_pos]) if region.area >= Amin_pixels and dist_to_edge > edge_dist_pix: # analyze only regions larger than 100 pixels, # and only of the canny filtered band-passed image returend an object # the circumference of the ellipse circum = np.pi * ((3 * (a + b)) - np.sqrt(10 * a * b + 3 * (a**2 + b**2))) if 0: # %% compute radial intensity profile around each ellipse theta = np.arange(0, 2 * np.pi, np.pi / 8) i_r = np.zeros(int(3 * r)) for d in range(0, int(3 * r)): # get points on the circumference of the ellipse x = d / r * a * np.cos(theta) y = d / r * b * np.sin(theta) # rotate the points by the angle fo the ellipse t = ellipse_angle xrot = (x * np.cos(t) - y * np.sin(t) + region.centroid[1]).astype(int) yrot = (x * np.sin(t) + y * np.cos(t) + region.centroid[0]).astype(int) # crop for points inside the iamge index = (xrot < 0) | (xrot >= im.shape[1]) | (yrot < 0) | ( yrot >= im.shape[0]) x = xrot[~index] y = yrot[~index] # average over all these points i_r[d] = np.mean(im[y, x]) # define a sharpness value sharp = (i_r[int(r + 2)] - i_r[int(r - 2)]) / 5 / np.std(i_r) sharp = 0 # %% store the cells yy = region.centroid[0] - config["channel_width_px"] / 2 yy = yy * config["pixel_size_m"] * 1e6 data = {} data.update(frame_data) data.update({ "x": region.centroid[1], # x_pos "y": region.centroid[0], # y_pos "rp": yy, # RadialPos "long_axis": float(format(region.major_axis_length)) * config["pixel_size_m"] * 1e6, # LongAxis "short_axis": float(format(region.minor_axis_length)) * config["pixel_size_m"] * 1e6, # ShortAxis "angle": np.rad2deg(ellipse_angle), # angle "irregularity": region.perimeter / circum, # irregularity "solidity": region.solidity, # solidity "sharpness": sharp, # sharpness }) cells.append(data) if return_mask: return cells, prediction_mask else: return cells
def test_dtypes(dtype): binimg = img == SEGIDS[0] comparison = fill_voids.fill(binimg, in_place=False) res = fill_voids.fill(binimg.astype(dtype), in_place=False) assert np.all(comparison == res)