Source code for exr.utils.utils

"""
the Utils module offers a variety of utility functions designed to facilitate image processing tasks in the ExR-Tools package.
"""
import os
import numpy as np
from pathlib import Path
from typing import Optional
from multiprocessing import Pool
from skimage.restoration import rolling_ball
from scipy.ndimage import white_tophat
from skimage.morphology import disk
from exr.utils.log import configure_logger

logger = configure_logger('ExR-Tools')


[docs] def chmod(path: Path) -> None: """ Sets permissions so that users and the owner can read, write and execute files at the given path. :param path: Path in which privileges should be granted. :type path: pathlib.Path """ if os.name != "nt": # Skip for Windows OS try: path.chmod(0o766) # octal notation for permissions except Exception as e: logger.error( f"Failed to change permissions for {path}. Error: {e}") raise
[docs] def subtract_background_rolling_ball(volume: np.ndarray, radius: int = 50, num_threads: Optional[int] = 40) -> np.ndarray: """ Performs background subtraction on a volume image using the rolling ball method. :param volume: The input volume image. :type volume: np.ndarray :param radius: The radius of the rolling ball used for background subtraction. Default is 50. :type radius: int, optional :param num_threads: The number of threads to use for the rolling ball operation. Default is 40. :type num_threads: int, optional :return: The volume image after background subtraction. :rtype: np.ndarray """ corrected_volume = np.empty_like(volume) logger.info(f"Rolling_ball background subtraction") try: for slice_index in range(volume.shape[0]): corrected_volume[slice_index] = volume[slice_index] - rolling_ball( volume[slice_index], radius=radius, num_threads=num_threads) return corrected_volume except Exception as e: logger.error(f"Error during rolling ball background subtraction: {e}") raise
[docs] def subtract_background_top_hat(volume: np.ndarray, radius: int = 50, use_gpu: Optional[bool] = True) -> np.ndarray: """ Performs top-hat background subtraction on a volume image. :param volume: The input volume image. :type volume: np.ndarray :param radius: The radius of the disk structuring element used for top-hat transformation. Default is 50. :type radius: int, optional :param use_gpu: If True, uses GPU for computation (requires cupy). Default is False. :type use_gpu: bool, optional :return: The volume image after background subtraction. :rtype: np.ndarray """ structuring_element = disk(radius) corrected_volume = np.empty_like(volume) logger.info(f"top-hat background subtraction") try: if use_gpu: from cupyx.scipy.ndimage import white_tophat import cupy as cp for i in range(volume.shape[0]): if use_gpu: corrected_volume[i] = cp.asnumpy( white_tophat( cp.asarray(volume[i]), structure=cp.asarray(structuring_element) ) ) else: corrected_volume[i] = white_tophat( volume[i], structure=structuring_element) return corrected_volume except Exception as e: logger.error(f"Error during top-hat background subtraction: {e}") raise
[docs] def calculate_volume_and_surface_area(labeled_image, label=1,spacing=[1,1,1]): r""" Calculates the volume and surface area of a specified object within a 3D labeled image. :param labeled_image: A 3D array where different objects are distinguished by unique integer labels. :type labeled_image: numpy.ndarray :param label: The specific label of the object for which the volume and surface area are to be calculated. :type label: int :returns: A tuple containing two elements - the volume and the surface area of the specified object. :rtype: tuple """ # Initialize volume and surface_area volume = 0 surface_area = 0 # Define the directions to check for neighboring voxels directions = [(0, 1, 0), (1, 0, 0), (0, -1, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)] # Iterate over the 3D array for z in range(labeled_image.shape[0]): for y in range(labeled_image.shape[1]): for x in range(labeled_image.shape[2]): # If this voxel is part of the object if labeled_image[z, y, x] == label: # Increment the volume volume += 1 # Check the neighbors to calculate surface area for dx, dy, dz in directions: neighbor_x = x + dx neighbor_y = y + dy neighbor_z = z + dz # Check if the neighbor coordinates are within bounds if 0 <= neighbor_x < labeled_image.shape[2] and \ 0 <= neighbor_y < labeled_image.shape[1] and \ 0 <= neighbor_z < labeled_image.shape[0]: # If the neighbor is not part of the object, it's a boundary face if labeled_image[neighbor_z, neighbor_y, neighbor_x] != label: surface_area += 1 else: # Edge of the image is also considered a boundary face surface_area += 1 z_dim , y_dim , x_dim = spacing physical_volume = volume * (z_dim * y_dim * x_dim) avg_face_area = (y_dim * x_dim + z_dim * y_dim + z_dim * x_dim) / 3 # Calculate the physical surface area physical_surface_area = surface_area * avg_face_area return volume, surface_area, physical_volume, physical_surface_area
# multiprocessing acclerated # def subtract_background_rolling_ball(volume_img, radius=50, num_threads=20): # def subtract_slice(args): # slice_index, volume_slice, radius, num_threads = args # logger.info(slice_index) # corrected_slice = volume_slice - rolling_ball(volume_slice, radius=radius, num_threads=num_threads) # return slice_index, corrected_slice # corrected_volume_img = np.empty_like(volume_img) # args = [(i, volume_img[i], radius, num_threads) for i in range(volume_img.shape[0])] # with Pool(processes=5) as pool: # results = pool.map(subtract_slice, args) # for slice_index, corrected_slice in sorted(results): # logger.info(slice_index) # corrected_volume_img[slice_index] = corrected_slice # return corrected_volume_img