From b897935b928c5a5232566ccae0dbf7112921a8cb Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 11 Dec 2019 09:24:47 -0500 Subject: [PATCH 01/27] add `conform` and `_transform_range` funcs --- nibabel/processing.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index cf9f60c76c..b48efa9c33 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -23,8 +23,9 @@ spnd, _, _ = optional_package('scipy.ndimage') from .affines import AffineError, to_matvec, from_matvec, append_diag +from .funcs import as_closest_canonical from .spaces import vox2out_vox -from .nifti1 import Nifti1Image +from .nifti1 import Nifti1Header, Nifti1Image from .imageclasses import spatial_axes_first SIGMA2FWHM = np.sqrt(8 * np.log(2)) @@ -311,3 +312,31 @@ def smooth_image(img, mode=mode, cval=cval) return out_class(sm_data, img.affine, img.header) + + +def _transform_range(x, new_min, new_max): + x = np.asarray(x) + x_min, x_max = x.min(), x.max() + return (((x - x_min) * (new_max - new_min)) / (x_max - x_min)) + new_min + + +def conform(from_img, out_shape=(256, 256, 256), + voxel_size=(1.0, 1.0, 1.0), order=3, cval=0.0, out_class=Nifti1Image): + # Create fake image of the image we want to resample to. + hdr = Nifti1Header() + hdr.set_data_shape(out_shape) + hdr.set_zooms(voxel_size) + dst_aff = hdr.get_best_affine() + to_img = Nifti1Image(np.empty(out_shape), affine=dst_aff, header=hdr) + # Resample input image. + out_img = resample_from_to( + from_img=from_img, to_vox_map=to_img, order=order, mode="constant", cval=cval, out_class=out_class) + # Cast to uint8. + data = out_img.get_fdata() + data = _transform_range(data, new_min=0.0, new_max=255.0) + data = data.round(out=data).astype(np.uint8) + out_img._dataobj = data + out_img.set_data_dtype(data.dtype) + # Reorient to RAS. + out_img = as_closest_canonical(out_img) + return out_img From 9895eb09f78275b762b8a046b02a71e668e02c7c Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Thu, 12 Dec 2019 10:22:41 -0500 Subject: [PATCH 02/27] add documentation and fix style --- nibabel/processing.py | 68 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 546b719c39..53342b70ba 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -314,13 +314,74 @@ def smooth_image(img, def _transform_range(x, new_min, new_max): + """Transform data to a new range, while maintaining ratios. + + Parameters + ---------- + x : array-like + The data to transform. + new_min, new_max : scalar + The minimum and maximum of the output array. + + Returns + ------- + transformed : array-like + A copy of the transformed data. + + Examples + -------- + >>> _transform_range([2, 4, 6], -1, 1) + array([-1., 0., 1.]) + """ x = np.asarray(x) x_min, x_max = x.min(), x.max() return (((x - x_min) * (new_max - new_min)) / (x_max - x_min)) + new_min -def conform(from_img, out_shape=(256, 256, 256), - voxel_size=(1.0, 1.0, 1.0), order=3, cval=0.0, out_class=Nifti1Image): +def conform(from_img, + out_shape=(256, 256, 256), + voxel_size=(1.0, 1.0, 1.0), + order=3, + cval=0.0, + out_class=Nifti1Image): + """ Resample image to ``out_shape`` with voxels of size ``voxel_size``. + + Using the default arguments, this function is meant to replicate most parts + of FreeSurfer's ``mri_convert --conform`` command. The output image is also + reoriented to RAS. + + Parameters + ---------- + from_img : object + Object having attributes ``dataobj``, ``affine``, ``header`` and + ``shape``. If `out_class` is not None, ``img.__class__`` should be able + to construct an image from data, affine and header. + out_shape : sequence, optional + The shape of the output volume. Default is (256, 256, 256). + voxel_size : sequence, optional + The size in millimeters of the voxels in the resampled output. Default + is 1mm isotropic. + order : int, optional + The order of the spline interpolation, default is 3. The order has to + be in the range 0-5 (see ``scipy.ndimage.affine_transform``) + mode : str, optional + Points outside the boundaries of the input are filled according + to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). + Default is 'constant' (see ``scipy.ndimage.affine_transform``) + cval : scalar, optional + Value used for points outside the boundaries of the input if + ``mode='constant'``. Default is 0.0 (see + ``scipy.ndimage.affine_transform``) + out_class : None or SpatialImage class, optional + Class of output image. If None, use ``from_img.__class__``. + + Returns + ------- + out_img : object + Image of instance specified by `out_class`, containing data output from + resampling `from_img` into axes aligned to the output space of + ``from_img.affine`` + """ # Create fake image of the image we want to resample to. hdr = Nifti1Header() hdr.set_data_shape(out_shape) @@ -329,7 +390,8 @@ def conform(from_img, out_shape=(256, 256, 256), to_img = Nifti1Image(np.empty(out_shape), affine=dst_aff, header=hdr) # Resample input image. out_img = resample_from_to( - from_img=from_img, to_vox_map=to_img, order=order, mode="constant", cval=cval, out_class=out_class) + from_img=from_img, to_vox_map=to_img, order=order, mode="constant", + cval=cval, out_class=out_class) # Cast to uint8. data = out_img.get_fdata() data = _transform_range(data, new_min=0.0, new_max=255.0) From 49e4adaa3c7fa2a28cd405447960b53fc5eaf369 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Thu, 12 Dec 2019 23:10:33 -0500 Subject: [PATCH 03/27] clean up documentation + only conform 3D --- nibabel/processing.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 53342b70ba..ad7ccf43f7 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -314,7 +314,7 @@ def smooth_image(img, def _transform_range(x, new_min, new_max): - """Transform data to a new range, while maintaining ratios. + """ Transform data to a new range, while maintaining ratios. Parameters ---------- @@ -335,7 +335,7 @@ def _transform_range(x, new_min, new_max): """ x = np.asarray(x) x_min, x_max = x.min(), x.max() - return (((x - x_min) * (new_max - new_min)) / (x_max - x_min)) + new_min + return (x - x_min) * (new_max - new_min) / (x_max - x_min) + new_min def conform(from_img, @@ -347,8 +347,13 @@ def conform(from_img, """ Resample image to ``out_shape`` with voxels of size ``voxel_size``. Using the default arguments, this function is meant to replicate most parts - of FreeSurfer's ``mri_convert --conform`` command. The output image is also - reoriented to RAS. + of FreeSurfer's ``mri_convert --conform`` command. Specifically, this + function: + - Resamples data to ``output_shape`` + - Resamples voxel sizes to ``voxel_size`` + - Transforms data to range [0, 255] (while maintaining ratios) + - Casts to unsigned eight-bit integer + - Reorients to RAS (``mri_convert --conform`` reorients to LIA) Parameters ---------- @@ -364,10 +369,6 @@ def conform(from_img, order : int, optional The order of the spline interpolation, default is 3. The order has to be in the range 0-5 (see ``scipy.ndimage.affine_transform``) - mode : str, optional - Points outside the boundaries of the input are filled according - to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). - Default is 'constant' (see ``scipy.ndimage.affine_transform``) cval : scalar, optional Value used for points outside the boundaries of the input if ``mode='constant'``. Default is 0.0 (see @@ -382,6 +383,8 @@ def conform(from_img, resampling `from_img` into axes aligned to the output space of ``from_img.affine`` """ + if from_img.ndim != 3: + raise ValueError("Only 3D images are supported.") # Create fake image of the image we want to resample to. hdr = Nifti1Header() hdr.set_data_shape(out_shape) From 57c36481b47f9355a86f8c2963030535faff896f Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Thu, 12 Dec 2019 23:11:13 -0500 Subject: [PATCH 04/27] add tests for `conform` and `_transform_range` --- nibabel/tests/test_processing.py | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 0e1dbb83c7..98d4244095 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -19,10 +19,11 @@ import nibabel as nib from nibabel.processing import (sigma2fwhm, fwhm2sigma, adapt_affine, - resample_from_to, resample_to_output, smooth_image) + resample_from_to, resample_to_output, smooth_image, + _transform_range, conform) from nibabel.nifti1 import Nifti1Image from nibabel.nifti2 import Nifti2Image -from nibabel.orientations import flip_axis, inv_ornt_aff +from nibabel.orientations import aff2axcodes, flip_axis, inv_ornt_aff from nibabel.affines import (AffineError, from_matvec, to_matvec, apply_affine, voxel_sizes) from nibabel.eulerangles import euler2mat @@ -426,3 +427,32 @@ def test_against_spm_resample(): moved2output = resample_to_output(moved_anat, 4, order=1, cval=np.nan) spm2output = nib.load(pjoin(DATA_DIR, 'reoriented_anat_moved.nii')) assert_spm_resampling_close(moved_anat, moved2output, spm2output); + + +def test__transform_range(): + assert_array_equal(_transform_range([2, 4, 6], -1, 1), [-1, 0, 1]) + assert_array_equal(_transform_range([-1, 0, 1], 2, 6), [2, 4, 6]) + assert_array_equal(_transform_range(np.arange(11), 0, 5), + np.arange(0, 5.5, 0.5)) + assert_array_equal(_transform_range(np.arange(-100, 101), 0, 200), + np.arange(201)) + + +def test_conform(): + anat = nib.load(pjoin(DATA_DIR, 'anatomical.nii')) + + c = conform(anat) + assert c.shape == (256, 256, 256) + assert c.header.get_zooms() == (1, 1, 1) + assert c.dataobj.dtype == np.dtype(np.uint8) + assert aff2axcodes(c.affine) == ('R', 'A', 'S') + + c = conform(anat, out_shape=(100, 100, 200), voxel_size=(2, 2, 1.5)) + assert c.shape == (100, 100, 200) + assert c.header.get_zooms() == (2, 2, 1.5) + assert c.dataobj.dtype == np.dtype(np.uint8) + assert aff2axcodes(c.affine) == ('R', 'A', 'S') + + # Error on non-3D images. + func = nib.load(pjoin(DATA_DIR, 'functional.nii')) + assert_raises(ValueError, conform, func) From a681bddadb777bf7f37e65f28a944c8c0a728120 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Thu, 12 Dec 2019 23:27:20 -0500 Subject: [PATCH 05/27] only test `conform` if scipy installed --- nibabel/tests/test_processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 98d4244095..8e4b79b21d 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -438,6 +438,7 @@ def test__transform_range(): np.arange(201)) +@needs_scipy def test_conform(): anat = nib.load(pjoin(DATA_DIR, 'anatomical.nii')) From 4e62b7cb1a2341f58fd5a68aeef962d206a1540e Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 20:24:10 -0400 Subject: [PATCH 06/27] add `nib-conform` console script --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 069d93d007..423f61c2d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,6 +67,7 @@ all = [options.entry_points] console_scripts = + nib-conform=nibabel.cmdline.conform:main nib-ls=nibabel.cmdline.ls:main nib-dicomfs=nibabel.cmdline.dicomfs:main nib-diff=nibabel.cmdline.diff:main From e19b022c057465e600c123251fba89ba0082c158 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 22:19:32 -0400 Subject: [PATCH 07/27] tighten scope of conform function --- nibabel/processing.py | 60 ++++++++++++-------------------- nibabel/tests/test_processing.py | 28 +++++++-------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index ad7ccf43f7..35e020029a 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -25,6 +25,7 @@ from .funcs import as_closest_canonical from .spaces import vox2out_vox from .nifti1 import Nifti1Header, Nifti1Image +from .orientations import axcodes2ornt from .imageclasses import spatial_axes_first SIGMA2FWHM = np.sqrt(8 * np.log(2)) @@ -313,36 +314,12 @@ def smooth_image(img, return out_class(sm_data, img.affine, img.header) -def _transform_range(x, new_min, new_max): - """ Transform data to a new range, while maintaining ratios. - - Parameters - ---------- - x : array-like - The data to transform. - new_min, new_max : scalar - The minimum and maximum of the output array. - - Returns - ------- - transformed : array-like - A copy of the transformed data. - - Examples - -------- - >>> _transform_range([2, 4, 6], -1, 1) - array([-1., 0., 1.]) - """ - x = np.asarray(x) - x_min, x_max = x.min(), x.max() - return (x - x_min) * (new_max - new_min) / (x_max - x_min) + new_min - - def conform(from_img, out_shape=(256, 256, 256), voxel_size=(1.0, 1.0, 1.0), order=3, cval=0.0, + orientation='RAS', out_class=Nifti1Image): """ Resample image to ``out_shape`` with voxels of size ``voxel_size``. @@ -351,10 +328,12 @@ def conform(from_img, function: - Resamples data to ``output_shape`` - Resamples voxel sizes to ``voxel_size`` - - Transforms data to range [0, 255] (while maintaining ratios) - - Casts to unsigned eight-bit integer - Reorients to RAS (``mri_convert --conform`` reorients to LIA) + Unlike ``mri_convert --conform``, this command does not: + - Transform data to range [0, 255] + - Cast to unsigned eight-bit integer + Parameters ---------- from_img : object @@ -373,6 +352,8 @@ def conform(from_img, Value used for points outside the boundaries of the input if ``mode='constant'``. Default is 0.0 (see ``scipy.ndimage.affine_transform``) + orientation : str, optional + Orientation of output image. Default is "RAS". out_class : None or SpatialImage class, optional Class of output image. If None, use ``from_img.__class__``. @@ -383,24 +364,29 @@ def conform(from_img, resampling `from_img` into axes aligned to the output space of ``from_img.affine`` """ - if from_img.ndim != 3: + # Only support 3D images. This can be made more general in the future, once tests + # are written. + required_ndim = 3 + if from_img.ndim != required_ndim: raise ValueError("Only 3D images are supported.") + elif len(out_shape) != required_ndim: + raise ValueError("`out_shape` must have {} values".format(required_ndim)) + elif len(voxel_size) != required_ndim: + raise ValueError("`voxel_size` must have {} values".format(required_ndim)) + # Create fake image of the image we want to resample to. hdr = Nifti1Header() hdr.set_data_shape(out_shape) hdr.set_zooms(voxel_size) dst_aff = hdr.get_best_affine() to_img = Nifti1Image(np.empty(out_shape), affine=dst_aff, header=hdr) + # Resample input image. out_img = resample_from_to( from_img=from_img, to_vox_map=to_img, order=order, mode="constant", cval=cval, out_class=out_class) - # Cast to uint8. - data = out_img.get_fdata() - data = _transform_range(data, new_min=0.0, new_max=255.0) - data = data.round(out=data).astype(np.uint8) - out_img._dataobj = data - out_img.set_data_dtype(data.dtype) - # Reorient to RAS. - out_img = as_closest_canonical(out_img) - return out_img + + labels =list(zip('LPI', 'RAS')) + # list(zip('RPI', 'LAS') + # Reorient to desired orientation. + return out_img.as_reoriented(axcodes2ornt(orientation, labels=labels)) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 8e4b79b21d..58f3c928b0 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -20,7 +20,7 @@ import nibabel as nib from nibabel.processing import (sigma2fwhm, fwhm2sigma, adapt_affine, resample_from_to, resample_to_output, smooth_image, - _transform_range, conform) + conform) from nibabel.nifti1 import Nifti1Image from nibabel.nifti2 import Nifti2Image from nibabel.orientations import aff2axcodes, flip_axis, inv_ornt_aff @@ -429,30 +429,30 @@ def test_against_spm_resample(): assert_spm_resampling_close(moved_anat, moved2output, spm2output); -def test__transform_range(): - assert_array_equal(_transform_range([2, 4, 6], -1, 1), [-1, 0, 1]) - assert_array_equal(_transform_range([-1, 0, 1], 2, 6), [2, 4, 6]) - assert_array_equal(_transform_range(np.arange(11), 0, 5), - np.arange(0, 5.5, 0.5)) - assert_array_equal(_transform_range(np.arange(-100, 101), 0, 200), - np.arange(201)) - - @needs_scipy def test_conform(): anat = nib.load(pjoin(DATA_DIR, 'anatomical.nii')) + # Test with default arguments. c = conform(anat) assert c.shape == (256, 256, 256) assert c.header.get_zooms() == (1, 1, 1) - assert c.dataobj.dtype == np.dtype(np.uint8) + assert c.dataobj.dtype.type == anat.dataobj.dtype.type assert aff2axcodes(c.affine) == ('R', 'A', 'S') + assert isinstance(c, Nifti1Image) - c = conform(anat, out_shape=(100, 100, 200), voxel_size=(2, 2, 1.5)) + # Test with non-default arguments. + c = conform(anat, out_shape=(100, 100, 200), voxel_size=(2, 2, 1.5), + orientation="LPI", out_class=Nifti2Image) assert c.shape == (100, 100, 200) assert c.header.get_zooms() == (2, 2, 1.5) - assert c.dataobj.dtype == np.dtype(np.uint8) - assert aff2axcodes(c.affine) == ('R', 'A', 'S') + assert c.dataobj.dtype.type == anat.dataobj.dtype.type + assert aff2axcodes(c.affine) == ('L', 'P', 'I') + assert isinstance(c, Nifti2Image) + + # Error on non-3D arguments. + assert_raises(ValueError, conform, anat, out_shape=(100, 100)) + assert_raises(ValueError, conform, anat, voxel_size=(2, 2)) # Error on non-3D images. func = nib.load(pjoin(DATA_DIR, 'functional.nii')) From 89eedc5fee5e2c49a059cfe3d876e65abdfe6010 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 22:29:04 -0400 Subject: [PATCH 08/27] add `nib-conform` --- bin/nib-conform | 17 ++++++++++ nibabel/cmdline/conform.py | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 bin/nib-conform create mode 100644 nibabel/cmdline/conform.py diff --git a/bin/nib-conform b/bin/nib-conform new file mode 100644 index 0000000000..26ae5b5bf4 --- /dev/null +++ b/bin/nib-conform @@ -0,0 +1,17 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Conform a volume to a new shape and/or voxel size. +""" + +from nibabel.cmdline.conform import main + +if __name__ == '__main__': + main() diff --git a/nibabel/cmdline/conform.py b/nibabel/cmdline/conform.py new file mode 100644 index 0000000000..908c1752a9 --- /dev/null +++ b/nibabel/cmdline/conform.py @@ -0,0 +1,66 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" +Conform neuroimaging volume to arbitrary shape and voxel size. +""" + +import argparse +from pathlib import Path +import sys + +import numpy as np + +from nibabel import __version__ +from nibabel.loadsave import load +from nibabel.processing import conform + + +def _get_parser(): + """Return command-line argument parser.""" + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("infile", + help="Neuroimaging volume to conform.") + p.add_argument("outfile", + help="Name of output file.") + p.add_argument("--out-shape", nargs=3, default=(256, 256, 256), + help="Shape of the conformed output.") + p.add_argument("--voxel-size", nargs=3, default=(1, 1, 1), + help="Voxel size in millimeters of the conformed output.") + p.add_argument("--orientation", default="RAS", + help="Orientation of the conformed output.") + p.add_argument("-f", "--force", action="store_true", + help="Overwrite existing output files.") + p.add_argument("-V", "--version", action="version", version="{} {}".format(p.prog, __version__)) + + return p + + +def main(args=None): + """Main program function.""" + parser = _get_parser() + if args is None: + namespace = parser.parse_args(sys.argv[1:]) + else: + namespace = parser.parse_args(args) + + kwargs = vars(namespace) + from_img = load(kwargs["infile"]) + + if not kwargs["force"] and Path(kwargs["outfile"]).exists(): + raise FileExistsError("Output file exists: {}".format(kwargs["outfile"])) + + out_img = conform(from_img=from_img, + out_shape=kwargs["out_shape"], + voxel_size=kwargs["voxel_size"], + order=3, + cval=0.0, + orientation=kwargs["orientation"]) + + out_img.to_filename(kwargs["outfile"]) From 348f838dff2425b8dc568b5d5e3bc5c1eed11466 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 22:29:17 -0400 Subject: [PATCH 09/27] use proper labels for orientation --- nibabel/processing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 35e020029a..d38f2ac750 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -386,7 +386,6 @@ def conform(from_img, from_img=from_img, to_vox_map=to_img, order=order, mode="constant", cval=cval, out_class=out_class) - labels =list(zip('LPI', 'RAS')) - # list(zip('RPI', 'LAS') # Reorient to desired orientation. - return out_img.as_reoriented(axcodes2ornt(orientation, labels=labels)) + ornt = axcodes2ornt(orientation, labels=list(zip('RPI', 'LAS'))) + return out_img.as_reoriented(ornt) From 9491806a3e52d1743d41225c8be462f509d8475b Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 22:31:20 -0400 Subject: [PATCH 10/27] add non-3d tests --- nibabel/tests/test_processing.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 6d8e5c0dc8..0027975847 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -445,9 +445,12 @@ def test_conform(): assert isinstance(c, Nifti2Image) # Error on non-3D arguments. - assert_raises(ValueError, conform, anat, out_shape=(100, 100)) - assert_raises(ValueError, conform, anat, voxel_size=(2, 2)) + with pytest.raises(ValueError): + conform(anat, out_shape=(100, 100)) + with pytest.raises(ValueError): + conform(anat, voxel_size=(2, 2)) # Error on non-3D images. func = nib.load(pjoin(DATA_DIR, 'functional.nii')) - assert_raises(ValueError, conform, func) + with pytest.raises(ValueError): + conform(func) From a9ce73b56453cef56461993f360312c1bebd77b4 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 22:34:06 -0400 Subject: [PATCH 11/27] fix style --- nibabel/cmdline/conform.py | 5 ++--- nibabel/processing.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/nibabel/cmdline/conform.py b/nibabel/cmdline/conform.py index 908c1752a9..9b799599f3 100644 --- a/nibabel/cmdline/conform.py +++ b/nibabel/cmdline/conform.py @@ -15,8 +15,6 @@ from pathlib import Path import sys -import numpy as np - from nibabel import __version__ from nibabel.loadsave import load from nibabel.processing import conform @@ -56,7 +54,8 @@ def main(args=None): if not kwargs["force"] and Path(kwargs["outfile"]).exists(): raise FileExistsError("Output file exists: {}".format(kwargs["outfile"])) - out_img = conform(from_img=from_img, + out_img = conform( + from_img=from_img, out_shape=kwargs["out_shape"], voxel_size=kwargs["voxel_size"], order=3, diff --git a/nibabel/processing.py b/nibabel/processing.py index d38f2ac750..4a9cb86062 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -22,7 +22,6 @@ spnd, _, _ = optional_package('scipy.ndimage') from .affines import AffineError, to_matvec, from_matvec, append_diag -from .funcs import as_closest_canonical from .spaces import vox2out_vox from .nifti1 import Nifti1Header, Nifti1Image from .orientations import axcodes2ornt From 3911610cb4ad918b7e1a06fcd62e02c10e35ab41 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 23:06:12 -0400 Subject: [PATCH 12/27] make voxel size and out shape int type --- nibabel/cmdline/conform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/cmdline/conform.py b/nibabel/cmdline/conform.py index 9b799599f3..df12230508 100644 --- a/nibabel/cmdline/conform.py +++ b/nibabel/cmdline/conform.py @@ -27,9 +27,9 @@ def _get_parser(): help="Neuroimaging volume to conform.") p.add_argument("outfile", help="Name of output file.") - p.add_argument("--out-shape", nargs=3, default=(256, 256, 256), + p.add_argument("--out-shape", nargs=3, default=(256, 256, 256), type=int, help="Shape of the conformed output.") - p.add_argument("--voxel-size", nargs=3, default=(1, 1, 1), + p.add_argument("--voxel-size", nargs=3, default=(1, 1, 1), type=int, help="Voxel size in millimeters of the conformed output.") p.add_argument("--orientation", default="RAS", help="Orientation of the conformed output.") From 0d8843bf43ac8f75058ab2a5dc7b5b253062a309 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Tue, 7 Apr 2020 23:06:36 -0400 Subject: [PATCH 13/27] add tests for `nib-conform` command --- nibabel/cmdline/tests/test_conform.py | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 nibabel/cmdline/tests/test_conform.py diff --git a/nibabel/cmdline/tests/test_conform.py b/nibabel/cmdline/tests/test_conform.py new file mode 100644 index 0000000000..75bbbf3aea --- /dev/null +++ b/nibabel/cmdline/tests/test_conform.py @@ -0,0 +1,49 @@ +#!python +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## + +import pytest + +import nibabel as nib +from nibabel.testing import test_data +from nibabel.cmdline.conform import main + + +def test_default(tmpdir): + infile = test_data(fname="anatomical.nii") + outfile = tmpdir / "output.nii.gz" + main([str(infile), str(outfile)]) + assert outfile.isfile() + c = nib.load(outfile) + assert c.shape == (256, 256, 256) + assert c.header.get_zooms() == (1, 1, 1) + assert nib.orientations.aff2axcodes(c.affine) == ('R', 'A', 'S') + + +def test_nondefault(tmpdir): + infile = test_data(fname="anatomical.nii") + outfile = tmpdir / "output.nii.gz" + out_shape = (100, 100, 150) + voxel_size = (1, 2, 4) + orientation = "LAS" + args = "{} {} --out-shape {} --voxel-size {} --orientation {}".format( + infile, outfile, " ".join(map(str, out_shape)), " ".join(map(str, voxel_size)), orientation) + main(args.split()) + assert outfile.isfile() + c = nib.load(outfile) + assert c.shape == out_shape + assert c.header.get_zooms() == voxel_size + assert nib.orientations.aff2axcodes(c.affine) == tuple(orientation) + + +def test_non3d(tmpdir): + infile = test_data(fname="functional.nii") + outfile = tmpdir / "output.nii.gz" + with pytest.raises(ValueError): + main([str(infile), str(outfile)]) From 3e4da11597f77a78b5d5bc2face25277a0bd622f Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 07:40:55 -0400 Subject: [PATCH 14/27] skip tests if scipy not available --- nibabel/cmdline/tests/test_conform.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nibabel/cmdline/tests/test_conform.py b/nibabel/cmdline/tests/test_conform.py index 75bbbf3aea..556f8c96bf 100644 --- a/nibabel/cmdline/tests/test_conform.py +++ b/nibabel/cmdline/tests/test_conform.py @@ -8,13 +8,20 @@ # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +import unittest + import pytest import nibabel as nib from nibabel.testing import test_data from nibabel.cmdline.conform import main +from nibabel.optpkg import optional_package + +_, have_scipy, _ = optional_package('scipy.ndimage') +needs_scipy = unittest.skipUnless(have_scipy, 'These tests need scipy') +@needs_scipy def test_default(tmpdir): infile = test_data(fname="anatomical.nii") outfile = tmpdir / "output.nii.gz" @@ -26,6 +33,7 @@ def test_default(tmpdir): assert nib.orientations.aff2axcodes(c.affine) == ('R', 'A', 'S') +@needs_scipy def test_nondefault(tmpdir): infile = test_data(fname="anatomical.nii") outfile = tmpdir / "output.nii.gz" @@ -42,6 +50,7 @@ def test_nondefault(tmpdir): assert nib.orientations.aff2axcodes(c.affine) == tuple(orientation) +@needs_scipy def test_non3d(tmpdir): infile = test_data(fname="functional.nii") outfile = tmpdir / "output.nii.gz" From 8b712ca489ddccf7ee1af45721a297e670bdca39 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:04:09 -0400 Subject: [PATCH 15/27] use `nb.save(img, filename)` instead of `img.save(...)` Co-Authored-By: Chris Markiewicz --- nibabel/cmdline/conform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cmdline/conform.py b/nibabel/cmdline/conform.py index df12230508..92d4919128 100644 --- a/nibabel/cmdline/conform.py +++ b/nibabel/cmdline/conform.py @@ -62,4 +62,4 @@ def main(args=None): cval=0.0, orientation=kwargs["orientation"]) - out_img.to_filename(kwargs["outfile"]) + nb.save(out_img, kwargs["outfile"]) From 67ace2ffd5d1a072d894f84ae69210e8de8805bc Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:04:47 -0400 Subject: [PATCH 16/27] keep input class by default in `conform` Co-Authored-By: Chris Markiewicz --- nibabel/processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 4a9cb86062..af199fcfd7 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -319,7 +319,7 @@ def conform(from_img, order=3, cval=0.0, orientation='RAS', - out_class=Nifti1Image): + out_class=None): """ Resample image to ``out_shape`` with voxels of size ``voxel_size``. Using the default arguments, this function is meant to replicate most parts From 527400d6f2e152240e4d1b7fcb1c5cfac5b85437 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:08:02 -0400 Subject: [PATCH 17/27] do not error on non-3d inputs The `conform` function can be made more general in the future, and tests that error on non-3d inputs might signal to future developers that it should not be generalized. --- nibabel/cmdline/tests/test_conform.py | 8 -------- nibabel/tests/test_processing.py | 11 ----------- 2 files changed, 19 deletions(-) diff --git a/nibabel/cmdline/tests/test_conform.py b/nibabel/cmdline/tests/test_conform.py index 556f8c96bf..df836704aa 100644 --- a/nibabel/cmdline/tests/test_conform.py +++ b/nibabel/cmdline/tests/test_conform.py @@ -48,11 +48,3 @@ def test_nondefault(tmpdir): assert c.shape == out_shape assert c.header.get_zooms() == voxel_size assert nib.orientations.aff2axcodes(c.affine) == tuple(orientation) - - -@needs_scipy -def test_non3d(tmpdir): - infile = test_data(fname="functional.nii") - outfile = tmpdir / "output.nii.gz" - with pytest.raises(ValueError): - main([str(infile), str(outfile)]) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index 0027975847..cf63a5b3d1 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -443,14 +443,3 @@ def test_conform(): assert c.dataobj.dtype.type == anat.dataobj.dtype.type assert aff2axcodes(c.affine) == ('L', 'P', 'I') assert isinstance(c, Nifti2Image) - - # Error on non-3D arguments. - with pytest.raises(ValueError): - conform(anat, out_shape=(100, 100)) - with pytest.raises(ValueError): - conform(anat, voxel_size=(2, 2)) - - # Error on non-3D images. - func = nib.load(pjoin(DATA_DIR, 'functional.nii')) - with pytest.raises(ValueError): - conform(func) From 6e1929815f29c1a181cadc8cdb5bf2b39f6a3074 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:14:56 -0400 Subject: [PATCH 18/27] clean up code --- nibabel/cmdline/conform.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/nibabel/cmdline/conform.py b/nibabel/cmdline/conform.py index 92d4919128..65b4ccc388 100644 --- a/nibabel/cmdline/conform.py +++ b/nibabel/cmdline/conform.py @@ -13,10 +13,9 @@ import argparse from pathlib import Path -import sys from nibabel import __version__ -from nibabel.loadsave import load +from nibabel.loadsave import load, save from nibabel.processing import conform @@ -43,23 +42,18 @@ def _get_parser(): def main(args=None): """Main program function.""" parser = _get_parser() - if args is None: - namespace = parser.parse_args(sys.argv[1:]) - else: - namespace = parser.parse_args(args) + opts = parser.parse_args(args) + from_img = load(opts.infile) - kwargs = vars(namespace) - from_img = load(kwargs["infile"]) - - if not kwargs["force"] and Path(kwargs["outfile"]).exists(): - raise FileExistsError("Output file exists: {}".format(kwargs["outfile"])) + if not opts.force and Path(opts.outfile).exists(): + raise FileExistsError("Output file exists: {}".format(opts.outfile)) out_img = conform( from_img=from_img, - out_shape=kwargs["out_shape"], - voxel_size=kwargs["voxel_size"], + out_shape=opts.out_shape, + voxel_size=opts.voxel_size, order=3, cval=0.0, - orientation=kwargs["orientation"]) + orientation=opts.orientation) - nb.save(out_img, kwargs["outfile"]) + save(out_img, opts.outfile) From 07fa2543dd61bb7d301ee0c554ab97a540e052f1 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:27:06 -0400 Subject: [PATCH 19/27] correct the re-orientation of the output image in `conform` --- nibabel/processing.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index af199fcfd7..95772aafd4 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -24,7 +24,7 @@ from .affines import AffineError, to_matvec, from_matvec, append_diag from .spaces import vox2out_vox from .nifti1 import Nifti1Header, Nifti1Image -from .orientations import axcodes2ornt +from .orientations import axcodes2ornt, io_orientation, ornt_transform from .imageclasses import spatial_axes_first SIGMA2FWHM = np.sqrt(8 * np.log(2)) @@ -386,5 +386,7 @@ def conform(from_img, cval=cval, out_class=out_class) # Reorient to desired orientation. - ornt = axcodes2ornt(orientation, labels=list(zip('RPI', 'LAS'))) - return out_img.as_reoriented(ornt) + start_ornt = io_orientation(out_img.affine) + end_ornt = axcodes2ornt(orientation) + transform = ornt_transform(start_ornt, end_ornt) + return out_img.as_reoriented(transform) From 00825c7d5d9f38a0f9045f330d462dd650efbd71 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:32:13 -0400 Subject: [PATCH 20/27] make `to_img` the same image/header classes as input image --- nibabel/processing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 95772aafd4..778f9fe9e0 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -374,11 +374,11 @@ def conform(from_img, raise ValueError("`voxel_size` must have {} values".format(required_ndim)) # Create fake image of the image we want to resample to. - hdr = Nifti1Header() + hdr = from_img.header_class() hdr.set_data_shape(out_shape) hdr.set_zooms(voxel_size) dst_aff = hdr.get_best_affine() - to_img = Nifti1Image(np.empty(out_shape), affine=dst_aff, header=hdr) + to_img = from_img.__class__(np.empty(out_shape), affine=dst_aff, header=hdr) # Resample input image. out_img = resample_from_to( From 4ca32bacb29c866e7df81be36c95f0c6c10ae2ff Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 10:34:04 -0400 Subject: [PATCH 21/27] make pep8 gods happy --- nibabel/processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index 778f9fe9e0..f3ee3dd2c5 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -23,7 +23,7 @@ from .affines import AffineError, to_matvec, from_matvec, append_diag from .spaces import vox2out_vox -from .nifti1 import Nifti1Header, Nifti1Image +from .nifti1 import Nifti1Image from .orientations import axcodes2ornt, io_orientation, ornt_transform from .imageclasses import spatial_axes_first From a536ed3df09a7042db3a4e9fb17804f80c91aceb Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 11:20:10 -0400 Subject: [PATCH 22/27] test for errors on non-3d inputs and arguments A note to future developers is included that the `conform` function should be made more general. --- nibabel/tests/test_processing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nibabel/tests/test_processing.py b/nibabel/tests/test_processing.py index cf63a5b3d1..64139bab43 100644 --- a/nibabel/tests/test_processing.py +++ b/nibabel/tests/test_processing.py @@ -443,3 +443,13 @@ def test_conform(): assert c.dataobj.dtype.type == anat.dataobj.dtype.type assert aff2axcodes(c.affine) == ('L', 'P', 'I') assert isinstance(c, Nifti2Image) + + # TODO: support nD images in `conform` in the future, but for now, test that we get + # errors on non-3D images. + func = nib.load(pjoin(DATA_DIR, 'functional.nii')) + with pytest.raises(ValueError): + conform(func) + with pytest.raises(ValueError): + conform(anat, out_shape=(100, 100)) + with pytest.raises(ValueError): + conform(anat, voxel_size=(2, 2)) From 3af4bd88d5eb751ab11f8962acf0f1c27ac1605f Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 11:21:07 -0400 Subject: [PATCH 23/27] test that file is not overwritten without `--force` --- nibabel/cmdline/tests/test_conform.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nibabel/cmdline/tests/test_conform.py b/nibabel/cmdline/tests/test_conform.py index df836704aa..fd29cbf5a2 100644 --- a/nibabel/cmdline/tests/test_conform.py +++ b/nibabel/cmdline/tests/test_conform.py @@ -32,6 +32,12 @@ def test_default(tmpdir): assert c.header.get_zooms() == (1, 1, 1) assert nib.orientations.aff2axcodes(c.affine) == ('R', 'A', 'S') + with pytest.raises(FileExistsError): + main([str(infile), str(outfile)]) + + main([str(infile), str(outfile), "--force"]) + assert outfile.isfile() + @needs_scipy def test_nondefault(tmpdir): From 3658170cb1fb06deee1006e61528cfecb7a34f89 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Wed, 8 Apr 2020 14:30:54 -0400 Subject: [PATCH 24/27] remove bin/nib-conform because it is unused --- bin/nib-conform | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 bin/nib-conform diff --git a/bin/nib-conform b/bin/nib-conform deleted file mode 100644 index 26ae5b5bf4..0000000000 --- a/bin/nib-conform +++ /dev/null @@ -1,17 +0,0 @@ -#!python -# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -# -# See COPYING file distributed along with the NiBabel package for the -# copyright and license terms. -# -### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -""" -Conform a volume to a new shape and/or voxel size. -""" - -from nibabel.cmdline.conform import main - -if __name__ == '__main__': - main() From eb097f46c60360b6177a8db95cf1b63676c96a92 Mon Sep 17 00:00:00 2001 From: Jakub Kaczmarzyk Date: Sat, 11 Apr 2020 13:58:33 -0400 Subject: [PATCH 25/27] copy header from input image in `conform` The implementation for `conform` is informed by FreeSurfer: https://github.com/freesurfer/freesurfer/blob/4db941ef298c0ac5fb78c29fd0e95571ac363e16/utils/mri_conform.cpp#L140-L256 In that function, the KeepDC flag is false, so the shorter path in the if-else statement is taken. --- nibabel/processing.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index f3ee3dd2c5..bed059a6b0 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -373,16 +373,15 @@ def conform(from_img, elif len(voxel_size) != required_ndim: raise ValueError("`voxel_size` must have {} values".format(required_ndim)) - # Create fake image of the image we want to resample to. - hdr = from_img.header_class() - hdr.set_data_shape(out_shape) - hdr.set_zooms(voxel_size) - dst_aff = hdr.get_best_affine() - to_img = from_img.__class__(np.empty(out_shape), affine=dst_aff, header=hdr) + # Create template image to which input is resampled. + tmpl_hdr = from_img.header_class().from_header(from_img.header) + tmpl_hdr.set_data_shape(out_shape) + tmpl_hdr.set_zooms(voxel_size) + tmpl = from_img.__class__(np.empty(out_shape), affine=np.eye(4), header=tmpl_hdr) # Resample input image. out_img = resample_from_to( - from_img=from_img, to_vox_map=to_img, order=order, mode="constant", + from_img=from_img, to_vox_map=tmpl, order=order, mode="constant", cval=cval, out_class=out_class) # Reorient to desired orientation. From 241f58f76eb140e381a23eb3c1b5296cb4d5d320 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 15 Apr 2020 10:23:08 -0400 Subject: [PATCH 26/27] NF: Add nibabel.affines.rescale_affine function --- nibabel/affines.py | 40 +++++++++++++++++++++++++++++++++++ nibabel/tests/test_affines.py | 22 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/nibabel/affines.py b/nibabel/affines.py index c2b2a3b1d0..87bec4be07 100644 --- a/nibabel/affines.py +++ b/nibabel/affines.py @@ -323,3 +323,43 @@ def obliquity(affine): vs = voxel_sizes(affine) best_cosines = np.abs(affine[:-1, :-1] / vs).max(axis=1) return np.arccos(best_cosines) + + +def rescale_affine(affine, shape, zooms, new_shape=None): + """ Return a new affine matrix with updated voxel sizes (zooms) + + This function preserves the rotations and shears of the original + affine, as well as the RAS location of the central voxel of the + image. + + Parameters + ---------- + affine : (N, N) array-like + NxN transform matrix in homogeneous coordinates representing an affine + transformation from an (N-1)-dimensional space to an (N-1)-dimensional + space. An example is a 4x4 transform representing rotations and + translations in 3 dimensions. + shape : (N-1,) array-like + The extent of the (N-1) dimensions of the original space + zooms : (N-1,) array-like + The size of voxels of the output affine + new_shape : (N-1,) array-like, optional + The extent of the (N-1) dimensions of the space described by the + new affine. If ``None``, use ``shape``. + + Returns + ------- + affine : (N, N) array + A new affine transform with the specified voxel sizes + + """ + shape = np.array(shape, copy=False) + new_shape = np.array(new_shape if new_shape is not None else shape) + + s = voxel_sizes(affine) + rzs_out = affine[:3, :3] * zooms / s + + # Using xyz = A @ ijk, determine translation + centroid = apply_affine(affine, (shape - 1) // 2) + t_out = centroid - rzs_out @ ((new_shape - 1) // 2) + return from_matvec(rzs_out, t_out) diff --git a/nibabel/tests/test_affines.py b/nibabel/tests/test_affines.py index 6fd2f59fab..8013fdc2eb 100644 --- a/nibabel/tests/test_affines.py +++ b/nibabel/tests/test_affines.py @@ -7,7 +7,8 @@ from ..eulerangles import euler2mat from ..affines import (AffineError, apply_affine, append_diag, to_matvec, - from_matvec, dot_reduce, voxel_sizes, obliquity) + from_matvec, dot_reduce, voxel_sizes, obliquity, rescale_affine) +from ..orientations import aff2axcodes import pytest @@ -192,3 +193,22 @@ def test_obliquity(): assert_almost_equal(obliquity(aligned), [0.0, 0.0, 0.0]) assert_almost_equal(obliquity(oblique) * 180 / pi, [0.0810285, 5.1569949, 5.1569376]) + + +def test_rescale_affine(): + rng = np.random.RandomState(20200415) + orig_shape = rng.randint(low=20, high=512, size=(3,)) + orig_aff = np.eye(4) + orig_aff[:3, :] = rng.normal(size=(3, 4)) + orig_zooms = voxel_sizes(orig_aff) + orig_axcodes = aff2axcodes(orig_aff) + orig_centroid = apply_affine(orig_aff, (orig_shape - 1) // 2) + + for new_shape in (None, tuple(orig_shape), (256, 256, 256), (64, 64, 40)): + for new_zooms in ((1, 1, 1), (2, 2, 3), (0.5, 0.5, 0.5)): + new_aff = rescale_affine(orig_aff, orig_shape, new_zooms, new_shape) + assert aff2axcodes(new_aff) == orig_axcodes + if new_shape is None: + new_shape = tuple(orig_shape) + new_centroid = apply_affine(new_aff, (np.array(new_shape) - 1) // 2) + assert_almost_equal(new_centroid, orig_centroid) From f77fbb5a660ac227d49f42e6f574c4b4acede0a2 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 15 Apr 2020 10:23:45 -0400 Subject: [PATCH 27/27] RF: Update conformation to reorient, rescale and resample --- nibabel/processing.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/nibabel/processing.py b/nibabel/processing.py index bed059a6b0..b3bd83d706 100644 --- a/nibabel/processing.py +++ b/nibabel/processing.py @@ -21,7 +21,7 @@ from .optpkg import optional_package spnd, _, _ = optional_package('scipy.ndimage') -from .affines import AffineError, to_matvec, from_matvec, append_diag +from .affines import AffineError, to_matvec, from_matvec, append_diag, rescale_affine from .spaces import vox2out_vox from .nifti1 import Nifti1Image from .orientations import axcodes2ornt, io_orientation, ornt_transform @@ -373,19 +373,18 @@ def conform(from_img, elif len(voxel_size) != required_ndim: raise ValueError("`voxel_size` must have {} values".format(required_ndim)) - # Create template image to which input is resampled. - tmpl_hdr = from_img.header_class().from_header(from_img.header) - tmpl_hdr.set_data_shape(out_shape) - tmpl_hdr.set_zooms(voxel_size) - tmpl = from_img.__class__(np.empty(out_shape), affine=np.eye(4), header=tmpl_hdr) + start_ornt = io_orientation(from_img.affine) + end_ornt = axcodes2ornt(orientation) + transform = ornt_transform(start_ornt, end_ornt) + + # Reorient first to ensure shape matches expectations + reoriented = from_img.as_reoriented(transform) + + out_aff = rescale_affine(reoriented.affine, reoriented.shape, voxel_size, out_shape) # Resample input image. out_img = resample_from_to( - from_img=from_img, to_vox_map=tmpl, order=order, mode="constant", + from_img=from_img, to_vox_map=(out_shape, out_aff), order=order, mode="constant", cval=cval, out_class=out_class) - # Reorient to desired orientation. - start_ornt = io_orientation(out_img.affine) - end_ornt = axcodes2ornt(orientation) - transform = ornt_transform(start_ornt, end_ornt) - return out_img.as_reoriented(transform) + return out_img