Exemple #1
0
    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
Exemple #2
0
    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