-
Notifications
You must be signed in to change notification settings - Fork 262
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
Changes from 16 commits
b897935
76e9aed
9895eb0
49e4ada
57c3648
a681bdd
4e62b7c
e19b022
12ea136
89eedc5
348f838
9491806
a9ce73b
3911610
0d8843b
3e4da11
8b712ca
67ace2f
527400d
6e19298
07fa254
00825c7
4ca32ba
a536ed3
3af4bd8
3658170
eb097f4
241f58f
f77fbb5
2177a59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
#!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 | ||
|
||
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), type=int, | ||
help="Shape of the conformed output.") | ||
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.") | ||
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) | ||
kaczmarj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
kwargs = vars(namespace) | ||
kaczmarj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"]) | ||
kaczmarj marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
#!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 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" | ||
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') | ||
|
||
|
||
@needs_scipy | ||
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) | ||
|
||
|
||
@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)]) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
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 | ||
|
@@ -420,3 +421,36 @@ 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); | ||
|
||
|
||
@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.type == anat.dataobj.dtype.type | ||
assert aff2axcodes(c.affine) == ('R', 'A', 'S') | ||
assert isinstance(c, Nifti1Image) | ||
|
||
# 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.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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')) | ||
with pytest.raises(ValueError): | ||
conform(func) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we install these files anywhere...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should I remove this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so. I'd try removing and make sure
pip install .
produces a workingnib-conform
command.