import logging import numpy as np from typing import Tuple logger = logging.getLogger(__name__) def _pad_if_necessary( image: np.ndarray, target_shape: Tuple[int, int, int] ) -> np.ndarray: pad = [(0, 0)] * 3 for dim in range(3): if image.shape[dim] < target_shape[dim]: padding = target_shape[dim] - image.shape[dim] first_padding = padding // 2 second_padding = padding - first_padding pad[dim] = (first_padding, second_padding) return np.pad(image, tuple(pad), mode='constant', constant_values=0) def _crop_if_necessary( image: np.ndarray, target_shape: Tuple[int, int, int] ) -> np.ndarray: nonzero = np.where(image != 0) idx = [slice(None)] * 3 for dim in np.arange(3): if image.shape[dim] > target_shape[dim]: extrafluous = target_shape[dim] / 2 center = np.round(np.mean([ np.amin(nonzero[dim]), np.amax(nonzero[dim]) ])) min_idx = int(center - extrafluous) max_idx = int(center + extrafluous) if min_idx < 0: max_idx -= min_idx min_idx = 0 if max_idx > image.shape[dim]: diff = max_idx - image.shape[dim] min_idx -= diff max_idx -= diff idx[dim] = slice(min_idx, max_idx) image = image[tuple(idx)] return image def _center_crop_or_pad( image: np.ndarray, target_shape: Tuple[int, int, int] ) -> np.ndarray: image = _pad_if_necessary(image, target_shape) image = _crop_if_necessary(image, target_shape) return image def conform( image: np.ndarray, relative_normalization: bool = False ) -> np.ndarray: """Conforms an image to the expected format if necessary. The expected format means an image of shape 224x192x224 with voxel values spanning the range [0, 1]. If the image has a redundant channel-dimension, this is removed. If the image is currently too large along any dimension, a "central" crop is made by determining the bound of the brain (e.g. non-zero voxels) and retaining equivalent padding on each side. If the image is currently too small along either axis, the image is zero-padded equally on each side. If the voxel-values does not fall within the expected range, they are normalized. If the relative_normalization-flag is set, the values are normalized by dividing by the image max, otherwise they are divided by 255. However, if the largest value is >255, this indicates that the image has not been processed with FastSurfer, and an error is raised. Parameters ---------- image : np.ndarray A three-dimensional or four-dimensional tensor containing raw voxel-values. relative_normalization : bool If set, the voxel values are normalized by dividing by the image max, otherwise they are divided by 255. Returns ------- np.ndarray The conformed image. """ logger.debug('Original image shape: %s', str(image.shape)) image = image.astype(np.float32) if len(image.shape) == 4: if image.shape[-1] != 1: raise ValueError(f'Unable to handle multi-channel images') image = image[...,0] if image.shape != (224, 192, 224): image = _center_crop_or_pad(image, (224, 192, 224)) logger.debug('Conformed image shape: %s', str(image.shape)) logger.debug( 'Original image voxel value range: %f-%f', np.amin(image), np.amax(image) ) if relative_normalization: image -= np.amin(image) image /= np.amax(image) image *= 255.0 logger.debug( 'Conformed image voxel value range: %f-%f', np.amin(image), np.amax(image) ) return image