#!/usr/bin/env python # # resample.py - The resample functino # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module defines the :func:`resample` function, which can be used to resample an :class:`.Image` object to a different resolution. The :func:`resampleToPixdims` and :func:`resampleToReference` functions are convenience wrappers around :func:`resample`. The :func:`applySmoothing` function is a sub-function of :func:`resample`. """ import numpy as np import scipy.ndimage as ndimage import fsl.transform.affine as affine def resampleToPixdims(image, newPixdims, **kwargs): """Resample ``image`` so that it has the specified voxel dimensions. This is a wrapper around :func:`resample` - refer to its documenttion for details on the other arguments and the return values. :arg image: :class:`.Image` to resample :arg pixdims: New voxel dimensions to resample ``image`` to. """ newPixdims = np.array(newPixdims) oldShape = np.array(image.shape) oldPixdims = np.array(image.pixdim) newShape = oldShape * (oldPixdims / newPixdims) return resample(image, newShape, **kwargs) def resampleToReference(image, reference, matrix=None, **kwargs): """Resample ``image`` into the space of the ``reference``. This is a wrapper around :func:`resample` - refer to its documenttion for details on the other arguments and the return values. When resampling to a reference image, resampling will only be applied along the spatial (first three) dimensions. :arg image: :class:`.Image` to resample :arg reference: :class:`.Nifti` defining the space to resample ``image`` into :arg matrix: Optional world-to-world affine alignment matrix """ oldShape = list(image.shape) newShape = list(reference.shape[:3]) if matrix is None: matrix = np.eye(4) # If the input image is >3D, pad the # new shape so that we only resample # along the first 3 dimensions. if len(newShape) < len(oldShape): newShape = newShape + oldShape[len(newShape):] # Align the two images together # via their vox-to-world affines, # and the world-to-world affine # if provided matrix = affine.concat(image.worldToVoxMat, affine.invert(matrix), reference.voxToWorldMat) # If the input image is >3D, we # have to adjust the resampling # matrix to take into account the # additional dimensions (see scipy. # ndimage.affine_transform) if len(newShape) > 3: rotmat = matrix[:3, :3] offsets = matrix[:3, 3] matrix = np.eye(len(newShape) + 1) matrix[:3, :3] = rotmat matrix[:3, -1] = offsets kwargs['mode'] = kwargs.get('mode', 'constant') kwargs['newShape'] = newShape kwargs['matrix'] = matrix data = resample(image, **kwargs)[0] # The image is now in the same space # as the reference, so it inherits # ref's voxel-to-world affine return data, reference.voxToWorldMat def resample(image, newShape, sliceobj=None, dtype=None, order=1, smooth=True, origin=None, matrix=None, mode=None, cval=0): """Returns a copy of the data in the ``image``, resampled to the specified ``newShape``. The space that the image is resampled into can be defined in one of the following ways, in decreasing order of precedence: 1. If a ``matrix`` is provided, it is applied to the voxel coordinates when retrieving values from the ``image`` 2. Otherwise the image is simply scaled according to the ratio calculated by ``image.shape / newShape``. In this case the ``origin`` argument may be used to adjust the alignemnt of the original and resampled voxel grids. See the ``scipy.ndimage.affine_transform`` function for more details, particularly on the ``order``, ``matrix``, ``mode`` and ``cval`` arguments. .. note:: If a custom resampling ``matrix`` is specified, the adjusted voxel-to-world afffine cannot be calculated by this function, so ``None`` will be returned instead. :arg image: :class:`.Image` object to resample :arg newShape: Desired shape. May containg floating point values, in which case the resampled image will have shape ``round(newShape)``, but the voxel sizes will have scales ``self.shape / newShape`` (unless ``matrix`` is specified). :arg sliceobj: Slice into this ``Image``. If ``None``, the whole image is resampled, and it is assumed that it has the same number of dimensions as ``newShape``. A :exc:`ValueError` is raised if this is not the case. :arg dtype: ``numpy`` data type of the resampled data. If ``None``, the :meth:`dtype` of this ``Image`` is used. :arg order: Spline interpolation order, passed through to the ``scipy.ndimage.affine_transform`` function - ``0`` corresponds to nearest neighbour interpolation, ``1`` (the default) to linear interpolation, and ``3`` to cubic interpolation. :arg smooth: If ``True`` (the default), the data is smoothed before being resampled, but only along axes which are being down-sampled (i.e. where ``newShape[i] < self.shape[i]``). :arg origin: ``'centre'`` (the default) or ``'corner'``. ``'centre'`` resamples the image such that the centre of the corner voxels of this image and the resampled data are aligned. ``'corner'`` resamples the image such that the corner of the corner voxels are aligned (and therefore the voxel grids are aligned). Ignored if ``offset`` or ``matrix`` is specified. :arg matrix: Arbitrary affine transformation matrix to apply to the voxel coordinates of ``image`` when resampling. :arg mode: How to handle regions which are outside of the image FOV. Defaults to `''nearest'``. :arg cval: Constant value to use when ``mode='constant'``. :returns: A tuple containing: - A ``numpy`` array of shape ``newShape``, containing an interpolated copy of the data in this ``Image``. - A ``numpy`` array of shape ``(4, 4)``, containing the adjusted voxel-to-world transformation for the spatial dimensions of the resampled data, or ``None`` if a ``matrix`` was provided. """ if sliceobj is None: sliceobj = slice(None) if dtype is None: dtype = image.dtype if origin is None: origin = 'centre' if mode is None: mode = 'nearest' if origin == 'center': origin = 'centre' ownMatrix = matrix is None if origin not in ('centre', 'corner'): raise ValueError('Invalid value for origin: {}'.format(origin)) data = np.array(image[sliceobj], dtype=dtype, copy=False) if len(data.shape) != len(newShape): raise ValueError('Data dimensions do not match new shape: ' 'len({}) != len({})'.format(data.shape, newShape)) # If matrix not provided, calculate # a scaling/offset matrix from the # old/new shape ratio and the origin # setting. if matrix is None: matrix = affine.rescale(data.shape, newShape, origin) # same shape and identity matrix? the # image doesn't need to be resampled if np.all(np.isclose(data.shape, newShape)) and \ np.all(np.isclose(matrix, np.eye(len(newShape) + 1))): return data, image.voxToWorldMat newShape = np.array(np.round(newShape), dtype=np.int) # Apply smoothing if requested, # and if not using nn interp if order > 0 and smooth: data = applySmoothing(data, matrix, newShape) # Do the resample thing data = ndimage.affine_transform(data, matrix, output_shape=newShape, order=order, mode=mode, cval=cval) # Construct an affine transform which # puts the resampled image into the # same world coordinate system as this # image. We may be working with >3D data, # so here we discard the non-spatial # parts of the resampling matrix if matrix.shape != (4, 4): rotmat = matrix[:3, :3] offsets = matrix[:3, -1] matrix = np.eye(4) matrix[:3, :3] = rotmat matrix[:3, -1] = offsets # We can only adjust the v2w affine if # the input space and resampling space # are aligned (e.g. if we're just # resampling to different dimensions). # We can't assume this when a custom # matrix is specified, so we just give # up and return None. if ownMatrix: matrix = affine.concat(image.voxToWorldMat, matrix) else: matrix = None return data, matrix def applySmoothing(data, matrix, newShape): """Called by the :func:`resample` function. If interpolating and smoothing, we apply a gaussian filter along axes with a resampling ratio greater than 1.1. We do this so that interpolation has an effect when down-sampling to a resolution where the voxel centres are aligned (as otherwise any interpolation regime will be equivalent to nearest neighbour). This more-or-less mimics the behaviour of FLIRT. See the ``scipy.ndimage.gaussian_filter`` function for more details. :arg data: Data to be smoothed. :arg matrix: Affine matrix to be used during resampling. The voxel scaling factors are extracted from this. :arg newShape: Shape the data is to be resampled into. :returns: A smoothed copy of ``data``. """ ratio = affine.decompose(matrix[:3, :3])[0] if len(newShape) > 3: ratio = np.concatenate(( ratio, [float(o) / float(s) for o, s in zip(data.shape[3:], newShape[3:])])) sigma = np.array(ratio) sigma[ratio < 1.1] = 0 sigma[ratio >= 1.1] *= 0.425 return ndimage.gaussian_filter(data, sigma)