Source code for giant.image_processing.image_segmenter

from dataclasses import dataclass
from typing import NamedTuple

import numpy as np
from numpy.typing import NDArray

import cv2

from giant.image_processing.otsu import otsu

from giant.utilities.options import UserOptions
from giant.utilities.mixin_classes.user_option_configured import UserOptionConfigured
from giant.utilities.mixin_classes.attribute_equality_comparison import AttributeEqualityComparison
from giant.utilities.mixin_classes.attribute_printing import AttributePrinting


[docs] class ImageSegmenterOut(NamedTuple): labeled_image: NDArray[np.int32] """ An image with labeled foreground objects (>=0) of the same shape as the input and dtype np.int32 """ foreground_image: NDArray[np.uint8] """ A boolean array the same shape as the input with 0 in the background pixels and 1 in the foreground pixels """ stats: NDArray[np.int32] """ The stats vector returned from opencv's connectectedComponentsWithStats as a nx5 array. The columns are [left x coordinate, top y coordinate, width, height, area] in pixels. It is best to access the appropriate column using `cv2.CC_STAT_LEFT`, `cv2.CC_STAT_TOP`, `cv2.CC_STAT_WIDTH`, `cv2.CC_STAT_HEIGHT`, or `cv2.CC_STAT_AREA` in case opencv ever changes the order. Each row corresponds to the object number in the labeled image """ centroids: NDArray[np.float64] """ The unweighted centroids of each blob in the image as a nx2 array (x, y) in pixels. """
[docs] @dataclass class ImageSegmenterOptions(UserOptions): otsu_levels: int = 2 """ This sets the number of levels to attempt to segment the histogram into for Otsu based multi level thresholding. See the :func:`.otsu` function for more details. """ minimum_segment_area: int = 10 """ This sets the minimum area for a segment to be considered not noise. Segments with areas less than this are discarded as noise spikes """ minimum_segment_dn: float = 200 """ The minimum that the average DN for a segment must be for it to not be discarded as the background. Segments with average DNs less than this are discarded as the background """
[docs] class ImageSegmenter(UserOptionConfigured[ImageSegmenterOptions], ImageSegmenterOptions, AttributePrinting, AttributeEqualityComparison): """ This class segments images into foreground and background objects using a multi-level Otsu thresholding approach. It is configured using the :class:`ImageSegmenterOptions` dataclass, which specifies parameters such as the number of Otsu levels, minimum segment area, and minimum segment DN. The main functionality is provided by the :meth:`__call__` method, which takes an input image and returns a :class:`ImageSegmenterOut` named tuple containing the segmentation results. Example usage:: segmenter = ImageSegmenter() segment_results = segmenter(image) """ def __init__(self, options: ImageSegmenterOptions | None = None) -> None: """ :param options: the options configuring the image segmenter. If `None`, defaults will be used """ super().__init__(ImageSegmenterOptions, options=options)
[docs] def __call__(self, image: NDArray) -> ImageSegmenterOut: """ This method attempts to segment images into foreground/background objects. The objects are segmented by #. Performing a multi-level Otsu threshold on the image #. Choosing all but the bottom level from Otsu as likely foreground. #. Performing connected components on all the likely foreground objects #. Rejecting connected objects where the DN is less than the :attr:`minimum_segment_dn` #. Rejecting connected objects where the area is less than the :attr:`minimum_segment_area` The resulting objects are returned as a label matrix, where values >=1 indicate a pixel containing a foreground object (values of 0 are the background object). In addition, the statistics about the foreground objects are returned. :param image: The image to attempt to segment :return: A named tuple containing the label array, a boolean array specifying foreground objects, a stats array about the labels in order, and the centroids of the segments """ # threshold the image levels, thresholded = otsu(image, self.otsu_levels) if float(levels[0]) > self.minimum_segment_dn: print(f'warning, the minimum Otsu level is greater than the minimum segment DN. This could indicate that ' f'there is an issue with your settings.\n\tminimum_segment_dn = {self.minimum_segment_dn}\n\t' f'otsu_level = {levels[0]}') foreground_image = (thresholded >= 1).astype(np.uint8) _, labeled, stats, centroids = cv2.connectedComponentsWithStats(foreground_image) out_labeled = -np.ones(labeled.shape, dtype=np.int32) out_stats = [] out_centroids = [] stored_ind = 0 sorted_labs = np.argsort(-stats[:, cv2.CC_STAT_AREA]) # sort the labels by size for ind in sorted_labs: stat = stats[ind] centroid = centroids[ind] if stat[cv2.CC_STAT_AREA] < self.minimum_segment_area: break # since we are going in reverse size order if we get here we're done boolean_label = labeled == ind if np.median(image[boolean_label]) < self.minimum_segment_dn: continue out_labeled[boolean_label] = stored_ind out_stats.append(stat) out_centroids.append(centroid) stored_ind += 1 return ImageSegmenterOut(out_labeled, foreground_image, np.array(out_stats), np.array(out_centroids))