Source code for topostats.array_manipulation

"""Functions for manipulating numpy arrays."""

import logging

import numpy as np
import numpy.typing as npt

from topostats.logs.logs import LOGGER_NAME

LOGGER = logging.getLogger(LOGGER_NAME)


[docs] def re_crop_grain_image_and_mask_to_set_size_nm( filename: str, grain_number: int, grain_bbox: tuple[int, int, int, int], pixel_to_nm_scaling: float, full_image: npt.NDArray[np.float32], full_mask_tensor: npt.NDArray[np.bool_], target_size_nm: float, ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.bool_]]: """ Re-crop a grain image and mask to be a target size in nanometres. Parameters ---------- filename : str The name of the file being processed, used for logging. grain_number : int The number of the grain being processed, used for logging. grain_bbox : tuple[int, int, int, int] The bounding box of the grain in the form (min_row, min_col, max_row (exclusive), max_col (exclusive)). pixel_to_nm_scaling : float Pixel to nanometre scaling factor. full_image : npt.NDArray[np.float32] The full image from which to crop the grain image. full_mask_tensor : npt.NDArray[np.bool_] The full mask tensor from which to crop the mask. target_size_nm : float The target size in nanometres to crop the grain image and mask to. Returns ------- tuple[npt.NDArray[np.float32], npt.NDArray[np.bool_]] The cropped grain image and mask, both as numpy arrays. Raises ------ ValueError If the target size in nanometres is larger than the full image or mask dimensions. """ # Re-slice the image to get a larger or smaller crop depending on the grain size. target_size_px = int(target_size_nm / pixel_to_nm_scaling) # To create a bbox that is the right size, we can create a small bbox and then pad it. # - find the centroid of the grain bbox # - determine if the target bbox is going to be odd or even centred. # - If odd centred, we can pad the centre pixel(1x1) bbox by target_size_nm // 2 in each direction # - If even centred, we can pad the centre pixels(2x2) bbox by target_size_nm // 2 - 1 in each direction. if target_size_px % 2 == 0: # Even proposed size, so take the centre 2x2 pixels and pad by half the size minus 1 # Get centre 2x2 pixel bbox of grain crop grain_crop_bbox_centre = ( (grain_bbox[0] + grain_bbox[2]) // 2 - 1, (grain_bbox[1] + grain_bbox[3]) // 2 - 1, (grain_bbox[0] + grain_bbox[2]) // 2 + 1, (grain_bbox[1] + grain_bbox[3]) // 2 + 1, ) to_pad = target_size_px // 2 - 1 else: # Odd proposed size, so take the centre 1x1 pixel and pad by half the size # Get centre 1x1 pixel bbox of grain crop grain_crop_bbox_centre = ( (grain_bbox[0] + grain_bbox[2]) // 2, (grain_bbox[1] + grain_bbox[3]) // 2, (grain_bbox[0] + grain_bbox[2]) // 2 + 1, (grain_bbox[1] + grain_bbox[3]) // 2 + 1, ) to_pad = target_size_px // 2 # Pad the bbox to the desired size try: grain_crop_bbox_resized = pad_bounding_box_dynamically_at_limits( bbox=grain_crop_bbox_centre, limits=(0, 0, full_image.shape[0], full_image.shape[1]), padding=to_pad, ) except ValueError as e: if "Proposed size" in str(e): raise ValueError( f"[{filename}] : Grain {grain_number} crop cannot be re-cropped at size {target_size_nm} nm " f"({target_size_px} px) " ) from e # If the error is not about the proposed size, re-raise it raise e # Crop the image and mask to the new bbox crop_image = full_image[ grain_crop_bbox_resized[0] : grain_crop_bbox_resized[2], grain_crop_bbox_resized[1] : grain_crop_bbox_resized[3], ] crop_mask = full_mask_tensor[ grain_crop_bbox_resized[0] : grain_crop_bbox_resized[2], grain_crop_bbox_resized[1] : grain_crop_bbox_resized[3], :, ] return crop_image, crop_mask
[docs] def pad_bounding_box_dynamically_at_limits( bbox: tuple[int, int, int, int], limits: tuple[int, int, int, int], padding: int, ) -> tuple[int, int, int, int]: """ Pad a bounding box within limits. If the padding would exceed the limits bounds, pad in the other direction. Parameters ---------- bbox : tuple[int, int, int, int] The bounding box to pad. limits : tuple[int, int, int, int] The region to limit the bounding box to in the form (min_row, min_col, max_row, max_col). padding : int The padding to apply to the bounding box. Returns ------- tuple[int, int, int, int] The new bounding box indices. """ # check that the padded size is smaller than the limits bbox_height = bbox[2] - bbox[0] bbox_width = bbox[3] - bbox[1] proposed_height = bbox_height + 2 * padding proposed_width = bbox_width + 2 * padding limits_height = limits[2] - limits[0] limits_width = limits[3] - limits[1] if proposed_height > limits_height or proposed_width > limits_width: raise ValueError( f"Proposed size {proposed_height}x{proposed_width} px = ({bbox_width}x{bbox_height}) + " f"({2*padding}x{2*padding}) px is larger than limits size " f"({limits_height}x{limits_width}) px. Cannot pad bounding box beyond limits." ) pad_up_amount = padding pad_down_amount = padding pad_left_amount = padding pad_right_amount = padding # try padding up, check if hit the top of the limits if bbox[0] - padding < limits[0]: # if so, restrict up padding to the limits and add the remaining padding to the down padding pad_up_amount = bbox[0] - limits[0] # Can safely assume can increase down padding since we checked earlier that the proposed size is smaller than # limits pad_down_amount += padding - pad_up_amount # try padding down, check if hit the bottom of the limits elif bbox[2] + padding > limits[2]: # if so, restrict down padding to the limits and add the remaining padding to the up padding pad_down_amount = limits[2] - bbox[2] # Can safely assume can increase up padding since we checked earlier that the proposed size is smaller than # limits pad_up_amount += padding - pad_down_amount # try padding left, check if hit the left of the limits if bbox[1] - padding < limits[1]: # if so, restrict left padding to the limits and add the remaining padding to the right padding pad_left_amount = bbox[1] - limits[1] # Can safely assume can increase right padding since we checked earlier that the proposed size is smaller than # limits pad_right_amount += padding - pad_left_amount # try padding right, check if hit the right of the limits elif bbox[3] + padding > limits[3]: # if so, restrict right padding to the limits and add the remaining padding to the left padding pad_right_amount = limits[3] - bbox[3] # Can safely assume can increase left padding since we checked earlier that the proposed size is smaller than # limits pad_left_amount += padding - pad_right_amount # Return the new bounding box indices return ( bbox[0] - pad_up_amount, bbox[1] - pad_left_amount, bbox[2] + pad_down_amount, bbox[3] + pad_right_amount, )