def sub_segment(cls, label_id, dtype): """Calculate metrics for a given label or set of labels. Wrapper to call :func:``measure_variation`` and :func:``measure_edge_dist``. Args: label_id: Integer of the label in :attr:``labels_img_np`` to sub-divide. Returns: Tuple of the given label ID, list of slices where the label resides in :attr:``labels_img_np``, and an array in the same shape of the original label, now sub-segmented. The base value of this sub-segmented array is multiplied by :const:``config.SUB_SEG_MULT``, with each sub-region incremented by 1. """ label_mask = cls.labels_img_np == label_id label_size = np.sum(label_mask) labels_seg = None slices = None if label_size > 0: props = measure.regionprops(label_mask.astype(np.int)) _, slices = cv_nd.get_bbox_region(props[0].bbox) # work on a view of the region for efficiency labels_region = np.copy(cls.labels_img_np[tuple(slices)]) label_mask_region = labels_region == label_id atlas_edge_region = cls.atlas_edge[tuple(slices)] #labels_region[atlas_edge_region != 0] = 0 labels_region[~label_mask_region] = 0 # segment from anatomic borders, limiting peaks to get only # dominant regions labels_seg = watershed_distance(atlas_edge_region == 0, num_peaks=5, compactness=0.01) labels_seg[~label_mask_region] = 0 #labels_seg = measure.label(labels_region) # ensure that sub-segments occupy at least a certain # percentage of the total label labels_retained = np.zeros_like(labels_region, dtype=dtype) labels_unique = np.unique(labels_seg[labels_seg != 0]) print("found {} subregions for label ID {}".format( labels_unique.size, label_id)) i = 0 for seg_id in labels_unique: seg_mask = labels_seg == seg_id size = np.sum(seg_mask) ratio = size / label_size if ratio > 0.1: # relabel based on original label, expanded to # allow for sub-labels unique_id = np.abs(label_id) * config.SUB_SEG_MULT + i unique_id = int(unique_id * label_id / np.abs(label_id)) print("keeping subregion {} of size {} (ratio {}) within " "label {}".format(unique_id, size, ratio, label_id)) labels_retained[seg_mask] = unique_id i += 1 retained_unique = np.unique(labels_retained[labels_retained != 0]) print("labels retained within {}: {}".format( label_id, retained_unique)) ''' # find neighboring sub-labels to merge into retained labels neighbor_added = True done = [] while len(done) < retained_unique.size: for seg_id in retained_unique: if seg_id in done: continue neighbor_added = False seg_mask = labels_retained == seg_id exterior = plot_3d.exterior_nd(seg_mask) neighbors = np.unique(labels_seg[exterior]) for neighbor in neighbors: mask = np.logical_and( labels_seg == neighbor, labels_retained == 0) if neighbor == 0 or np.sum(mask) == 0: continue print("merging in neighbor {} (size {}) to label {}" .format(neighbor, np.sum(mask), seg_id)) labels_retained[mask] = seg_id neighbor_added = True if not neighbor_added: print("{} is done".format(seg_id)) done.append(seg_id) print(done, retained_unique) labels_seg = labels_retained ''' if retained_unique.size > 0: # in-paint missing space from non-retained sub-labels labels_seg = cv_nd.in_paint(labels_retained, labels_retained == 0) labels_seg[~label_mask_region] = 0 else: # if no sub-labels retained, replace whole region with # new label labels_seg[label_mask_region] = label_id * config.SUB_SEG_MULT return label_id, slices, labels_seg
def erode_label(cls, label_id, filter_size, target_frac=None, min_filter_size=1, use_min_filter=False, skel_eros_filt_size=0): """Convert a label to a marker as an eroded version of the label. By default, labels will be eroded with the given ``filter_size`` as long as their final size is > 20% of the original volume. If the eroded volume is below threshold, ``filter_size`` will be progressively decreased until the filter cannot be reduced further. Skeletonization of the labels recovers some details by partially preserving the original labels' extent, including thin regions that would be eroded away, thus serving a similar function as that of adaptive morphological filtering. ``skel_eros_filt_size`` allows titrating the amount of the labels` extent to be preserved. If :attr:`wt_dists` is present, the label's distance will be used to weight the starting filter size. Args: label_id (int): ID of label to erode. filter_size (int): Size of structing element to start erosion. target_frac (float): Target fraction of original label to erode. Erosion will start with ``filter_size`` and use progressively smaller filters until remaining above this target. Defaults to None to use a fraction of 0.2. Titrates the relative amount of erosion allowed. min_filter_size (int): Minimum filter size, below which the original, uneroded label will be used instead. Defaults to 1. Use 0 to erode at size 1 even if below ``target_frac``. Titrates the absolute amount of erosion allowed. use_min_filter (bool): True to erode at ``min_filter_size`` if a smaller filter size would otherwise be required; defaults to False to revert to original, uneroded size if a filter smaller than ``min_filter_size`` would be needed. skel_eros_filt_size (int): Erosion filter size before skeletonization to balance how much of the labels' extent will be preserved during skeletonization. Increase to reduce the skeletonization. Defaults to 0, which will cause skeletonization to be skipped. Returns: :obj:`pd.DataFrame`, List[slice], :obj:`np.ndarray`: stats, including ``label_id`` for reference and sizes of labels; list of slices denoting where to insert the eroded label; and the eroded label itself. """ if cls.wt_dists is not None: # weight the filter size by the fractional distance from median # of label distance and max dist wt = (np.median(cls.wt_dists[cls.labels_img == label_id]) / np.amax(cls.wt_dists)) filter_size = int(filter_size * wt) print("label {}: distance weight {}, adjusted filter size to {}". format(label_id, wt, filter_size)) if use_min_filter and filter_size < min_filter_size: filter_size = min_filter_size # get region as mask; assume that label exists and will yield a # bounding box since labels here are generally derived from the # labels image itself bbox = cv_nd.get_label_bbox(cls.labels_img, label_id) _, slices = cv_nd.get_bbox_region(bbox) region = cls.labels_img[tuple(slices)] label_mask_region = region == label_id region_size = np.sum(label_mask_region) region_size_filtered = region_size fn_selem = cv_nd.get_selem(cls.labels_img.ndim) # erode the labels, starting with the given filter size and decreasing # if the resulting label size falls below a given size ratio chosen_selem_size = np.nan filtered = label_mask_region size_ratio = 1 for selem_size in range(filter_size, -1, -1): if selem_size < min_filter_size: if not use_min_filter: print("label {}: could not erode without dropping below " "minimum filter size of {}, reverting to original " "region size of {}".format(label_id, min_filter_size, region_size)) filtered = label_mask_region region_size_filtered = region_size chosen_selem_size = np.nan break # erode check size ratio filtered = morphology.binary_erosion(label_mask_region, fn_selem(selem_size)) region_size_filtered = np.sum(filtered) size_ratio = region_size_filtered / region_size thresh = 0.2 if target_frac is None else target_frac chosen_selem_size = selem_size if region_size_filtered < region_size and size_ratio > thresh: # stop eroding if underwent some erosion but stayed above # threshold size; skimage erosion treats border outside image # as True, so images may not undergo erosion and should # continue until lowest filter size is taken (eg NaN) break if not np.isnan(chosen_selem_size): print("label {}: changed num of pixels from {} to {} " "(size ratio {}), initial filter size {}, chosen {}".format( label_id, region_size, region_size_filtered, size_ratio, filter_size, chosen_selem_size)) if skel_eros_filt_size and np.sum(filtered) > 0: # skeletonize the labels to recover details from erosion; # need another labels erosion before skeletonization to avoid # preserving too much of the original labels' extent label_mask_region = morphology.binary_erosion( label_mask_region, fn_selem(skel_eros_filt_size)) filtered = np.logical_or( filtered, morphology.skeletonize_3d(label_mask_region).astype(bool)) stats_eros = (label_id, region_size, region_size_filtered, chosen_selem_size) return stats_eros, slices, filtered