Source code for cBrainMRIPrePro.utils.image_processing

# -*- coding: utf-8 -*-
"""
| Author: Alexandre CARRE 
| Created on: Nov 23, 2020
"""
import logging
from typing import Tuple, Union, Optional

import numpy as np
from numba import njit, prange
from skimage.filters import threshold_otsu
from skimage.morphology import remove_small_objects

logger = logging.getLogger(__name__)
logging.getLogger('numba').setLevel(logging.WARNING)


def _handle_zeros_in_scale(scale, copy=True):
    """
    Makes sure that whenever scale is zero, we handle it correctly.
    This happens in most scalers when we have constant features.
    """

    # if we are fitting on 1D arrays, scale might be a scalar
    if np.isscalar(scale):
        if scale == .0:
            scale = 1.
        return scale
    elif isinstance(scale, np.ndarray):
        if copy:
            # New array to avoid side-effects
            scale = scale.copy()
        scale[scale == 0.0] = 1.0
        return scale


[docs]@njit(parallel=True) def fill_mask(mask_arr: np.ndarray) -> np.ndarray: """ Fill a 3D mask array. Useful when use a threshold function and need to fill hole :param mask_arr: mask to fill :return: mask filled """ assert mask_arr.ndim == 3, "Mask to fill need to be a 3d array" for z in prange(0, mask_arr.shape[0]): # we use np convention -> z,y,x for x in prange(0, mask_arr.shape[2]): if np.max(mask_arr[z, :, x]) == 1: a0 = mask_arr.shape[1] - 1 b0 = 0 while mask_arr[z, a0, x] == 0: if a0 != 0: a0 = a0 - 1 # Top of the data. Above it is zero. while mask_arr[z, b0, x] == 0: if b0 != mask_arr.shape[1] - 1: b0 = b0 + 1 # Bottom of the data. Below it is zero. for k in prange(b0, a0 + 1): mask_arr[z, k, x] = 1 for y in prange(0, mask_arr.shape[1]): if np.max(mask_arr[z, y, :]) == 1: c0 = mask_arr.shape[2] - 1 d0 = 0 while mask_arr[z, y, c0] == 0: if c0 != 0: c0 = c0 - 1 # Top of the data. Above it is zero. while mask_arr[z, y, d0] == 0: if d0 != mask_arr.shape[1] - 1: d0 = d0 + 1 # Bottom of the data. Below it is zero. for j in prange(d0, c0 + 1): mask_arr[z, y, j] = 1 return mask_arr
[docs]def get_mask(input_array: np.ndarray) -> np.ndarray: """ Get a (head) mask. Based on Otsu threshold and noise reduced. Then result mask is holes filled. :param input_array: input image array :return: binary head mask """ thresh = threshold_otsu(input_array) otsu_mask = input_array > thresh noise_reduced = remove_small_objects(otsu_mask, 10, ) head_mask = fill_mask(noise_reduced.astype(np.uint8)) return head_mask
[docs]def min_max_scaling(input_array: np.ndarray, scaling_range: Tuple[int, int] = (10, 100)) \ -> Tuple[np.ndarray, float, float]: """ Transform image input pixels/voxels to a given range. (type min-max scaler) :param input_array: input image array :param scaling_range: min, max = feature_range :return: image_scaled, min, scale """ image_scaled = np.copy(input_array).astype(np.float32) scale_ = ((scaling_range[1] - scaling_range[0]) / _handle_zeros_in_scale( np.max(input_array) - np.min(input_array))).astype(np.float32) min_ = scaling_range[0] - np.min(input_array) * scale_ image_scaled *= scale_ image_scaled += min_ return image_scaled, min_, scale_
[docs]def invert_min_max_scaling(input_array_scaled: np.ndarray, scale_: float, min_: float) -> np.ndarray: """ Invert min max scaling :param input_array_scaled: input image array scaled :param scale_: Per pixels/voxels relative scaling of the data. :param min_: Per pixels/voxels minimum seen in the data :return: input image array unscaled """ input_array_scaled = np.copy(input_array_scaled).astype(np.float32) input_array_scaled -= min_ input_array_scaled /= scale_ return input_array_scaled
[docs]def zscore_normalize(input_array: Union[np.ndarray, str], scaling_factor: int = 1, mask: Optional[Union[np.ndarray, str]] = None) -> np.ndarray: """ Function to normalize array with Z-Score. Normalize a target image by subtracting the mean of the whole brain and dividing by the standard deviation. :param input_array: input array image to normalize :param scaling_factor: scaling factor to apply to normalization :param mask: input mask where to apply the normalization. If not provided default will be in non zero value :return: input array normalize """ if mask is None: mask = input_array > 0 logical_mask = mask == 1 # force the mask to be logical type mean = input_array[logical_mask].mean() std = input_array[logical_mask].std() normalized_input_array = (((input_array - mean) / std) * scaling_factor) * logical_mask return normalized_input_array