Source code for neuro_morpho.model.tiler

"""Image tiling and stitching."""

import math

import numpy as np
from sklearn.mixture import GaussianMixture


[docs] class Tiler: """Image tiling and stitching. This class provides methods for tiling an image into smaller patches and stitching them back together. """ def __init__( self, tile_size: int = 512, tile_assembly: str = "nn", x_coords: np.ndarray | None = None, y_coords: np.ndarray | None = None, nearest_map: np.ndarray | None = None, ): """Initialize the Tiler. Args: tile_size (int, optional): The size of the tiles. Defaults to 512. tile_assembly (str, optional): The method for assembling the tiles. Can be 'nn' (nearest neighbor), 'mean', or 'max'. Defaults to "nn". x_coords (np.ndarray, optional): The x-coordinates of the tiles. Defaults to None. y_coords (np.ndarray, optional): The y-coordinates of the tiles. Defaults to None. nearest_map (np.ndarray, optional): The nearest neighbor map. Defaults to None. """ super().__init__() self.tile_size = tile_size self.tile_assembly = tile_assembly self.x_coords = x_coords if x_coords is not None else np.array([]) self.y_coords = y_coords if y_coords is not None else np.array([]) nearest_map = nearest_map if nearest_map is not None else None
[docs] def get_tiling_attributes(self, image_size): """Calculate the tiling attributes based on the image size. This method calculates the x and y coordinates of the tiles and the nearest neighbor map if the tile assembly method is 'nn'. Args: image_size (tuple[int, int]): The size of the image. """ n_x = math.ceil(image_size[1] / self.tile_size) self.x_coords = np.zeros(n_x, dtype=int) if n_x == 1: gap_x = 0 else: gap_x = math.floor((self.tile_size * n_x - image_size[1]) / (n_x - 1)) gap_x_plus_one__amount = self.tile_size * n_x - image_size[1] - gap_x * (n_x - 1) for i in range(1, n_x): if i <= gap_x_plus_one__amount: self.x_coords[i] = int(self.x_coords[i - 1] + self.tile_size - (gap_x + 1)) else: self.x_coords[i] = int(self.x_coords[i - 1] + self.tile_size - gap_x) n_y = math.ceil(image_size[0] / self.tile_size) self.y_coords = np.zeros(n_y, dtype=int) if n_y == 1: gap_y = 0 else: gap_y = math.floor((self.tile_size * n_y - image_size[0]) / (n_y - 1)) gap_y_plus_one__amount = self.tile_size * n_y - image_size[0] - gap_y * (n_y - 1) for i in range(1, n_y): if i <= gap_y_plus_one__amount: self.y_coords[i] = int(self.y_coords[i - 1] + self.tile_size - (gap_y + 1)) else: self.y_coords[i] = int(self.y_coords[i - 1] + self.tile_size - gap_y) self.nearest_map = None if self.tile_assembly == "nn": # prepare nearest neighbor map x_centers = np.tile(self.x_coords, n_y) + (self.tile_size - 1) / 2 y_centers = np.repeat(self.y_coords, n_x) + (self.tile_size - 1) / 2 y_grid, x_grid = np.meshgrid(np.arange(image_size[0]), np.arange(image_size[1]), indexing="ij") y_grid = y_grid[..., np.newaxis] x_grid = x_grid[..., np.newaxis] distances = np.sqrt((x_grid - x_centers) ** 2 + (y_grid - y_centers) ** 2) self.nearest_map = np.argmin(distances, axis=-1)
[docs] def extend_image_shape(self, orig_image: np.ndarray) -> tuple[np.ndarray, tuple[int, int]]: """Create image with extended size to fit the tile size. This method calculates models the distribution of noise/background as a gaussian and creates extended with pixels' greylevels distributed in the same manner. Args: orig_image (np.ndarray): Original image. """ height, width = orig_image.shape extended_height, extended_width = height, width start_y_coord, start_x_coord = 0, 0 if height < self.tile_size: extended_height = self.tile_size start_y_coord = (extended_height - height) // 2 if width < self.tile_size: extended_width = self.tile_size start_x_coord = (extended_width - width) // 2 extended_image = np.zeros((extended_height, extended_width), dtype=orig_image.dtype) # Implement Gaussian Mixture for Background Analysis: pixels = orig_image.flatten().reshape(-1, 1) # Reshape to (n_samples, 1) gmm = GaussianMixture(n_components=2, random_state=42) gmm.fit(pixels) means = gmm.means_.flatten() variances = gmm.covariances_.flatten() background_label = np.argmin(means) # Step 3: Generate Gaussian-distributed pixel values extended_image = np.random.normal( loc=means[background_label], scale=np.sqrt(variances[background_label]), size=(extended_height, extended_width), ) extended_image[start_y_coord : start_y_coord + height, start_x_coord : start_x_coord + width] = orig_image return extended_image, (start_y_coord, start_x_coord)
[docs] def tile_image(self, image: np.ndarray) -> np.ndarray: """Tile an image into smaller patches. Args: image (np.ndarray): The input image. Returns: np.ndarray: An array of tiles. """ y_bounds = [(y, y + self.tile_size) for y in self.y_coords] x_bounds = [(x, x + self.tile_size) for x in self.x_coords] tiles = [image[y0:y1, x0:x1] for y0, y1 in y_bounds for x0, x1 in x_bounds] return np.stack(tiles)