Source code for towbintools.foundation.binary_image

import cv2
import numpy as np
import scipy.ndimage
from scipy.spatial import distance


[docs] def find_endpoints( binary_image: np.ndarray, ) -> np.ndarray: """ Find the endpoints of a binary image using the Hit-Or-Miss morphology operation. Parameters: binary_image (np.ndarray): A binary image where the foreground is represented by 1s and the background is represented by 0s. Returns: np.ndarray: A binary image with the same dimensions as the input, where 1s represent the locations of the endpoints. """ # Define kernels for the Hit-Or-Miss morphology operation. kernel_0 = np.array(([-1, -1, -1], [-1, 1, -1], [-1, 1, -1]), dtype="int") kernel_1 = np.array(([-1, -1, -1], [-1, 1, -1], [1, -1, -1]), dtype="int") kernel_2 = np.array(([-1, -1, -1], [1, 1, -1], [-1, -1, -1]), dtype="int") kernel_3 = np.array(([1, -1, -1], [-1, 1, -1], [-1, -1, -1]), dtype="int") kernel_4 = np.array(([-1, 1, -1], [-1, 1, -1], [-1, -1, -1]), dtype="int") kernel_5 = np.array(([-1, -1, 1], [-1, 1, -1], [-1, -1, -1]), dtype="int") kernel_6 = np.array(([-1, -1, -1], [-1, 1, 1], [-1, -1, -1]), dtype="int") kernel_7 = np.array(([-1, -1, -1], [-1, 1, -1], [-1, -1, 1]), dtype="int") kernel_8 = np.array(([-1, -1, -1], [-1, 1, -1], [-1, 1, 1]), dtype="int") kernels = np.array( ( kernel_0, kernel_1, kernel_2, kernel_3, kernel_4, kernel_5, kernel_6, kernel_7, kernel_8, ) ) output_image = np.zeros(binary_image.shape) # Apply the Hit-Or-Miss morphology operation for all the kernels. for kernel in kernels: out = cv2.morphologyEx(binary_image, cv2.MORPH_HITMISS, kernel) output_image = output_image + out return output_image
[docs] def connect_endpoints( binary_image: np.ndarray, max_distance: int = 200, ) -> np.ndarray: """ Connect endpoints of a binary image if they are within a specified maximum distance. First identifies the endpoints in the binary image using a Hit-Or-Miss morphology operation. If there are at least two endpoints, calculates the pairwise distances between them. Endpoints that are within the specified maximum distance from each other are connected using a straight line. Parameters: binary_image (np.ndarray): A binary image where the foreground is represented by 1s and the background is represented by 0s. max_distance (int, optional): The maximum distance between two endpoints to consider connecting them. (default: 200) Returns: np.ndarray: A binary image with the same dimensions as the input, where endpoints within the maximum distance are connected by straight lines. """ # Find the endpoints of a binary image using the Hit-Or-Miss morphology operation. endpoints = find_endpoints(binary_image) if np.sum(endpoints) < 2: return binary_image endpoints_x, endpoints_y = np.where(endpoints) endpoints_coordinates = np.column_stack((endpoints_x, endpoints_y)) distances = distance.cdist(endpoints_coordinates, endpoints_coordinates) output_image = binary_image.copy() for i, distance_for_point in enumerate(distances): nonzero_distances = distance_for_point[np.nonzero(distance_for_point)] if nonzero_distances.size == 0: return output_image if np.min(nonzero_distances) < max_distance: mindist_index = np.where(distance_for_point == np.min(nonzero_distances))[ 0 ][0] start_point = ( endpoints_coordinates[i][1], endpoints_coordinates[i][0], ) end_point = ( endpoints_coordinates[mindist_index][1], endpoints_coordinates[mindist_index][0], ) cv2.line(output_image, start_point, end_point, 1, 1) return output_image
[docs] def fill_bright_holes( image: np.ndarray, mask: np.ndarray, scale: float, ) -> np.ndarray: """ Fill bright holes in an image based on a given mask and statistical properties of the background. Identifies holes in a provided mask and evaluates the brightness of these holes in the original image. Bright holes with a median brightness significantly greater than the background mean are filled. Parameters: image (np.ndarray): Grayscale input image. mask (np.ndarray): Binary mask with foreground equal to 1. scale (float): A scaling factor that defines how many standard deviations above the background mean a hole needs to be in order for it to be considered 'bright' and filled. Returns: np.ndarray: A binary mask with the same dimensions as the input, where bright holes have been filled. """ filled_mask = scipy.ndimage.binary_fill_holes(mask) holes = (filled_mask - mask).astype(np.uint8) # Early exit if no holes if not np.any(holes): return mask number_of_holes, holes_labels = cv2.connectedComponents(holes, connectivity=4) hole_medians = np.zeros(number_of_holes - 1) for label in range(1, number_of_holes): hole_pixels = image[holes_labels == label] hole_medians[label - 1] = np.median(hole_pixels) image_without_object = image * (1 - filled_mask) background_pixels = image_without_object[image_without_object > 0] if len(background_pixels) == 0: return mask background_thresholds = np.percentile(background_pixels, [10, 90]) background_mask = (background_pixels >= background_thresholds[0]) & ( background_pixels <= background_thresholds[1] ) background = background_pixels[background_mask] background_mean = np.mean(background) background_std = np.std(background) threshold = background_mean + scale * background_std # Fill holes that meet the brightness criterion for label, median in enumerate(hole_medians, start=1): if median > threshold: mask[holes_labels == label] = 1 return mask
[docs] def get_biggest_object( mask: np.ndarray, connectivity: int = 4, ) -> np.ndarray: """ Retrieve the largest connected object from a binary mask. Identifies connected components in the provided binary mask and returns a mask of the largest object. If no objects are identified, returns a zero mask of the same shape as the input. Parameters: mask (np.ndarray): Binary image mask where the objects are set to 1. connectivity (int, optional): Connectivity for connected components. (default: 4) Returns: np.ndarray: Binary mask of the biggest object. """ # Get the mask's connected components nb_labels, labels = cv2.connectedComponents(mask, connectivity=connectivity) if nb_labels >= 2: # Find the biggest object, ignoring the background biggest_object_label = np.argmax(np.bincount(labels.flatten())[1:]) + 1 biggest_object_mask = (labels == biggest_object_label).astype(np.uint8) else: biggest_object_label = 0 biggest_object_mask = np.zeros(mask.shape, dtype=np.uint8) return biggest_object_mask