cytometer/utils.py
def clean_segmentation(labels,
                       min_cell_area=0, max_cell_area=np.inf,
                       remove_edge_labels=False,
                       mask=None, min_mask_overlap=0.8,
                       phagocytosis=False,
                       labels_class=None, min_class_prop=1.0):
    """
    The function packs several methods to remove unwanted labels from a segmentation:
      * Remove labels that are smaller than a certain size.
      * Remove labels that don't overlap enough with a binary mask.
      * Remove labels that don't contain enough pixels of class 1.
      * Merge labels that are completely surrounded by another label into the surrounding label.
      * Remove labels that touch the edges of the segmentation.
      * Remove labels that are larger than a certain size.

    :param labels: (row, col) or (n, row, col) np.ndarray with segmentation labels.
    :param min_cell_area: (def 0) Remove labels with area < min_cell_area. Area is computed as the number of pixels.
    :param max_cell_area: (def np.inf) Remove labels with area > max_cell_area. Area is computed as the number of
    pixels.
    :param remove_edge_labels: (def False) Boolean to remove labels that touch the edge of the image.
    :param mask: (def None) (row, col) or (n, row, col) np.ndarray binary mask. If provided, remove labels that don't
    overlap enough with the mask.
    :param min_mask_overlap: (def 0.8) Remove labels that don't have at least min_mask_overlap of their pixels
    within the mask.
    :param phagocytosis: (def False) Boolean to remove labels that are completely surrounded by another label.
    :param labels_class: (def None) np.array with the same size as labels. Each pixel has class False or True.
    :param min_class_prop: (def 1.0) Scalar. Minimum proportion of pixels with class True that an object must have to be
    accepted. If min_class_prop=1.0, then all pixels must be of class True. If min_class_prop=0.6, then at least 60% of
    pixels need to be of class True.
    :return:
    * labels: (row, col) or (n, row, col) np.ndarray with removed labels as requested.
    * is_removed_edge_label: (row, col) or (n, row, col) boolean np.ndarray. True pixels belong to edge labels that were
      removed.
    """

    # if mask provided, it must have the same shape as the labels array
    if mask is not None and labels.shape != mask.shape:
        raise ValueError('If provided, mask must have the same shape as labels')

    # if labels_class provided, it must have the same shape as the labels array
    if labels_class is not None and labels.shape != labels_class.shape:
        raise ValueError('If provided, labels_class must have the same shape as labels')

    # convert (row, col) labels to (1, row, col), so that we can use the same code for one or multiple inputs
    labels_is2d = labels.ndim == 2
    if labels_is2d:
        labels = np.expand_dims(labels, axis=0)
        if mask is not None:
            mask = np.expand_dims(mask, axis=0)
        if labels_class is not None:
            labels_class = np.expand_dims(labels_class, axis=0)

    is_removed_edge_label = np.zeros(shape=labels.shape, dtype=np.bool)
    for i in range(labels.shape[0]):  # loop label images

        if DEBUG:
            plt.clf()
            plt.subplot(221)
            plt.imshow(labels[i, :, :])

        # remove labels that are too small
        labels[i, :, :] = remove_small_objects(labels[i, :, :], min_size=min_cell_area)

        if DEBUG:
            plt.subplot(222)
            plt.imshow(labels[i, :, :])

        # remove labels that are not substantially within the mask
        if mask is not None:

            # count number of pixels in each label
            prop = regionprops(labels[i, :, :])
            prop_masked = regionprops(labels[i, :, :] * mask[i, :, :])

            # create a lookup table for quick search of label area
            max_label = np.max([p.label for p in prop])
            area_lut = np.zeros(shape=(max_label + 1,))
            area_masked_lut = np.zeros(shape=(max_label + 1,))
            for p in prop:
                area_lut[p.label] = p.area
            for p in prop_masked:
                area_masked_lut[p.label] = p.area

            # check for each original label whether it is at least min_mask_overlap (def 80%) covered by the mask
            for p in prop:
                if area_masked_lut[p.label] < area_lut[p.label] * min_mask_overlap:
                    aux = labels[i, :, :]
                    aux[labels[i, :, :] == p.label] = 0

        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])

        # remove objects that don't contain enough pixels of class 1
        if labels_class is not None:
            # list of labels that are not 0
            lab_list = np.unique(labels[i, :, :])
            lab_list = lab_list[lab_list != 0]

            # loop labels
            for lab in lab_list:

                # pixels that belong to the current label
                aux = labels[i, :, :]
                idx = aux == lab

                # proportion of class=True pixels
                aux_class = labels_class[i, :, :]
                pixel_prop = np.count_nonzero(aux_class[idx]) / np.count_nonzero(idx)

                # delete the label if it doesn't contain enough pixels of class True
                if pixel_prop < min_class_prop:
                    aux[idx] = 0

        if DEBUG:
            plt.subplot(224)
            plt.cla()
            plt.imshow(labels[i, :, :])

        # remove labels that are completely surrounded by another label
        background = 0  # background label
        labels_list = np.unique(labels[i, :, :])
        n_non_background_labels = np.count_nonzero(labels_list != background)
        if phagocytosis and n_non_background_labels >= 2:

            # a donut is a label with another label inside
            there_are_donuts = True
            while there_are_donuts:

                # unless we find a donut, by default we stop
                there_are_donuts = False

                prop = regionprops(labels[i, :, :])
                for p in prop:
                    # if this label has another label inside
                    there_are_donuts = p.area != p.filled_area
                    if there_are_donuts:
                        # fill up the donut
                        lab = labels[i, :, :]
                        lab[binary_fill_holes(lab == p.label)] = p.label
                        labels[i, :, :] = lab
                        break

        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])

        # remove large objects
        if max_cell_area < np.inf:
            aux = labels[i, :, :]
            prop = regionprops(aux)
            for p in prop:
                if p.area > max_cell_area:
                    aux[aux == p.label] = 0

        if DEBUG:
            plt.subplot(224)
            plt.cla()
            plt.imshow(labels[i, :, :])

        # remove edge segmentations, because in general they correspond to incomplete objects
        if remove_edge_labels:
            labels_edge = edge_labels(labels[i, :, :])
            is_removed_edge_label[i, :, :] = np.isin(labels[i, :, :], test_elements=labels_edge)  # bool of pixels on edge cells
            aux = labels[i, :, :]
            aux[is_removed_edge_label[i, :, :]] = 0

        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])
            plt.contour(is_removed_edge_label[i, :, :], colors='w')

    # remove dummy dimension if the input was 2D
    if labels_is2d:
        labels = labels[0, ...]
        is_removed_edge_label = is_removed_edge_label[0, ...]

    return labels, is_removed_edge_label

    The function packs several methods to remove unwanted labels from a segmentation:
      * Remove labels that are smaller than a certain size.
      * Remove labels that don't overlap enough with a binary mask.
      * Remove labels that don't contain enough pixels of class 1.
      * Merge labels that are completely surrounded by another label into the surrounding label.
      * Remove labels that touch the edges of the segmentation.
      * Remove labels that are larger than a certain size.

    :param labels: (row, col) or (n, row, col) np.ndarray with segmentation labels.
    :param min_cell_area: (def 0) Remove labels with area < min_cell_area. Area is computed as the number of pixels.
    :param max_cell_area: (def np.inf) Remove labels with area > max_cell_area. Area is computed as the number of
    pixels.
    :param remove_edge_labels: (def False) Boolean to remove labels that touch the edge of the image.
    :param mask: (def None) (row, col) or (n, row, col) np.ndarray binary mask. If provided, remove labels that don't
    overlap enough with the mask.
    :param min_mask_overlap: (def 0.8) Remove labels that don't have at least min_mask_overlap of their pixels
    within the mask.
    :param phagocytosis: (def False) Boolean to remove labels that are completely surrounded by another label.
    :param labels_class: (def None) np.array with the same size as labels. Each pixel has class False or True.
    :param min_class_prop: (def 1.0) Scalar. Minimum proportion of pixels with class True that an object must have to be
    accepted. If min_class_prop=1.0, then all pixels must be of class True. If min_class_prop=0.6, then at least 60% of
    pixels need to be of class True.
    :return:
    * labels: (row, col) or (n, row, col) np.ndarray with removed labels as requested.
    * is_removed_edge_label: (row, col) or (n, row, col) boolean np.ndarray. True pixels belong to edge labels that were
      removed.
    
The function packs several methods to remove unwanted labels from a segmentation:
Remove labels that are smaller than a certain size.
Remove labels that don't overlap enough with a binary mask.
Remove labels that don't contain enough pixels of class 1.
Merge labels that are completely surrounded by another label into the surrounding label.
Remove labels that touch the edges of the segmentation.
Remove labels that are larger than a certain size.
The function packs several methods to remove unwanted labels from a segmentation : Remove labels that are smaller than a certain size . Remove labels that don ' t overlap enough with a binary mask . Remove labels that don ' t contain enough pixels of class 1 . Merge labels that are completely surrounded by another label into the surrounding label . Remove labels that touch the edges of the segmentation . Remove labels that are larger than a certain size .
def clean_segmentation(labels,
                       min_cell_area=0, max_cell_area=np.inf,
                       remove_edge_labels=False,
                       mask=None, min_mask_overlap=0.8,
                       phagocytosis=False,
                       labels_class=None, min_class_prop=1.0):
    if mask is not None and labels.shape != mask.shape:
        raise ValueError('If provided, mask must have the same shape as labels')
    if labels_class is not None and labels.shape != labels_class.shape:
        raise ValueError('If provided, labels_class must have the same shape as labels')
    labels_is2d = labels.ndim == 2
    if labels_is2d:
        labels = np.expand_dims(labels, axis=0)
        if mask is not None:
            mask = np.expand_dims(mask, axis=0)
        if labels_class is not None:
            labels_class = np.expand_dims(labels_class, axis=0)
    is_removed_edge_label = np.zeros(shape=labels.shape, dtype=np.bool)
    for i in range(labels.shape[0]):  
        if DEBUG:
            plt.clf()
            plt.subplot(221)
            plt.imshow(labels[i, :, :])
        labels[i, :, :] = remove_small_objects(labels[i, :, :], min_size=min_cell_area)
        if DEBUG:
            plt.subplot(222)
            plt.imshow(labels[i, :, :])
        if mask is not None:
            prop = regionprops(labels[i, :, :])
            prop_masked = regionprops(labels[i, :, :] * mask[i, :, :])
            max_label = np.max([p.label for p in prop])
            area_lut = np.zeros(shape=(max_label + 1,))
            area_masked_lut = np.zeros(shape=(max_label + 1,))
            for p in prop:
                area_lut[p.label] = p.area
            for p in prop_masked:
                area_masked_lut[p.label] = p.area
            for p in prop:
                if area_masked_lut[p.label] < area_lut[p.label] * min_mask_overlap:
                    aux = labels[i, :, :]
                    aux[labels[i, :, :] == p.label] = 0
        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])
        if labels_class is not None:
            lab_list = np.unique(labels[i, :, :])
            lab_list = lab_list[lab_list != 0]
            for lab in lab_list:
                aux = labels[i, :, :]
                idx = aux == lab
                aux_class = labels_class[i, :, :]
                pixel_prop = np.count_nonzero(aux_class[idx]) / np.count_nonzero(idx)
                if pixel_prop < min_class_prop:
                    aux[idx] = 0
        if DEBUG:
            plt.subplot(224)
            plt.cla()
            plt.imshow(labels[i, :, :])
        background = 0  
        labels_list = np.unique(labels[i, :, :])
        n_non_background_labels = np.count_nonzero(labels_list != background)
        if phagocytosis and n_non_background_labels >= 2:
            there_are_donuts = True
            while there_are_donuts:
                there_are_donuts = False
                prop = regionprops(labels[i, :, :])
                for p in prop:
                    there_are_donuts = p.area != p.filled_area
                    if there_are_donuts:
                        lab = labels[i, :, :]
                        lab[binary_fill_holes(lab == p.label)] = p.label
                        labels[i, :, :] = lab
                        break
        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])
        if max_cell_area < np.inf:
            aux = labels[i, :, :]
            prop = regionprops(aux)
            for p in prop:
                if p.area > max_cell_area:
                    aux[aux == p.label] = 0
        if DEBUG:
            plt.subplot(224)
            plt.cla()
            plt.imshow(labels[i, :, :])
        if remove_edge_labels:
            labels_edge = edge_labels(labels[i, :, :])
            is_removed_edge_label[i, :, :] = np.isin(labels[i, :, :], test_elements=labels_edge)  
            aux = labels[i, :, :]
            aux[is_removed_edge_label[i, :, :]] = 0
        if DEBUG:
            plt.subplot(223)
            plt.imshow(labels[i, :, :])
            plt.contour(is_removed_edge_label[i, :, :], colors='w')
    if labels_is2d:
        labels = labels[0, ...]
        is_removed_edge_label = is_removed_edge_label[0, ...]
    return labels, is_removed_edge_label
def clean_segmentation ( labels , min_cell_area = 0 , max_cell_area = np . inf , remove_edge_labels = False , mask = None , min_mask_overlap = 0.8 , phagocytosis = False , labels_class = None , min_class_prop = 1.0 ) : if mask is not None and labels . shape != mask . shape : raise ValueError ( 'If provided, mask must have the same shape as labels' ) if labels_class is not None and labels . shape != labels_class . shape : raise ValueError ( 'If provided, labels_class must have the same shape as labels' ) labels_is2d = labels . ndim == 2 if labels_is2d : labels = np . expand_dims ( labels , axis = 0 ) if mask is not None : mask = np . expand_dims ( mask , axis = 0 ) if labels_class is not None : labels_class = np . expand_dims ( labels_class , axis = 0 ) is_removed_edge_label = np . zeros ( shape = labels . shape , dtype = np . bool ) for i in range ( labels . shape [ 0 ] ) : if DEBUG : plt . clf ( ) plt . subplot ( 221 ) plt . imshow ( labels [ i , : , : ] ) labels [ i , : , : ] = remove_small_objects ( labels [ i , : , : ] , min_size = min_cell_area ) if DEBUG : plt . subplot ( 222 ) plt . imshow ( labels [ i , : , : ] ) if mask is not None : prop = regionprops ( labels [ i , : , : ] ) prop_masked = regionprops ( labels [ i , : , : ] * mask [ i , : , : ] ) max_label = np . max ( [ p . label for p in prop ] ) area_lut = np . zeros ( shape = ( max_label + 1 , ) ) area_masked_lut = np . zeros ( shape = ( max_label + 1 , ) ) for p in prop : area_lut [ p . label ] = p . area for p in prop_masked : area_masked_lut [ p . label ] = p . area for p in prop : if area_masked_lut [ p . label ] < area_lut [ p . label ] * min_mask_overlap : aux = labels [ i , : , : ] aux [ labels [ i , : , : ] == p . label ] = 0 if DEBUG : plt . subplot ( 223 ) plt . imshow ( labels [ i , : , : ] ) if labels_class is not None : lab_list = np . unique ( labels [ i , : , : ] ) lab_list = lab_list [ lab_list != 0 ] for lab in lab_list : aux = labels [ i , : , : ] idx = aux == lab aux_class = labels_class [ i , : , : ] pixel_prop = np . count_nonzero ( aux_class [ idx ] ) / np . count_nonzero ( idx ) if pixel_prop < min_class_prop : aux [ idx ] = 0 if DEBUG : plt . subplot ( 224 ) plt . cla ( ) plt . imshow ( labels [ i , : , : ] ) background = 0 labels_list = np . unique ( labels [ i , : , : ] ) n_non_background_labels = np . count_nonzero ( labels_list != background ) if phagocytosis and n_non_background_labels >= 2 : there_are_donuts = True while there_are_donuts : there_are_donuts = False prop = regionprops ( labels [ i , : , : ] ) for p in prop : there_are_donuts = p . area != p . filled_area if there_are_donuts : lab = labels [ i , : , : ] lab [ binary_fill_holes ( lab == p . label ) ] = p . label labels [ i , : , : ] = lab break if DEBUG : plt . subplot ( 223 ) plt . imshow ( labels [ i , : , : ] ) if max_cell_area < np . inf : aux = labels [ i , : , : ] prop = regionprops ( aux ) for p in prop : if p . area > max_cell_area : aux [ aux == p . label ] = 0 if DEBUG : plt . subplot ( 224 ) plt . cla ( ) plt . imshow ( labels [ i , : , : ] ) if remove_edge_labels : labels_edge = edge_labels ( labels [ i , : , : ] ) is_removed_edge_label [ i , : , : ] = np . isin ( labels [ i , : , : ] , test_elements = labels_edge ) aux = labels [ i , : , : ] aux [ is_removed_edge_label [ i , : , : ] ] = 0 if DEBUG : plt . subplot ( 223 ) plt . imshow ( labels [ i , : , : ] ) plt . contour ( is_removed_edge_label [ i , : , : ] , colors = 'w' ) if labels_is2d : labels = labels [ 0 , ... ] is_removed_edge_label = is_removed_edge_label [ 0 , ... ] return labels , is_removed_edge_label
The function packs several methods to remove unwanted labels from a segmentation:
 Remove labels that are smaller than a certain size.
The function packs several methods to remove unwanted labels from a segmentation : Remove labels that are smaller than a certain size .
"""
    The function packs several methods to remove unwanted labels from a segmentation:
      * Remove labels that are smaller than a certain size.
      * Remove labels that don't overlap enough with a binary mask.
      * Remove labels that don't contain enough pixels of class 1.
      * Merge labels that are completely surrounded by another label into the surrounding label.
      * Remove labels that touch the edges of the segmentation.
      * Remove labels that are larger than a certain size.

    :param labels: (row, col) or (n, row, col) np.ndarray with segmentation labels.
    :param min_cell_area: (def 0) Remove labels with area < min_cell_area. Area is computed as the number of pixels.
    :param max_cell_area: (def np.inf) Remove labels with area > max_cell_area. Area is computed as the number of
    pixels.
    :param remove_edge_labels: (def False) Boolean to remove labels that touch the edge of the image.
    :param mask: (def None) (row, col) or (n, row, col) np.ndarray binary mask. If provided, remove labels that don't
    overlap enough with the mask.
    :param min_mask_overlap: (def 0.8) Remove labels that don't have at least min_mask_overlap of their pixels
    within the mask.
    :param phagocytosis: (def False) Boolean to remove labels that are completely surrounded by another label.
    :param labels_class: (def None) np.array with the same size as labels. Each pixel has class False or True.
    :param min_class_prop: (def 1.0) Scalar. Minimum proportion of pixels with class True that an object must have to be
    accepted. If min_class_prop=1.0, then all pixels must be of class True. If min_class_prop=0.6, then at least 60% of
    pixels need to be of class True.
    :return:
    * labels: (row, col) or (n, row, col) np.ndarray with removed labels as requested.
    * is_removed_edge_label: (row, col) or (n, row, col) boolean np.ndarray. True pixels belong to edge labels that were
      removed.
    """ # if mask provided, it must have the same shape as the labels array # if labels_class provided, it must have the same shape as the labels array # convert (row, col) labels to (1, row, col), so that we can use the same code for one or multiple inputs # loop label images # remove labels that are too small # remove labels that are not substantially within the mask # count number of pixels in each label # create a lookup table for quick search of label area # check for each original label whether it is at least min_mask_overlap (def 80%) covered by the mask # remove objects that don't contain enough pixels of class 1 # list of labels that are not 0 # loop labels # pixels that belong to the current label # proportion of class=True pixels # delete the label if it doesn't contain enough pixels of class True # remove labels that are completely surrounded by another label # background label # a donut is a label with another label inside # unless we find a donut, by default we stop # if this label has another label inside # fill up the donut # remove large objects # remove edge segmentations, because in general they correspond to incomplete objects # bool of pixels on edge cells # remove dummy dimension if the input was 2D
(row, col) or (n, row, col) np.ndarray with removed labels as requested.
is_removed_edge_label: (row, col) or (n, row, col) boolean np.ndarray. True pixels belong to edge labels that were
removed.
( row col ) or ( n row col ) np . ndarray with removed labels as requested . is_removed_edge_label : ( row col ) or ( n row col ) boolean np . ndarray . True pixels belong to edge labels that were removed .
labels
(row, col) or (n, row, col) np.ndarray with segmentation labels.
( row col ) or ( n row col ) np . ndarray with segmentation labels .
min_cell_area
(def 0) Remove labels with area < min_cell_area. Area is computed as the number of pixels.
( def 0 ) Remove labels with area < min_cell_area . Area is computed as the number of pixels .
max_cell_area
(def np.inf) Remove labels with area > max_cell_area. Area is computed as the number of
pixels.
( def np . inf ) Remove labels with area > max_cell_area . Area is computed as the number of pixels .
remove_edge_labels
(def False) Boolean to remove labels that touch the edge of the image.
( def False ) Boolean to remove labels that touch the edge of the image .
mask
(def None) (row, col) or (n, row, col) np.ndarray binary mask. If provided, remove labels that don't
overlap enough with the mask.
( def None ) ( row col ) or ( n row col ) np . ndarray binary mask . If provided remove labels that don ' t overlap enough with the mask .
min_mask_overlap
(def 0.8) Remove labels that don't have at least min_mask_overlap of their pixels
within the mask.
( def 0 . 8 ) Remove labels that don ' t have at least min_mask_overlap of their pixels within the mask .
phagocytosis
(def False) Boolean to remove labels that are completely surrounded by another label.
( def False ) Boolean to remove labels that are completely surrounded by another label .
labels_class
(def None) np.array with the same size as labels. Each pixel has class False or True.
( def None ) np . array with the same size as labels . Each pixel has class False or True .
min_class_prop
None