Source code for pycat.toolbox.segmentation_tools

"""
Image Segmentation and Analysis Module for PyCAT 

This module provides functions for image segmentation and analysis, including local thresholding, watershed segmentation,
felzenszwalb segmentation, cellpose segmentation, random forest pixel classification, and more. These functions are designed 
to process grayscale images and binary masks, segment objects of interest, and extract relevant features for further analysis. 
Segmentation and post-segmentation filtering and processing functions are contained within to ensure accurate and reliable
segmentation results.

Author
------
    Christian Neureuter, GitHub: https://github.com/cneureuter

Date
----
    4-20-2024
"""

# Standard library imports
import math 

# Third party imports
import numpy as np
import skimage as sk
import cv2
import scipy.ndimage as ndi
import scipy.stats as stats
import pandas as pd
from cellpose import models
from sklearn.ensemble import RandomForestClassifier
import napari
from napari.utils.notifications import show_info as napari_show_info
from napari.utils.notifications import show_warning as napari_show_warning

# Local application imports
from pycat.toolbox.label_and_mask_tools import binary_morph_operation, opencv_contour_func, extend_mask_to_edges
from pycat.ui.ui_utils import refresh_viewer_with_new_data, add_image_with_default_colormap
from pycat.utils.general_utils import dtype_conversion_func, check_contrast_func
from pycat.utils.math_utils import remove_outliers_iqr
from pycat.toolbox.image_processing_tools import apply_rescale_intensity, rb_gaussian_bg_removal_with_edge_enhancement





[docs] def local_thresholding_func(image, window_size, k_val=-0.5, mode='AND'): """ Applies local thresholding on the input image based on the specified method and parameters. Local thresholding is applied using either the Niblack or Sauvola method, or a combination thereof, to produce a binary mask that highlights regions of interest in the image based on local pixel value variations. Parameters ---------- image : numpy.ndarray The input grayscale image to undergo thresholding. window_size : int Size of the window for local threshold calculations. Adjusted to the nearest odd number if even. k_val : float, optional The parameter influencing the threshold computation for both Niblack and Sauvola methods. Defaults to -0.5. mode : str, optional Specifies the thresholding method or the combination of binary masks. Valid options are 'Niblack', 'Sauvola', 'AND' (intersection of Niblack and Sauvola masks), and 'OR' (union of Niblack and Sauvola masks). Defaults to 'AND'. Returns ------- thresh_mask : numpy.ndarray Binary mask from the applied thresholds, indicating areas of interest (1) against the background (0). Raises ------ ValueError If the mode provided is not supported. Example ------- Applying combined Niblack and Sauvola thresholding with a window size of 15 and a k-value of -0.5: >>> image = np.random.rand(100, 100) >>> mask = local_thresholding_func(image, 15, -0.5, 'AND') >>> mask.shape (100, 100) """ # Ensure window size is odd to meet the thresholding function requirements window_size = int(window_size) if window_size % 2 == 0: window_size += 1 # Adjust to the next odd number if even # Compute thresholds and binary masks using Niblack and/or Sauvola methods as required by the mode if mode in ['AND', 'OR', 'Niblack']: # Calculate Niblack threshold and apply to image thresh_niblack = sk.filters.threshold_niblack(image, window_size=window_size, k=k_val) binary_niblack = image > thresh_niblack # Create binary mask if mode in ['AND', 'OR', 'Sauvola']: # Calculate Sauvola threshold and apply to image thresh_sauvola = sk.filters.threshold_sauvola(image, window_size=window_size, k=k_val) binary_sauvola = image > thresh_sauvola # Create binary mask # Combine or select the masks based on the mode specified by the user if mode == 'AND': # Logical AND combines the masks, keeping only overlapping true regions thresh_mask = np.logical_and(binary_niblack, binary_sauvola) elif mode == 'OR': # Logical OR combines the masks, including any true regions from either thresh_mask = np.logical_or(binary_niblack, binary_sauvola) elif mode == 'Niblack': thresh_mask = binary_niblack # Use Niblack mask directly elif mode == 'Sauvola': thresh_mask = binary_sauvola # Use Sauvola mask directly else: # Handle unsupported modes by raising an error raise ValueError("Invalid mode. Supported modes are 'Niblack', 'Sauvola', 'AND', and 'OR'.") # Optional: Apply morphological operations based on mode to refine the mask further if mode in ['AND', 'OR']: # The type of morphological operation could depend on the combined method operation_mode = 'Opening' if mode == 'AND' else 'Closing' thresh_mask = binary_morph_operation(thresh_mask, iterations=3, element_size=1, element_shape='Disk', mode=operation_mode) return thresh_mask
[docs] def run_local_thresholding(k_slider, window_slider, mode_dropdown, viewer): """ Applies local thresholding to an active image layer in a Napari viewer based on user inputs from sliders and a dropdown menu. The process uses either Niblack, Sauvola, or a combination of these methods to highlight areas of interest in the image. Parameters ---------- k_slider : QSlider A slider widget to set the k-value for thresholding, adjusting the sensitivity of the method. window_slider : QSlider A slider widget to set the window size for local threshold calculations. mode_dropdown : QComboBox A dropdown to select the thresholding mode: 'Niblack', 'Sauvola', 'AND', or 'OR'. viewer : napari.viewer.Viewer The viewer instance where the processed image will be displayed. Raises ------ Error If no active image layer is selected. Notes ----- This function retrieves settings from the sliders and dropdown, applies the thresholding to the selected image, and updates the viewer by either adding a new layer or updating an existing one with the processed image. """ # Convert slider value to k-value and retrieve window size k_value = (k_slider.value() * 0.01) - 0.5 # Adjust slider value to k-value range window_size = window_slider.value() # Directly use slider value for window size # Identify the currently active layer active_layer = viewer.layers.selection.active current_active_layer_name = active_layer.name # Store name for later use # Verify active layer is a Napari image layer if active_layer is not None and isinstance(active_layer, napari.layers.Image): image = active_layer.data # Extract image data for processing else: # If no valid image layer is active, raise an error napari_show_warning("No active image layer selected.") return # Apply local thresholding to the image and convert result to integer for display thresh_mask = local_thresholding_func(image, window_size, k_val=k_value, mode=mode_dropdown).astype(int) # Update or add the processed layer to the viewer layer_name = f'Locally Thresholded {current_active_layer_name}' # Name for the new or updated layer existing_layer = next((layer for layer in viewer.layers if layer.name == layer_name), None) if existing_layer: # Update existing layer with the thresholded mask refresh_viewer_with_new_data(viewer, existing_layer, thresh_mask) # Refresh the viewer to display changes else: viewer.add_labels(thresh_mask, name=layer_name) # Add new layer # Reset the active layer to update it in the viewer viewer.layers.selection.active = viewer.layers[current_active_layer_name]
[docs] def apply_watershed_labeling(original_image, binary_mask, sigma=1.5): """ Apply watershed segmentation to an image for labeling different segments. The segmentation is based on a binary mask that defines the regions of interest. The function first converts the original image to a suitable dtype, applies Gaussian filtering to smooth the image, calculates the distance transform of the binary mask, and then performs the watershed segmentation on the smoothed distance map. Finally, it refines the segmentation by a binary morphological operation and labels the segments. Parameters ---------- original_image : numpy.ndarray The original image to be segmented. It can be of any dimensional shape. binary_mask : numpy.ndarray A binary mask defining the regions of interest in the `original_image`. It must have the same shape as `original_image`. sigma : float, optional The sigma value for the Gaussian filter applied to the distance transform. This controls the amount of smoothing. Default is 1.5. Returns ------- labeled_segments : numpy.ndarray An array of the same shape as `original_image` and `binary_mask`, containing labels for different segments identified by the watershed algorithm. Notes ----- The watershed algorithm is sensitive to the number of local maxima in the distance transform, which are used as markers. The sigma parameter can be adjusted to control the smoothing applied to the distance transform, thus influencing the segmentation result. This function utilizes a disk-shaped structuring element for the final morphological operation to refine the segmentation. The size and shape of this element can be adjusted for different applications. Examples -------- >>> original_image = np.array([...]) # Some image data >>> binary_mask = np.array([...]) # A binary mask for the image >>> labeled_segments = apply_watershed_labeling(original_image, binary_mask, sigma=1.5) """ # Convert the original image to 16-bit unsigned integers for processing image = dtype_conversion_func(original_image, output_bit_depth='uint16') # Ensure the binary mask is a boolean array binary_mask = np.asarray(binary_mask).astype(bool) # Compute the distance transform of the binary mask distance = ndi.distance_transform_edt(binary_mask) # Apply Gaussian filter to the distance transform with user-defined sigma blurred_distance = ndi.gaussian_filter(distance, sigma=sigma) # Identify local maxima in the blurred distance map as markers for watershed max_coords = sk.feature.peak_local_max(blurred_distance, labels=binary_mask) local_maxima = np.zeros_like(image, dtype=bool) local_maxima[tuple(max_coords.T)] = True # Label the local maxima markers = sk.measure.label(local_maxima) # Apply the watershed algorithm using the negative blurred distance map and markers labels = sk.segmentation.watershed(-blurred_distance, markers, mask=binary_mask, watershed_line=True) # Create a mask where labels are assigned (segmented regions) agreement_mask = labels > 0 # Refine the segmentation with a binary morphological operation agreement_mask = binary_morph_operation(agreement_mask, iterations=3, element_size=1, element_shape='Disk', mode='Opening') # Label the refined segments labeled_segments = sk.measure.label(agreement_mask) return labeled_segments
[docs] def opencv_watershed_func(binary_mask, original_image=None, dist_thresh=0.5, sigma=3.5, dilation_size=2, dilation_iterations=3): """ Applies the Watershed algorithm to segment objects from a binary mask of an image. This function refines the binary mask using morphological operations, applies a distance transform, and uses the Watershed algorithm to delineate separate objects. Optionally, the algorithm can utilize the original image for improved segmentation accuracy. Parameters ---------- binary_mask : numpy.ndarray A binary mask where the contours are to be detected and drawn. The mask should be in a format compatible with OpenCV (usually a binary image). original_image : numpy.ndarray, optional The original intensity image which, if provided, should match the dimensions of `binary_mask`. dist_thresh : float, optional Threshold for the distance transform, specified as a fraction of its maximum value. Defaults to 0.5. sigma : float, optional The standard deviation for Gaussian filtering, used to smooth the distance transform and the original image if provided. Defaults to 3.5. dilation_size : int, optional The size of the structuring element used for dilation, which helps define sure background areas. Defaults to 2. dilation_iterations : int, optional The number of iterations for dilation, used to enhance the background determination. Defaults to 3. Returns ------- watershed_contours : numpy.ndarray A binary mask indicating the boundary contours of segmented objects, with the same dimensions as the input `binary_mask`. Raises ------ ValueError If the dimensions of the original image and binary mask do not match. Notes ----- The function performs an initial morphological opening to clean up small noise in the mask, followed by dilation to determine sure background areas. It applies a Gaussian blur to smooth the distance transform and optionally the original image, uses a threshold to identify sure foreground areas, and subtracts the foreground from the background to define regions of uncertainty. The Watershed algorithm is then applied using either the original image or the refined mask, depending on the input provided. The resulting segmented boundaries are returned as a binary mask. """ # Ensure binary_mask is boolean binary_mask = binary_mask.astype(bool) # Apply morphological opening to clean up small noise in the mask mask = binary_morph_operation(binary_mask, iterations=3, element_size=2, element_shape='Disk', mode='Opening') # Dilation to find sure background area sure_bg = binary_morph_operation(mask, iterations=dilation_iterations, element_size=dilation_size, element_shape='Disk', mode='Dilation') # Convert mask to uint8 for distance transform mask = mask.astype(np.uint8) * 255 mask_copy = mask.copy() # Compute the distance transform dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 5) # Apply Gaussian blur to the distance transform #dist_transform = cv2.GaussianBlur(dist_transform, (5,5), sigma, sigma) dist_transform = ndi.gaussian_filter(dist_transform, sigma=sigma) # Thresholding the distance transform to find sure foreground area ret, sure_fg = cv2.threshold(dist_transform, dist_thresh * dist_transform.max(), 255, 0) sure_fg = np.uint8(sure_fg) # Ensure the sure background matches the sure foreground format sure_bg = np.uint8(sure_bg) * 255 # Find unknown region by subtracting foreground from background unknown_region = cv2.subtract(sure_bg, sure_fg) # Mark connected components in the foreground ret, markers = cv2.connectedComponents(sure_fg) markers += 1 # Increment all labels so background is not 0, but 1 markers[unknown_region == 255] = 0 # Mark unknown regions with zero # Process the original image if it is provided if original_image is not None: if original_image.shape[:2] != mask.shape[:2]: raise ValueError("The original image and mask must have the same dimensions.") # Apply Gaussian filtering and normalize the original image image = ndi.gaussian_filter(original_image, sigma=sigma) image = (image - np.min(image)) / (np.max(image) - np.min(image)) * 255 image = image.astype(np.uint8) image_bgr = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Apply watershed using the processed original image watershed_markers = cv2.watershed(image_bgr, markers) else: # Apply watershed using the binary mask watershed_markers = cv2.watershed(cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR), markers) # Extract contours from the watershed markers contours_list = [] for label in np.unique(watershed_markers)[2:]: # Skip the background and border target = np.where(watershed_markers == label, 255, 0).astype(np.uint8) contours, _ = cv2.findContours(target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours_list.append(contours[0]) # Draw contours on the mask copy watershed_contours = mask_copy.copy() watershed_contours = cv2.drawContours(watershed_contours, contours_list, -1, 0, thickness=2) # Return the final contours as a binary mask return watershed_contours.astype(bool)
def _weight_mean_color(graph, src, dst, n): """ Callback to handle merging nodes by recomputing mean color. This function is a utility designed to facilitate the merging process in a Region Adjacency Graph (RAG) by calculating the weight of the edge that will connect the merged node to its neighbors. The weight is determined based on the absolute difference in mean color between the `dst` node and its neighbors (`n`). It assumes the mean color of `dst` has already been updated to reflect the merging. Parameters ---------- graph : RAG The graph under consideration. src, dst : int The vertices in `graph` to be merged. n : int A neighbor of `src` or `dst` or both. Returns ------- data : dict A dictionary with the `"weight"` attribute set as the absolute difference of the mean color between node `dst` and `n`. """ # Calculate the difference in mean color between `dst` and neighbor `n` diff = graph.nodes[dst]['mean color'] - graph.nodes[n]['mean color'] # Use numpy's linear algebra norm function to compute the Euclidean distance # This distance represents the absolute difference in mean color diff = np.linalg.norm(diff) # Return a dictionary with the calculated weight return {'weight': diff}
[docs] def merge_mean_color(graph, src, dst): """ Callback called before merging two nodes of a mean color distance graph. Prior to merging two nodes in a RAG, this function updates the `dst` node's attributes to reflect the combined color information of both `src` and `dst`. This is crucial for accurately computing the mean color of the merged node, ensuring the graph's integrity and the accuracy of its color representation. Parameters ---------- graph : RAG The graph under consideration. src, dst : int The vertices in `graph` to be merged. """ # Add the `total color` of `src` to `dst` to reflect merging graph.nodes[dst]['total color'] += graph.nodes[src]['total color'] # Similarly, combine the `pixel count` of both nodes graph.nodes[dst]['pixel count'] += graph.nodes[src]['pixel count'] # Recalculate `mean color` of `dst` to account for the merged node's new color data graph.nodes[dst]['mean color'] = (graph.nodes[dst]['total color'] / graph.nodes[dst]['pixel count'])
[docs] def felzenszwalb_segmentation_and_merging(image, scale=7.0, sigma=0.5, min_size=2): """ Performs image segmentation using Felzenszwalb's method followed by merging based on color similarity. This function applies an initial segmentation to the input image using Felzenszwalb's efficient graph-based segmentation algorithm. It then constructs a Region Adjacency Graph (RAG) from the initial segments and merges segments based on the similarity of their mean color (intensity for grayscale). The merging process is controlled by comparing the color distance between segments against a threshold derived from the image's standard deviation. Parameters ---------- image : numpy.ndarray The input image to segment. Can be a grayscale or RGB image. scale : float, optional The scale parameter influences the size of the clusters in the initial segmentation. Higher values result in larger clusters. This controls how aggressively pixels are merged together in the initial segmentation. Defaults to 7.0. sigma : float, optional The standard deviation for the Gaussian kernel used in smoothing the image before segmenting. This preprocessing step can help reduce noise and improve the quality of segmentation. Defaults to 0.5. min_size : int, optional The minimum size of final segments. Smaller segments are merged during post-processing to ensure that every segment is at least this size. Defaults to 2. Returns ------- segmented_img : numpy.ndarray The segmented image with segments represented by the average color (or intensity) of their respective pixels, returned in the same data type as the input. Notes ----- - 'scale' directly influences how aggressively pixels are merged in the initial segmentation, correlating with the 'k' parameter in Felzenszwalb's paper. - Appropriate selection of 'scale', 'sigma', and 'min_size' is crucial for optimal segmentation results, depending on the image's specific characteristics. """ # Store the input image's data type to convert back at the end input_dtype = str(image.dtype) # Convert input image to float32 for processing; necessary for certain operations and ensures consistency img = dtype_conversion_func(image, output_bit_depth='float32') # Apply Felzenszwalb's segmentation algorithm to the image # This step segments the image into regions based on pixel similarity and the specified parameters segments_fz = sk.segmentation.felzenszwalb(img, scale=scale, sigma=sigma, min_size=min_size) # Construct a Region Adjacency Graph (RAG) from the initial segmentation # The RAG represents how segments are connected and allows for merging based on further criteria g = sk.graph.rag_mean_color(img, segments_fz, mode='similarity') # Define a threshold for merging segments based on color similarity # This threshold is set dynamically based on the square of the normalized float image's standard deviation (so it will be a sub 1 value) threshold = (np.std(img)**2)/2 # Merge segments hierarchically based on their mean color similarity # `merge_func` determines how the color information is combined when segments are merged labels = sk.graph.merge_hierarchical(segments_fz, g, thresh=threshold, rag_copy=False, in_place_merge=True, merge_func=merge_mean_color, weight_func=_weight_mean_color) # Convert the merged segment labels into a segmented image with averaged colors # The `label2rgb` function assigns the average color of a segment to all its pixels merged_fz = sk.color.label2rgb(labels, img, kind='avg', bg_label=0) segmented_img = merged_fz[..., 0] # Extract the grayscale channel for simplicity # Convert the segmented image back to the original input data type for consistency with the input segmented_img = dtype_conversion_func(segmented_img, output_bit_depth=input_dtype) return segmented_img
[docs] def run_fz_segmentation_and_merging(scale_input, sigma_input, min_size_input, viewer): """ Applies Felzenszwalb's segmentation and merging to an active image layer in a Napari viewer based on user-provided settings. This function allows for dynamic interaction, enabling users to adjust segmentation parameters in real-time. Parameters ---------- scale_input : QLineEdit Input field for the scale parameter, affecting the size of the initial segmentation clusters. sigma_input : QLineEdit Input field for the sigma parameter, controlling the degree of Gaussian smoothing prior to segmentation. min_size_input : QLineEdit Input field for the minimum size of the segments to be considered in the final output. viewer : napari.viewer.Viewer Viewer instance where the segmented image will be displayed. Raises ------ Error If no active image layer is selected. """ # Check for an active image layer in the viewer active_layer = viewer.layers.selection.active if active_layer is None or not isinstance(active_layer, napari.layers.Image): raise ValueError("No active image layer selected") image = active_layer.data # Extract the image data from the active layer # Read scale, sigma, and min_size from inputs, defaulting to preset values if empty scale = float(scale_input.text()) if scale_input.text() else 7.0 sigma = float(sigma_input.text()) if sigma_input.text() else 0.5 min_size = int(min_size_input.text()) if min_size_input.text() else 2 # Apply the segmentation and merging process to the selected image layer segmented_img = felzenszwalb_segmentation_and_merging(image, scale=scale, sigma=sigma, min_size=min_size) # Display the segmented image in the viewer add_image_with_default_colormap(segmented_img, viewer, name=f"Felzenszwalb Segmented {active_layer.name}")
[docs] def fz_segmentation_and_binarization(image, mask, ball_radius): """ Applies Felzenszwalb's segmentation method followed by additional processing to convert the segmented image into a refined binary mask. This involves contrast adjustments, morphological operations, and local thresholding to highlight distinct objects within a specified region of interest. Additionally, external contours are detected and filled to ensure solid object representation in the binary mask. Parameters ---------- image : numpy.ndarray The input grayscale image for segmentation. mask : numpy.ndarray A binary mask defining the region of interest where segmentation is focused. ball_radius : int The radius influencing the segmentation sensitivity and scale, particularly used in local thresholding. Returns ------- boolean_mask : numpy.ndarray A binary mask refined from the segmented image, highlighting detected objects within the region defined by the input mask. Notes ----- - The process dynamically adjusts to the 'ball_radius' to ensure appropriate scale processing for different image details. - A correct 'ball_radius' is crucial for optimal segmentation and post-processing results. - The function assumes the input image has undergone basic preprocessing for noise reduction and contrast enhancement. - The binary mask is further processed through morphological operations and local thresholding to ensure a clean and usable output. """ img = dtype_conversion_func(image, output_bit_depth='float32') # Convert image to float32 for processing object_radius = ball_radius / 1.5 # Adjust object radius based on ball_radius for segmentation scale # Perform initial segmentation with adjusted parameters fz_segmented_img = felzenszwalb_segmentation_and_merging(img, scale=object_radius, sigma=0.5, min_size=2) # Check image contrast and return empty mask if insufficient for segmentation contrast_flag = check_contrast_func(fz_segmented_img) if contrast_flag: return np.zeros_like(img, dtype=bool) clip_limit = 0.0025 # Adaptive histogram equalization parameter k_size = math.ceil(ball_radius * 4) # Set a window size of ~ 2x larger than the object diameter for CLAHE # Enhance segmented image using adaptive histogram equalization segmented_img = sk.exposure.equalize_adapthist(fz_segmented_img, kernel_size=k_size, clip_limit=clip_limit) # Apply morphological operations to smooth the segmented image segmented_img = ndi.grey_dilation(segmented_img, footprint=sk.morphology.disk(1)) segmented_img = ndi.grey_erosion(segmented_img, footprint=sk.morphology.disk(1)) # Further smooth the image using Gaussian filtering segmented_img = ndi.gaussian_filter(segmented_img, sigma=0.5) #viewer.add_image(segmented_img, name='Segmented Image') # Refine segmentation into a binary mask using local thresholding segmented_mask = local_thresholding_func(segmented_img, int(ball_radius)) # Determine the maximum area for objects based on the input cell mask max_area = (np.sum(mask.astype(bool)) / 4) # Set maximum area based on the input mask # Detect external contours and fill them to ensure solid object representation contour_mask = opencv_contour_func(segmented_mask, max_area=max_area) # Combine with the eroded input mask to refine the final mask and reduce edge artifacts boolean_mask = (contour_mask * ndi.binary_erosion(mask, sk.morphology.disk(1))).astype(bool) # Dilate the mask to ensure objects are fully covered boolean_mask = binary_morph_operation(boolean_mask, iterations=1, element_size=1, element_shape='Disk', mode='Dilation') return boolean_mask
[docs] def cellpose_segmentation(image, object_diameter): """ Perform cell segmentation on an image using Cellpose, a deep-learning-based method for cell/nucleus segmentation. This function processes an input image to enhance its features and applies the Cellpose deep learning model for cell and nucleus segmentation. It focuses on segmenting the image into distinct cell or nucleus areas. The `object_diameter` parameter is used to determine the scale of the objects to be segmented. Parameters ---------- image : numpy.ndarray The input image for cell segmentation, expected to be in a format compatible with Cellpose. object_diameter : int The approximate diameter (in pixels) of the cells or nuclei to be segmented in the image. This value scales the segmentation process. Returns ------- mask : numpy.ndarray A binary mask of the segmented cells/nuclei in the input image, refined to enhance separation between adjacent objects and extend segmentation to image edges. Notes ----- - Cellpose model 'cyto2' is used by default for broader applicability in cell and nucleus segmentation. - The input image is processed through several steps including dynamic range conversion, adaptive histogram equalization, denoising, and intensity rescaling to optimize it for segmentation. - Ensure that the Cellpose library is installed and properly configured in your environment. For more information on Cellpose, see: https://cellpose.readthedocs.io/en/latest/. - This function assumes the availability of several skimage and custom preprocessing functions to prepare the image for segmentation. """ # Initialize Cellpose with the preferred model for cell and nucleus segmentation. model = models.Cellpose(model_type='cyto2') #model = models.Cellpose(model_type='nuclei') # Placeholders for future users to specify model type #model = models.Cellpose(gpu=True, model_type='nuclei') # This just shows how to use GPU acceleration # Preprocess the image to improve segmentation quality. img = dtype_conversion_func(image, 'float32') # Convert image to float32 for processing img = sk.exposure.equalize_adapthist(img, kernel_size=object_diameter//2, clip_limit=0.0025) img = sk.restoration.denoise_wavelet(img) img = apply_rescale_intensity(img, out_min=0.0, out_max=1.0) image_preprocessed = dtype_conversion_func(img, 'uint16') # Convert the image to uint16 for Cellpose # Apply Cellpose model to segment cells/nuclei. masks, flows, styles, diams = model.eval(image_preprocessed, diameter=object_diameter, channels=[0,0], resample=True) # Post-process segmentation masks to improve results. mask = masks > 0 # Ensure the mask is binary # Split objects that are erroneously connected. deprecated method replaced by cv2 binary watershed #split_mask = split_touching_objects(mask, sigma=object_diameter//4) split_mask = opencv_watershed_func(mask) mask = binary_morph_operation(split_mask, iterations=7, element_size=3, element_shape='Disk', mode='Opening') mask = extend_mask_to_edges(mask, 3) # Extend the mask to eliminate the empty border cellpose leaves return mask
[docs] def run_cellpose_segmentation(image_layer, data_instance, viewer): """ Applies cell segmentation to an image layer using Cellpose and displays the results in the Napari viewer. Retrieves the necessary parameters from provided objects, executes cell segmentation with `cellpose_segmentation`, and integrates the resulting mask into the viewer as a new layer. Parameters ---------- image_layer : napari.layers.Image The image layer to be segmented. data_instance : object An object containing a data repository with segmentation parameters, such as 'cell_diameter'. viewer : napari.Viewer The viewer object where the segmentation results will be displayed. """ # Retrieve the image data and cell diameter from the data instance image = image_layer.data object_diameter = data_instance.data_repository['cell_diameter'] # Perform cell segmentation using Cellpose. cell_masks = cellpose_segmentation(image, object_diameter) # Add the segmentation results as a new label layer to the viewer. viewer.add_labels(cell_masks, name=f"Cellpose Segmentation on {image_layer.name}")
[docs] def train_and_apply_rf_classifier(image, training_labels, object_diameter): """ Trains and applies a Random Forest classifier to segment an image based on training labels. The function enhances the input image using adaptive histogram equalization and denoising techniques before training a Random Forest classifier. The classifier is then used to predict segmentation masks across the entire image. These masks are refined to improve the segmentation quality. Parameters ---------- image : numpy.ndarray The input image for segmentation, expected to be in grayscale or compatible format. training_labels : numpy.ndarray The ground truth labels for training the classifier, must be the same dimensions as the image. object_diameter : int The approximate diameter of the target objects in pixels, used to tailor image preprocessing. Returns ------- refined_masks : List[numpy.ndarray] A list of refined segmentation masks for each detected classification type, adjusted for segmentation quality. Notes ----- The segmentation process includes image preprocessing for feature enhancement, classifier training on specified regions, and applying this classifier to the whole image. The resulting masks are then refined through morphological operations and contour adjustments to produce the final segmented output. """ # Image preprocessing for enhanced segmentation performance img = dtype_conversion_func(image, 'float32') # Convert image to float32 for processing img = sk.exposure.equalize_adapthist(img, kernel_size=object_diameter//2, clip_limit=0.0025) img = sk.restoration.denoise_wavelet(img) # Training data preparation training_img_pixels = img[training_labels != 0] training_label_pxs = training_labels[training_labels != 0] # Random Forest classifier initialization and training rf_classifier = RandomForestClassifier(n_estimators=500, max_depth=4, criterion='entropy', n_jobs=-1) rf_classifier.fit(training_img_pixels.reshape(-1, 1), training_label_pxs) # Segmentation using the trained classifier prediction_pixels = img.reshape(-1, 1) predicted_labels = rf_classifier.predict(prediction_pixels).reshape(img.shape) predicted_labels -= 1 # Shift labels to start from 0 predicted_labels = predicted_labels.astype(np.uint8) # Convert to uint8 for compatibility # Refinement of predicted labels refined_labels = np.zeros_like(predicted_labels) for label in np.unique(predicted_labels)[1:]: # Skip label 0 (background) label_mask = predicted_labels == label #label_mask = binary_morph_operation(label_mask, mode='Fill Holes') label_mask = binary_morph_operation(label_mask, iterations=3, element_size=5, element_shape='Disk', mode='Opening') label_mask = binary_morph_operation(label_mask, iterations=5, element_size=3, element_shape='Disk', mode='Closing') #label_mask = opencv_watershed_func(label_mask) refined_labels[label_mask] = label # Convert to binary mask and label connected components binary_mask = refined_labels > 0 labeled_mask = sk.measure.label(binary_mask) # Remove small objects from the labeled mask min_area = (np.pi * (object_diameter / 2) ** 2) // 10 labeled_mask = sk.morphology.remove_small_objects(labeled_mask, min_size=min_area) binary_mask = labeled_mask > 0 # Use the binary mask to remove the small objects from the refined labels refined_labels *= binary_mask # Extend mask to the edges and refine each label's mask refined_labels = extend_mask_to_edges(refined_labels, 3) refined_masks = refine_labels_with_contours(refined_labels, min_area) return refined_masks
[docs] def refine_labels_with_contours(refined_labels, min_area): """ Refines segmentation masks for each label within a given input mask using contour detection and area filtering. This function iterates over each unique label in the input mask, extracts contours for each label using the specified minimum area criteria, and applies morphological operations to refine these contours. Parameters ---------- refined_labels : numpy.ndarray The input mask containing different labels for segmented regions, typically obtained from segmentation algorithms. min_area : int The minimum area threshold for contours to be considered during the refinement process. Only contours larger than this threshold are included. Returns ------- refined_masks : List[numpy.ndarray] A list of refined masks for each label present in `refined_labels`. Each mask in the list corresponds to a unique label and contains the refined contours for that label. Notes ----- The function first segregates each label within the input mask and then applies `opencv_contour_func` to detect and draw contours that meet the specified area criteria. It further refines these contours using a binary morphological operation (e.g., opening) to smooth edges and remove small artifacts. If no valid objects are found for a label after processing, a message is printed, and the label is skipped in the output. The resulting refined masks are returned as a list, one for each label, ensuring that the refined contours correspond to the initial segmented regions. """ # Initialize an empty list to store the refined masks for each label refined_masks = [] # Iterate over each unique label found in `refined_labels` (skip the background label, typically 0) for label in np.unique(refined_labels)[1:]: # Skip background label # Create a binary mask for the current label binary_mask = (refined_labels == label) # Find contours in the binary mask current_label_mask = opencv_contour_func(binary_mask, min_area) # Final post-processing steps for the current label mask current_label_mask = binary_morph_operation(current_label_mask, mode='Opening', iterations=7, element_size=3, element_shape='Disk') if np.sum(current_label_mask) == 0: napari_show_warning(f"RF Label {label+1} has no valid objects.") continue current_label_mask[current_label_mask > 0] = label # Assign the label value to the refined mask refined_masks.append(current_label_mask) # Store the refined mask for the current label return refined_masks
[docs] def run_train_and_apply_rf_classifier(image_layer, label_layer, data_instance, viewer): """ Facilitates the training and application of a Random Forest classifier on an image layer and displays the results in a Napari viewer. This function extracts the necessary data from the provided image and label layers, trains a Random Forest classifier based on the training labels, and applies this classifier to segment the image. The segmented results are then displayed as new layers in the viewer. Parameters ---------- image_layer : napari.layers.Image The layer containing the image data to be segmented. label_layer : napari.layers.Labels The layer containing label data used for training the classifier. data_instance : object An object containing additional parameters such as 'cell_diameter' needed for processing. viewer : napari.Viewer The viewer in which to display the segmented results. Notes ----- - Multiple refined masks are displayed in separate layers if more than one valid object classification is found. """ # Extract necessary data for segmentation object_diameter = data_instance.data_repository['cell_diameter'] image = image_layer.data training_labels = label_layer.data # Train and apply the Random Forest classifier for segmentation output_mask_list = train_and_apply_rf_classifier(image, training_labels, object_diameter) # Display the segmentation results in the viewer if len(output_mask_list) == 0: napari_show_info("No valid objects were found.") elif len(output_mask_list) == 1: viewer.add_labels(output_mask_list[0].astype(int), name=f"Random Forest Segmentation on {image_layer.name}") else: for idx, output_mask in enumerate(output_mask_list): output_mask = output_mask.astype(int) output_mask[output_mask > 0] = idx + 1 viewer.add_labels(output_mask, name=f"Random Forest Segmentation {idx+1} on {image_layer.name}")
[docs] def puncta_refinement_filtering_func(original_img, processed_img, puncta_mask, cell_mask, labeled_puncta_mask, min_spot_radius): """ Refines a segmentation mask by filtering based on intensity, size, shape, and local background conditions. It aims to ensure that detected objects are valid and significant relative to the cell and the background, employing multiple criteria including intensity thresholds, kurtosis, ellipticity, area conditions, and signal-to-noise ratio (SNR). Parameters ---------- original_img : numpy.ndarray The original image, before any processing. processed_img : numpy.ndarray The processed image, potentially after enhancing objects or other preprocessing steps. puncta_mask : numpy.ndarray A binary mask where the objects are identified, before refinement. cell_mask : numpy.ndarray A binary mask of the cell(s), used to define the cell background and exclude non-cell areas. labeled_puncta_mask : numpy.ndarray A labeled mask of the objects, where each punctum is assigned a unique label. min_spot_radius : float The minimum radius of objects, used in various calculations and filtering criteria. Returns ------- refined_puncta_mask : numpy.ndarray The refined binary mask of objects after applying the filtering criteria. Notes ----- This function applies a series of criteria to refine detected objects, including: - Local and global intensity thresholds to remove false objects. - Kurtosis to filter out objects with flat pixel intensity distributions, which are likely false positives. - Area conditions to exclude objects that are too large or too small. - Ellipticity to remove objects that are too long and narrow. - Gradient and SNR conditions to ensure objects stand out from their background and are not indistinguishable from noise. """ # Convert images to 16-bit for consistent intensity analysis original_image_16 = dtype_conversion_func(original_img, output_bit_depth='uint16') processed_image_16 = dtype_conversion_func(processed_img, output_bit_depth='uint16') refined_puncta_mask = puncta_mask.copy() # Calculate the Gaussian Gradient Magnitude (DoG) DoG_img = ndi.gaussian_gradient_magnitude(original_img, sigma=min_spot_radius) # Exclude puncta from the cell mask to analyze background cell_xor_puncta_mask = cell_mask ^ puncta_mask cell_bg = original_img[cell_xor_puncta_mask] # Refine background analysis by removing outliers for accurate mean and std dev calculation cell_bg_iqr = remove_outliers_iqr(cell_bg) cell_bg_mean = np.mean(cell_bg_iqr) cell_bg_std = np.std(cell_bg_iqr) # Measure properties of each object in the labeled mask properties = ('label', 'area', 'intensity_mean', 'axis_major_length', 'axis_minor_length') puncta_region_props_df = pd.DataFrame(sk.measure.regionprops_table(labeled_puncta_mask, intensity_image=original_img, properties=properties)) cell_area = np.sum(cell_mask) # Analyze each object individually for label in np.unique(labeled_puncta_mask)[1:]: # Create a binary mask for each object puncta_mask_holder = labeled_puncta_mask == label # Erode the mask for the gradient image eroded_puncta_holder = ndi.binary_erosion(puncta_mask_holder, sk.morphology.disk(1)) # Dilate the mask (encompases more of the full spot fluorescence to aviod its tails in the local bg) dilated_puncta_holder = ndi.binary_dilation(puncta_mask_holder, sk.morphology.disk(1)) # Dilate the mask by 3 pixels for local bg aroud the object dilated_local_mask = puncta_mask_holder.copy() for _ in range(3): dilated_local_mask = ndi.binary_dilation(dilated_local_mask, sk.morphology.disk(1)) # diamond element gives smaller dilations # The local bg is simply the dilated mask minus the puncta mask local_bg_mask = dilated_local_mask ^ dilated_puncta_holder # Collect pixel values from various masks and images for analysis # Get the pixels for each mask from the original image img_object_pixels = original_img[puncta_mask_holder] img_dilated_object_pixels = original_img[dilated_puncta_holder] img_local_bg_pixels = original_img[local_bg_mask] # Get the pixels for each mask from the processed image processed_object_pixels = processed_img[puncta_mask_holder] #processed_dilated_object_pixels = processed_img[dilated_puncta_holder] processed_local_bg_pixels = processed_img[local_bg_mask] # Get the pixels for each mask from the DoG gradient image gradient_object_pixels = DoG_img[eroded_puncta_holder] gradient_local_bg_pixels = DoG_img[dilated_puncta_holder ^ eroded_puncta_holder] # Get the local pixels from the 16 bit versions of the images img_local_pixels = original_image_16[dilated_local_mask] processed_local_pixels = processed_image_16[dilated_local_mask] # Calculate the local standard deviation as a quick check of variation in pixel intensity img_local_std_dev = np.std(img_local_pixels) processed_local_std_dev = np.std(processed_local_pixels) if img_local_std_dev < 2 or processed_local_std_dev < 2: refined_puncta_mask[labeled_puncta_mask == label] = 0 continue # Calculate the kurtosis of the pixel distributions in the mask img_object_kurtosis = stats.kurtosis(img_local_pixels) processed_object_kurtosis = stats.kurtosis(processed_local_pixels) # Calculate the mean and std dev of the pixel distributions in the masks img_object_mean = np.mean(img_object_pixels) processed_object_mean = np.mean(processed_object_pixels) gradient_object_mean = np.mean(gradient_object_pixels) img_dilated_object_mean = np.mean(img_dilated_object_pixels) img_local_bg_mean = np.mean(img_local_bg_pixels) processed_local_bg_mean = np.mean(processed_local_bg_pixels) gradient_local_bg_mean = np.mean(gradient_local_bg_pixels) img_local_bg_std = np.std(img_local_bg_pixels) processed_local_bg_std = np.std(processed_local_bg_pixels) # Calculate ellipticity and area from the region props dataframe df = puncta_region_props_df[puncta_region_props_df['label']==label] ellipticity = 1 - (df['axis_minor_length'].values[0]/df['axis_major_length'].values[0]) # Setup local intensity based conditions hwhm = np.sqrt(2*np.log(2)) # HWHM is approx 1.17 sigma local_intensity_condition = ( img_object_mean < (img_local_bg_mean + hwhm*img_local_bg_std) or # could use dilated object mean too processed_object_mean < (processed_local_bg_mean + hwhm*processed_local_bg_std) ) # Setup global intensity based conditions cell_intensity_condition = img_object_mean < cell_bg_mean # Setup kurtosis based conditions kurtosis_condition = img_object_kurtosis < -3.0 or processed_object_kurtosis < -3.0 # Setup area based conditions min_area = math.ceil(np.pi * min_spot_radius**2) area_condition = df['area'].values[0] > cell_area/4 or df['area'].values[0] < min_area # Setup ellipticity based condition ellipticty_condition = ellipticity > 0.99 # Setup gradient based condition gradient_condition = gradient_local_bg_mean < (gradient_object_mean + np.std(gradient_object_pixels)/4) # Setup 'local' SNR condition local_snr_condition = (img_dilated_object_mean/(img_local_bg_std+np.finfo(np.float32).eps)) <= 1.0 # Setup 'global' SNR condition global_snr_condition = (img_dilated_object_mean/(cell_bg_std+np.finfo(np.float32).eps)) <= 1.0 # If any of the conditions are met, remove the object from the mask if (local_intensity_condition or cell_intensity_condition or kurtosis_condition or area_condition or ellipticty_condition or gradient_condition or local_snr_condition or global_snr_condition): # remove the puncta from the mask refined_puncta_mask[labeled_puncta_mask == label] = 0 return refined_puncta_mask
[docs] def puncta_refinement_func(original_image, processed_image, puncta_mask, cell_mask, min_spot_radius=2): """ Refines a puncta mask through a series of image processing steps, including smoothing, morphological operations, refinement filtering, and watershed segmentation. This function is designed to improve the accuracy of puncta detection and segmentation by reducing noise and separating closely positioned objects. Parameters ---------- original_image : numpy.ndarray The original microscopy image, before any processing. This image is used to guide the refinement process and to apply the watershed segmentation. processed_image : numpy.ndarray The processed image, which has potentially undergone preprocessing steps to enhance puncta or otherwise prepare the image for segmentation. puncta_mask : numpy.ndarray A binary mask where puncta have been initially identified. This mask is subject to refinement through this function. cell_mask : numpy.ndarray A binary mask of the cell(s) used to define areas of interest and exclude regions outside of cells. min_spot_radius : float, optional The minimum radius of puncta, which influences several processing steps including smoothing and watershed segmentation. Default is 2. Returns ------- refined_mask : numpy.ndarray The refined binary mask of puncta after applying all processing and refinement steps. Notes ----- The refinement process includes: - Converting images to a suitable data type and smoothing based on the minimum spot size. - Applying binary opening to the initial puncta mask to remove single-pixel noise. - Labeling the puncta mask for individual puncta identification. - Refining the labeled puncta mask through custom filtering criteria (primarily based on the local intensity distribution aroud the object). - Separating closely positioned objects using watershed segmentation. - Further refining the segmentation to ensure accurate and distinct object detection, providing an iterative refinement approach. - Final morphological opening to clean up the segmentation result. """ # Convert image data types for processing original_img = dtype_conversion_func(original_image, 'float32') processed_img = dtype_conversion_func(processed_image, 'float32') puncta_mask = puncta_mask.astype(bool) cell_mask = cell_mask.astype(bool) # Smooth the images using a Gaussian filter based on the minimum spot size original_img = ndi.gaussian_filter(original_img, sigma=1.5) processed_img = ndi.gaussian_filter(processed_img, sigma=min_spot_radius) # Refine initial puncta mask to remove noise puncta_mask = binary_morph_operation(puncta_mask, iterations=2, element_size=1, element_shape='Cross', mode='Opening') # Label the puncta within the mask for individual analysis labeled_puncta_mask = sk.measure.label(puncta_mask) # First round of puncta refinement using filtering criteria refined_puncta_mask = puncta_refinement_filtering_func(original_img, processed_img, puncta_mask, cell_mask, labeled_puncta_mask, min_spot_radius) # Apply watershed segmentation to separate closely positioned puncta watershed_puncta_mask = apply_watershed_labeling(original_img, refined_puncta_mask, sigma=min_spot_radius/2) # Deprecated method involving cv2 watershed #watershed_puncta_mask = opencv_watershed_func(refined_puncta_mask, dist_thresh=0.5, sigma=min_spot_radius, dilation_size=1, dilation_iterations=3) #watershed_puncta_mask = sk.measure.label(watershed_puncta_mask) # Second round of refinement after watershed segmentation refined_puncta_mask = puncta_refinement_filtering_func(original_img, processed_img, refined_puncta_mask, cell_mask, watershed_puncta_mask, min_spot_radius) # Final morphological opening to clean up the segmentation refined_mask = binary_morph_operation(refined_puncta_mask, iterations=1, element_size=1, element_shape='Disk', mode='Opening') return refined_mask
[docs] def cell_mask_stretching(image, cell_masks): """ Enhances the contrast within specific areas of an image defined by cell masks, followed by smoothing operations. The function dilates the cell masks to include surrounding areas, then applies CLAHE (Contrast Limited Adaptive Histogram Equalization) to these regions for contrast enhancement. The areas are then slightly eroded to fit the original mask dimensions. After processing all masks, the background is zeroed out, and the image is smoothed using grey-scale morphological operations to avoid blurring. Parameters ---------- image : numpy.ndarray The input image to perform contrast stretching on. Must be a 2D array. cell_masks : numpy.ndarray A labeled mask image where each cell is represented by a unique integer label, and the background is 0. Returns ------- output_image : numpy.ndarray The image after applying contrast enhancement and smoothing operations, in the original data type. Notes ----- This function assumes the presence of at least one cell label in `cell_masks` (i.e., not all values are 0). It enhances only the areas defined by the masks, leaving the background unaffected except for smoothing. The CLAHE parameters are dynamically adjusted based on the object's estimated radius from its area. """ input_dtype = str(image.dtype) img = dtype_conversion_func(image, 'float32') # Convert image to float32 for processing # Copy the input image to apply contrast stretching img_contrast_stretched = img.copy() # Initialize a total cell mask with the same shape as the input image but with boolean type total_cell_mask = np.zeros_like(cell_masks, dtype=bool) for label in np.unique(cell_masks)[1:]: # Exclude the background label (0). # Create a mask for the current cell. cell_mask = (cell_masks == label).astype(bool) # Check the contrast of the cell contrast_flag = check_contrast_func(img * cell_mask) if contrast_flag: # Update the contrast-stretched image within the mask img_contrast_stretched[cell_mask] = 0 # Update the total cell mask. total_cell_mask |= cell_mask continue # Dilate the mask slightly to include a bit of the surrounding area. dilated_mask = ndi.binary_dilation(cell_mask, sk.morphology.disk(3)) # Create a masked version of the input image using the dilated mask. masked_cell = img * dilated_mask # Calculate parameters for CLAHE based on the object's size. mask_area = np.sum(cell_mask) # Total area of the cell object_rad = np.sqrt(mask_area / np.pi) # Estimated radius of the cell k_size = math.ceil(object_rad / 4) # Kernel size for CLAHE. clip_lim = 0.0025 # Clip limit for CLAHE. # Apply CLAHE to the masked cell. stretched_cell = sk.exposure.equalize_adapthist(masked_cell, kernel_size=k_size, clip_limit=clip_lim) # Erode the dilated mask to reduce artifacts at the edges. eroded_mask = ndi.binary_erosion(dilated_mask, sk.morphology.disk(3)).astype(bool) # Update the contrast-stretched image within the eroded mask img_contrast_stretched[eroded_mask] = stretched_cell[eroded_mask] # Update the total cell mask. total_cell_mask |= eroded_mask # Set the background (areas not covered by any cell mask) to 0. img_contrast_stretched[~total_cell_mask] = 0 # Apply grey dilation and erosion to smooth the image data without blurring. structuring_element = sk.morphology.disk(1) output_image = ndi.grey_dilation(img_contrast_stretched, footprint=structuring_element) output_image = ndi.grey_erosion(output_image, footprint=structuring_element) # Convert the output image back to the original data type. output_image = dtype_conversion_func(output_image, output_bit_depth=input_dtype) return output_image
[docs] def segment_subcellular_objects(original_image, pre_processed_image, cell_mask, cell_label, ball_radius, cell_df=None): """ Segments and refines subcellular objects within a specified cell mask from microscopy images. The function uses pre-processed images and cell-specific metrics to remove background, enhance edges, and segment objects like puncta. It then refines the segmentation based on image quality metrics such as kurtosis and signal-to-noise ratio (SNR). Parameters ---------- original_image : numpy.ndarray The original microscopy image before any processing. pre_processed_image : numpy.ndarray The image after pre-processing steps, ready for segmentation. cell_mask : numpy.ndarray A binary mask representing a single cell within which objects are to be segmented. cell_label : int or float The label identifying the current cell within `cell_df` or used for reporting. ball_radius : float The radius used in background removal and edge enhancement algorithms. cell_df : pandas.DataFrame, optional A DataFrame containing cell-specific metrics such as kurtosis and SNR. Default is None. Returns ------- refined_puncta_mask : numpy.ndarray The refined binary mask of segmented subcellular objects. puncta_mask : numpy.ndarray The initial binary mask of segmented subcellular objects before refinement. Notes ----- This function applies background removal and edge enhancement before segmenting objects. It assesses the quality of segmentation using contrast checks and refines the segmentation through a separate refinement function to ensure accurate object detection. """ # Convert images to float32 for consistent processing original_img = dtype_conversion_func(original_image, 'float32') pre_processed_img = dtype_conversion_func(pre_processed_image, 'float32') cell_mask = cell_mask.astype(bool) # Ensure mask is boolean # Initialize a flag indicating whether to perform background removal perform_bg_removal = True # Check if conditions are met to potentially skip background removal if cell_df is not None and not cell_df.empty: # Is no df, the user has not run cell analyzer cell_kurt = cell_df.loc[cell_df['label'] == cell_label, 'img_kurtosis'].values cell_gaussian_snr = cell_df.loc[cell_df['label'] == cell_label, 'gaussian_snr_estimate'].values # Check if kurtosis is NaN or Gaussian SNR is below 1.0 if np.isnan(cell_kurt[0]) or cell_gaussian_snr[0] < 1.0: # These values indicate nothing present in the cell perform_bg_removal = False # So we can skip this and save processing time # Perform background removal if perform_bg_removal: bg_removed_img = rb_gaussian_bg_removal_with_edge_enhancement(pre_processed_img, ball_radius, cell_mask) else: bg_removed_img = np.zeros_like(original_img) # Check for contrast after bg removal contrast_flag = check_contrast_func(bg_removed_img) if contrast_flag: napari_show_info(f"Cell {cell_label} has low contrast, likely has no puncta...") # If there is no contrast, return empty masks and skip the segmentation for faster processing puncta_mask = np.zeros_like(cell_mask) refined_puncta_mask = np.zeros_like(cell_mask) else: # Call segmentation_func with the appropriate arguments puncta_mask = fz_segmentation_and_binarization(bg_removed_img, cell_mask, ball_radius) # Refine the puncta mask using custom filtering criteria, primarily based on local intensity aroudn the 'object' refined_puncta_mask = puncta_refinement_func(original_img, pre_processed_img, puncta_mask, cell_mask, min_spot_radius=2) return refined_puncta_mask, puncta_mask
[docs] def run_segment_subcellular_objects(pre_processed_image_layer, original_image_layer, data_instance, viewer): """ Orchestrates the segmentation and refinement of subcellular objects across all cells in an image. It utilizes the napari viewer for visualization and operates on pre-processed and original images to detect and refine objects such as puncta within cell masks. Parameters ---------- pre_processed_image_layer : napari.layers.Image The pre-processed image layer, ready for segmentation. original_image_layer : napari.layers.Image The original image layer before any processing. data_instance : object An instance containing a data repository with necessary parameters such as ball_radius. viewer : napari.Viewer The napari viewer instance used for adding the segmentation results as new layers. Raises ------ Warning If the 'Labeled Cell Mask' layer is not present in the viewer, a warning is issued as the function is intended to be used after running the Cell Analyzer. If the layer is absent, the function will run on the entire image, which may lead to unintended behavior. Notes ----- The function iterates over all cells, segments subcellular objects, and refines the segmentation. It relies on cell-specific metrics if available. The final segmentation masks for all cells are combined and added to the napari viewer as new layers for visualization and further analysis. """ # Retrieve the data from the image layers and data instance original_image = original_image_layer.data pre_processed_image = pre_processed_image_layer.data ball_radius = data_instance.data_repository['ball_radius'] # Labeled Cell Mask is created by the cell analyzer, if it is not in the viewer the function # will run on the entire image, however this is not the desired behavior hence we warn the user if 'Labeled Cell Mask' in viewer.layers: cell_masks = viewer.layers['Labeled Cell Mask'].data # Get the labeled cell mask cell_df = data_instance.get_data('cell_df', pd.DataFrame()) # Get the cell_df if it is available CMS_img = cell_mask_stretching(pre_processed_image, cell_masks) # Apply per cell contrast enhancement else: cell_masks = np.ones_like(original_image).astype(int) # Create a dummy cell mask to run on the entire image cell_masks[0:2, 0:2] = 0 # Ensure there are 2 labels for the cell mask cell_df = pd.DataFrame() # Create cell_df since we assume it is not available because it is created by the cell analyzer too CMS_img = pre_processed_image.copy() # We cannot do per cell contrast enhancement without the cell masks napari_show_warning("Warning: This function is intended to be used after running Cell Analyzer.\n" "Ignore this warning if you intend on segmenting the entire image.\n" "Note that this may cause unintended behavior." ) # Get the number of cells in cell_masks unique_labels = np.unique(cell_masks) unique_labels = unique_labels[1:] # Initialize total masks to store the combined results total_puncta_mask = np.zeros_like(cell_masks, dtype=bool) total_refined_puncta_mask = np.zeros_like(cell_masks, dtype=bool) # Iterate over all cell labels, segment, and refine puncta within each cell for label in unique_labels: contrast_stretched_img = CMS_img.copy() original_img = original_image.copy() napari_show_info(f"Processing cell... {label} of {len(unique_labels)}") # Create a binary mask for the current cell cell_mask_holder = np.zeros_like(cell_masks) cell_mask_holder[cell_masks==label] = 1 cell_mask_holder = cell_mask_holder.astype(bool) # Segment and refine puncta within the cell refined_puncta_mask, puncta_mask = segment_subcellular_objects(original_img, contrast_stretched_img, cell_mask_holder, label, ball_radius, cell_df) # Add the segmented mask to the total mask total_puncta_mask += puncta_mask total_refined_puncta_mask += refined_puncta_mask viewer.add_labels(total_puncta_mask.astype(int), name=f"Total Puncta Mask") viewer.add_labels(total_refined_puncta_mask.astype(int), name=f"Total Refined Puncta Mask")