Source code for src.cytodataframe.image

"""
Helper functions for working with images in the context of CytoDataFrames.
"""

import cv2
import numpy as np
import skimage
import skimage.io
import skimage.measure
from PIL import Image, ImageEnhance
from skimage import draw, exposure
from skimage.util import img_as_ubyte


[docs] def is_image_too_dark( image: Image.Image, pixel_brightness_threshold: float = 10.0 ) -> bool: """ Check if the image is too dark based on the mean brightness. By "too dark" we mean not as visible to the human eye. Args: image (Image): The input PIL Image. threshold (float): The brightness threshold below which the image is considered too dark. Returns: bool: True if the image is too dark, False otherwise. """ # Convert the image to a numpy array and then to grayscale img_array = np.array(image) gray_image = cv2.cvtColor(img_array, cv2.COLOR_RGBA2GRAY) # Calculate the mean brightness mean_brightness = np.mean(gray_image) return mean_brightness < pixel_brightness_threshold
[docs] def adjust_image_brightness(image: Image.Image) -> Image.Image: """ Adjust the brightness of an image using histogram equalization. Args: image (Image): The input PIL Image. Returns: Image: The brightness-adjusted PIL Image. """ # Convert the image to numpy array and then to grayscale img_array = np.array(image) gray_image = cv2.cvtColor(img_array, cv2.COLOR_RGBA2GRAY) # Apply histogram equalization to improve the contrast equalized_image = cv2.equalizeHist(gray_image) # Convert back to RGBA img_array[:, :, 0] = equalized_image # Update only the R channel img_array[:, :, 1] = equalized_image # Update only the G channel img_array[:, :, 2] = equalized_image # Update only the B channel # Convert back to PIL Image enhanced_image = Image.fromarray(img_array) # Slightly reduce the brightness enhancer = ImageEnhance.Brightness(enhanced_image) reduced_brightness_image = enhancer.enhance(0.7) return reduced_brightness_image
[docs] def draw_outline_on_image_from_outline( orig_image: np.ndarray, outline_image_path: str ) -> np.ndarray: """ Draws green outlines on an image based on a provided outline image and returns the combined result. Args: orig_image (np.ndarray): The original image on which the outlines will be drawn. It must be a grayscale or RGB image with shape `(H, W)` for grayscale or `(H, W, 3)` for RGB. outline_image_path (str): The file path to the outline image. This image will be used to determine the areas where the outlines will be drawn. It can be grayscale or RGB. Returns: np.ndarray: The original image with green outlines drawn on the non-black areas from the outline image. The result is returned as an RGB image with shape `(H, W, 3)`. """ # Load the outline image outline_image = skimage.io.imread(outline_image_path) # Resize if necessary if outline_image.shape[:2] != orig_image.shape[:2]: outline_image = skimage.transform.resize( outline_image, orig_image.shape[:2], preserve_range=True, anti_aliasing=True, ).astype(orig_image.dtype) # Create a mask for non-black areas (with threshold) threshold = 10 # Adjust as needed # Grayscale if outline_image.ndim == 2: # noqa: PLR2004 non_black_mask = outline_image > threshold else: # RGB/RGBA non_black_mask = np.any(outline_image[..., :3] > threshold, axis=-1) # Ensure the original image is RGB if orig_image.ndim == 2: # noqa: PLR2004 orig_image = np.stack([orig_image] * 3, axis=-1) elif orig_image.shape[-1] != 3: # noqa: PLR2004 raise ValueError("Original image must have 3 channels (RGB).") # Ensure uint8 data type if orig_image.dtype != np.uint8: orig_image = (orig_image * 255).astype(np.uint8) # Apply the green outline combined_image = orig_image.copy() combined_image[non_black_mask] = [0, 255, 0] # Green in uint8 return combined_image
[docs] def draw_outline_on_image_from_mask( orig_image: np.ndarray, mask_image_path: str ) -> np.ndarray: """ Draws green outlines on an image based on a binary mask and returns the combined result. Please note: masks are inherently challenging to use when working with multi-compartment datasets and may result in outlines that do not pertain to the precise compartment. For example, if an object mask overlaps with one or many other object masks the outlines may not differentiate between objects. Args: orig_image (np.ndarray): Image which a mask will be applied to. Must be a NumPy array. mask_image_path (str): Path to the binary mask image file. Returns: np.ndarray: The resulting image with the green outline applied. """ # Load the binary mask image mask_image = skimage.io.imread(mask_image_path) # Ensure the original image is RGB # Grayscale input if orig_image.ndim == 2: # noqa: PLR2004 orig_image = np.stack([orig_image] * 3, axis=-1) # Unsupported input elif orig_image.shape[-1] != 3: # noqa: PLR2004 raise ValueError("Original image must have 3 channels (RGB).") # Ensure the mask is 2D (binary) if mask_image.ndim > 2: # noqa: PLR2004 mask_image = mask_image[..., 0] # Take the first channel if multi-channel # Detect contours from the mask contours = skimage.measure.find_contours(mask_image, level=0.5) # Create an outline image with the same shape as the original image outline_image = np.zeros_like(orig_image) # Draw contours as green lines for contour in contours: rr, cc = draw.polygon_perimeter( np.round(contour[:, 0]).astype(int), np.round(contour[:, 1]).astype(int), shape=orig_image.shape[:2], ) # Assign green color to the outline in all three channels outline_image[rr, cc, :] = [0, 255, 0] # Combine the original image with the green outline combined_image = orig_image.copy() mask = np.any(outline_image > 0, axis=-1) # Non-zero pixels in the outline combined_image[mask] = outline_image[mask] return combined_image
[docs] def adjust_with_adaptive_histogram_equalization(image: np.ndarray) -> np.ndarray: """ Adaptive histogram equalization with additional smoothing to reduce graininess. Parameters: image (np.ndarray): The input image to be processed. Returns: np.ndarray: The processed image with enhanced contrast. """ # Adjust parameters dynamically kernel_size = ( max(image.shape[0] // 10, 1), # Ensure the kernel size is at least 1 max(image.shape[1] // 10, 1), # Ensure the kernel size is at least 1 ) clip_limit = 0.02 # Lower clip limit to suppress over-enhancement nbins = 512 # Increase bins for finer histogram granularity # Check if the image has an alpha channel (RGBA) # RGBA image if image.shape[-1] == 4: # noqa: PLR2004 rgb_np = image[:, :, :3] alpha_np = image[:, :, 3] equalized_rgb_np = np.zeros_like(rgb_np, dtype=np.float32) for channel in range(3): equalized_rgb_np[:, :, channel] = exposure.equalize_adapthist( rgb_np[:, :, channel], kernel_size=kernel_size, clip_limit=clip_limit, nbins=nbins, ) equalized_rgb_np = img_as_ubyte(equalized_rgb_np) final_image_np = np.dstack([equalized_rgb_np, alpha_np]) # Grayscale image elif len(image.shape) == 2: # noqa: PLR2004 final_image_np = exposure.equalize_adapthist( image, kernel_size=kernel_size, clip_limit=clip_limit, nbins=nbins, ) final_image_np = img_as_ubyte(final_image_np) # RGB image elif image.shape[-1] == 3: # noqa: PLR2004 equalized_rgb_np = np.zeros_like(image, dtype=np.float32) for channel in range(3): equalized_rgb_np[:, :, channel] = exposure.equalize_adapthist( image[:, :, channel], kernel_size=kernel_size, clip_limit=clip_limit, nbins=nbins, ) final_image_np = img_as_ubyte(equalized_rgb_np) else: raise ValueError( "Unsupported image format. Ensure the image is grayscale, RGB, or RGBA." ) return final_image_np