Beispiel #1
0
def segment_from_labels(edges,
                        markers,
                        labels_img,
                        atlas_img=None,
                        exclude_labels=None,
                        mask_filt=config.SmoothingModes.opening,
                        mask_filt_size=2):
    """Segment an image using markers from a labels image.
    
    Labels images may have been generally manually and thus may not 
    perfectly match the underlying image. As a way to check or 
    augment the label image, segment the underlying image using 
    the labels as the seeds to prescribe the number and initial 
    location of each label.
    
    Args:
        edges (:obj:`np.ndarray`): Image as a Numpy array to segment,
            typically an edge-detected image of the main atlas.
        markers (:obj:`np.ndarray`): Image as an integer Numpy array of same
            shape as ``img`` to use as seeds for the watershed segmentation.
            This array is generally constructed from an array similar to
            ``labels_img``.
        labels_img (:obj:`np.ndarray`): Labels image as Numpy array of same
            shape as ``img``, used to generate a mask for the watershed.
            If None, a mask will be generated from a thresholded version of
            ``atlas_img``, so should only be None if ``atlas_img`` is not None. 
        atlas_img (:obj:`np.ndarray`): Atlas image as a Numpy array to use
            for finding foreground; defaults to None. If both ``labels_img``
            and ``atlas_img`` are not None, their combined volume will be
            used as a mask.
        exclude_labels (List[int]): Sequence of labels to exclude from the
            segmentation; defaults to None.
        mask_filt (:obj:`config.SmoothingModes`): Enumeration for a filter
            mode to use for the watershed mask; defaults to
            :obj:`config.SmoothingModes.opening`. Ignored if ``atlas_img``
            or both ``atlas_img`` and ``labels_img`` are given to generate
            the mask.
        mask_filt_size (int): Size of structuring element for the filter
            specified by ``mask_filt``; defaults to 2.
    
    Returns:
        :obj:`np.ndarray`: Segmented image of the same shape as ``img`` with
        the same number of labels as in ``markers``.
    
    """
    # generate mask for watershed
    if atlas_img is not None and labels_img is not None:
        # broad mask from both atlas and labels
        mask = mask_atlas(atlas_img, labels_img)
    elif atlas_img is not None:
        # otsu seems to give more inclusive threshold for these atlases
        _, mask = cv_nd.carve(atlas_img,
                              thresh=filters.threshold_otsu(atlas_img),
                              holes_area=5000)
    else:
        # default to using label foreground
        mask = labels_img != 0
        fn_mask = None
        if mask_filt is config.SmoothingModes.opening:
            # default filter opens the mask to prevent spillover across
            # artifacts that may bridge otherwise separate structures
            fn_mask = morphology.binary_opening
        elif mask_filt is config.SmoothingModes.closing:
            fn_mask = morphology.binary_closing
        if fn_mask and mask_filt_size:
            print("Filtering watershed mask with {}, size {}".format(
                fn_mask, mask_filt_size))
            mask = fn_mask(mask,
                           cv_nd.get_selem(labels_img.ndim)(mask_filt_size))

    exclude = None
    if exclude_labels is not None:
        # remove excluded labels from mask
        exclude = np.isin(labels_img, exclude_labels)
        mask[exclude] = False
        # WORKAROUND: remove excluded markers from marker image itself for
        # apparent Scikit-image bug (see PR 3809, fixed in 0.15)
        markers[np.isin(markers, exclude_labels)] = 0

    watershed = watershed_distance(edges == 0,
                                   markers,
                                   compactness=0.005,
                                   mask=mask)
    if exclude is not None:
        # add excluded labels directly to watershed image
        watershed[exclude] = labels_img[exclude]
    return watershed
Beispiel #2
0
    def erode_label(
        cls,
        label_id: int,
        filter_size: int,
        target_frac: float = None,
        min_filter_size: int = 1,
        use_min_filter: bool = False,
        skel_eros_filt_size: Union[int, bool] = False,
        wt: float = None
    ) -> Tuple[Tuple[int, np.ndarray, np.ndarray, Any], Union[
            Optional[List[slice]], Any], Any]:
        """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: ID of label to erode.
            filter_size: Size of structing element to start erosion.
            target_frac: 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: 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: 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: 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 False, which will cause
                skeletonization to be skipped.
            wt: Multiplier weight for ``filter_size``. Defaults to None, in
                which case the weighte will be calculated from
                :attr:``wt_dists`` if available, or ignored if not.
        
        Returns:
            Tuple of 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.
        
        Raises:
            ValueError: if ``region`` is None and :attr:`labels_img` is not
                available.
        
        """
        if cls.labels_img is None:
            cls.labels_img = cls.convert_shared_arr(config.RegNames.IMG_LABELS)

        if (wt is None and cls.wt_dists is not None
                and cls.labels_img is not None):
            # weight the filter size by the fractional distance from median
            # of label distance and max dist
            wt = cls.meas_wt(cls.labels_img, label_id, cls.wt_dists)
        if wt is not None:
            filter_size = int(filter_size * wt)
            print(f"Label {label_id}: distance weight {wt}, adjusted filter "
                  f"size to {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
        region, slices = cv_nd.extract_region(cls.labels_img, label_id)
        label_mask_region = region == label_id
        region_size = np.sum(label_mask_region)
        filtered, chosen_selem_size = cv_nd.filter_adaptive_size(
            label_mask_region, morphology.binary_erosion, filter_size,
            min_filter_size, use_min_filter, target_frac,
            f"Label ID: {label_id}")
        region_size_filtered = np.sum(filtered)
        if skel_eros_filt_size is not False 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,
                cv_nd.get_selem(label_mask_region.ndim)(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
Beispiel #3
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
Beispiel #4
0
def segment_from_labels(edges, markers, labels_img, atlas_img=None,
                        exclude_labels=None):
    """Segment an image using markers from a labels image.
    
    Labels images may have been generally manually and thus may not 
    perfectly match the underlying image. As a way to check or 
    augment the label image, segment the underlying image using 
    the labels as the seeds to prescribe the number and initial 
    location of each label.
    
    Args:
        edges (:obj:`np.ndarray`): Image as a Numpy array to segment,
            typically an edge-detected image of the main atlas.
        markers (:obj:`np.ndarray`): Image as an integer Numpy array of same
            shape as ``img`` to use as seeds for the watershed segmentation.
            This array is generally constructed from an array similar to
            ``labels_img``.
        labels_img (:obj:`np.ndarray`): Labels image as Numpy array of same
            shape as ``img``, used to generate a mask for the watershed.
            If None, a mask will be generated from a thresholded version of
            ``atlas_img``, so should only be None if ``atlas_img`` is not None. 
        atlas_img (:obj:`np.ndarray`): Atlas image as a Numpy array to use
            for finding foreground; defaults to None. If both ``labels_img``
            and ``atlas_img`` are not None, their combined volume will be
            used as a mask.
        exclude_labels (List[int]): Sequence of labels to exclude from the
            segmentation; defaults to None.
    
    Returns:
        :obj:`np.ndarray`: Segmented image of the same shape as ``img`` with
        the same number of labels as in ``markers``.
    """
    if atlas_img is not None and labels_img is not None:
        # broad mask from both atlas and labels
        mask = mask_atlas(atlas_img, labels_img)
    elif atlas_img is not None:
        # otsu seems to give more inclusive threshold for these atlases
        _, mask = cv_nd.carve(
            atlas_img, thresh=filters.threshold_otsu(atlas_img), 
            holes_area=5000)
    else:
        # default to using labels, opening them up small holes to prevent 
        # spillover across artifacts that may bridge them
        fn_selem = cv_nd.get_selem(labels_img.ndim)
        mask = morphology.binary_opening(labels_img != 0, fn_selem(2))
    
    exclude = None
    if exclude_labels is not None:
        # remove excluded labels from mask
        exclude = np.isin(labels_img, exclude_labels)
        mask[exclude] = False
        # WORKAROUND: remove excluded markers from marker image itself for
        # apparent Scikit-image bug (see PR 3809, fixed in 0.15)
        markers[np.isin(markers, exclude_labels)] = 0
    
    watershed = watershed_distance(
        edges == 0, markers, compactness=0.005, mask=mask)
    if exclude is not None:
        # add excluded labels directly to watershed image
        watershed[exclude] = labels_img[exclude]
    return watershed