Skip to content

NF: Conformation function and CLI tool to apply shape, orientation and zooms #853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Apr 19, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b897935
add `conform` and `_transform_range` funcs
Dec 11, 2019
76e9aed
Merge remote-tracking branch 'upstream/master' into add/fs-conform
Dec 11, 2019
9895eb0
add documentation and fix style
Dec 12, 2019
49e4ada
clean up documentation + only conform 3D
Dec 13, 2019
57c3648
add tests for `conform` and `_transform_range`
Dec 13, 2019
a681bdd
only test `conform` if scipy installed
Dec 13, 2019
4e62b7c
add `nib-conform` console script
kaczmarj Apr 8, 2020
e19b022
tighten scope of conform function
kaczmarj Apr 8, 2020
12ea136
Merge remote-tracking branch 'upstream/master' into add/fs-conform
kaczmarj Apr 8, 2020
89eedc5
add `nib-conform`
kaczmarj Apr 8, 2020
348f838
use proper labels for orientation
kaczmarj Apr 8, 2020
9491806
add non-3d tests
kaczmarj Apr 8, 2020
a9ce73b
fix style
kaczmarj Apr 8, 2020
3911610
make voxel size and out shape int type
kaczmarj Apr 8, 2020
0d8843b
add tests for `nib-conform` command
kaczmarj Apr 8, 2020
3e4da11
skip tests if scipy not available
kaczmarj Apr 8, 2020
8b712ca
use `nb.save(img, filename)` instead of `img.save(...)`
kaczmarj Apr 8, 2020
67ace2f
keep input class by default in `conform`
kaczmarj Apr 8, 2020
527400d
do not error on non-3d inputs
kaczmarj Apr 8, 2020
6e19298
clean up code
kaczmarj Apr 8, 2020
07fa254
correct the re-orientation of the output image in `conform`
kaczmarj Apr 8, 2020
00825c7
make `to_img` the same image/header classes as input image
kaczmarj Apr 8, 2020
4ca32ba
make pep8 gods happy
kaczmarj Apr 8, 2020
a536ed3
test for errors on non-3d inputs and arguments
kaczmarj Apr 8, 2020
3af4bd8
test that file is not overwritten without `--force`
kaczmarj Apr 8, 2020
3658170
remove bin/nib-conform because it is unused
kaczmarj Apr 8, 2020
eb097f4
copy header from input image in `conform`
kaczmarj Apr 11, 2020
241f58f
NF: Add nibabel.affines.rescale_affine function
effigies Apr 15, 2020
f77fbb5
RF: Update conformation to reorient, rescale and resample
effigies Apr 15, 2020
2177a59
Merge pull request #1 from effigies/add/fs-conform
kaczmarj Apr 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion nibabel/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,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))
Expand Down Expand Up @@ -310,3 +311,96 @@ 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):
""" 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it become resilient to x_max == x_min when it would either set it to new_min or blow some better than ZeroDivisonError exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @yarikoptic - i will account for x_max == x_min and i will add a test for this case.



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. 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
----------
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``)
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``
"""
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)
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
35 changes: 33 additions & 2 deletions nibabel/tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -426,3 +427,33 @@ 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))


@needs_scipy
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that what freesurfer does? I thought it might be nice to have this one applicable to 4D (or 5D - AFNI etc) series, to "conform" the first 3 dimensions to desired resolution/orientation etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think it is what freesurfer does, but for simplicity and due to my lack of knowledge, i made it this way. how should the output shape and zooms be changed to support 4- and 5-D images?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output shape and zooms would not be affected by rolling the first three axes.

In any event, I'm okay with getting this in and adding 4/5D image support later, but if we're going to have a test, then we should mark it as one that we intend to relax. Otherwise it may look like a desired constraint to future developers.

func = nib.load(pjoin(DATA_DIR, 'functional.nii'))
assert_raises(ValueError, conform, func)