-
Notifications
You must be signed in to change notification settings - Fork 259
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1113 from effigies/enh/nib-convert
NF: nib-convert CLI tool
- Loading branch information
Showing
4 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
# | ||
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## | ||
""" | ||
Convert neuroimaging file to new parameters | ||
""" | ||
|
||
import argparse | ||
from pathlib import Path | ||
import warnings | ||
|
||
import nibabel as nib | ||
|
||
|
||
def _get_parser(): | ||
"""Return command-line argument parser.""" | ||
p = argparse.ArgumentParser(description=__doc__) | ||
p.add_argument("infile", | ||
help="Neuroimaging volume to convert") | ||
p.add_argument("outfile", | ||
help="Name of output file") | ||
p.add_argument("--out-dtype", action="store", | ||
help="On-disk data type; valid argument to numpy.dtype()") | ||
p.add_argument("--image-type", action="store", | ||
help="Name of NiBabel image class to create, e.g. Nifti1Image. " | ||
"If specified, will be used prior to setting dtype. If unspecified, " | ||
"a new image like `infile` will be created and converted to a type " | ||
"matching the extension of `outfile`.") | ||
p.add_argument("-f", "--force", action="store_true", | ||
help="Overwrite output file if it exists, and ignore warnings if possible") | ||
p.add_argument("-V", "--version", action="version", version=f"{p.prog} {nib.__version__}") | ||
|
||
return p | ||
|
||
|
||
def main(args=None): | ||
"""Main program function.""" | ||
parser = _get_parser() | ||
opts = parser.parse_args(args) | ||
orig = nib.load(opts.infile) | ||
|
||
if not opts.force and Path(opts.outfile).exists(): | ||
raise FileExistsError(f"Output file exists: {opts.outfile}") | ||
|
||
if opts.image_type: | ||
klass = getattr(nib, opts.image_type) | ||
else: | ||
klass = orig.__class__ | ||
|
||
out_img = klass.from_image(orig) | ||
if opts.out_dtype: | ||
try: | ||
out_img.set_data_dtype(opts.out_dtype) | ||
except Exception as e: | ||
if opts.force: | ||
warnings.warn(f"Ignoring error: {e!r}") | ||
else: | ||
raise | ||
|
||
nib.save(out_img, opts.outfile) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
#!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 numpy as np | ||
|
||
import nibabel as nib | ||
from nibabel.testing import test_data | ||
from nibabel.cmdline import convert | ||
|
||
|
||
def test_convert_noop(tmp_path): | ||
infile = test_data(fname='anatomical.nii') | ||
outfile = tmp_path / 'output.nii.gz' | ||
|
||
orig = nib.load(infile) | ||
assert not outfile.exists() | ||
|
||
convert.main([str(infile), str(outfile)]) | ||
assert outfile.is_file() | ||
|
||
converted = nib.load(outfile) | ||
assert np.allclose(converted.affine, orig.affine) | ||
assert converted.shape == orig.shape | ||
assert converted.get_data_dtype() == orig.get_data_dtype() | ||
|
||
infile = test_data(fname='resampled_anat_moved.nii') | ||
|
||
with pytest.raises(FileExistsError): | ||
convert.main([str(infile), str(outfile)]) | ||
|
||
convert.main([str(infile), str(outfile), '--force']) | ||
assert outfile.is_file() | ||
|
||
# Verify that we did overwrite | ||
converted2 = nib.load(outfile) | ||
assert not ( | ||
converted2.shape == converted.shape | ||
and np.allclose(converted2.affine, converted.affine) | ||
and np.allclose(converted2.get_fdata(), converted.get_fdata()) | ||
) | ||
|
||
|
||
@pytest.mark.parametrize('data_dtype', ('u1', 'i2', 'float32', 'float', 'int64')) | ||
def test_convert_dtype(tmp_path, data_dtype): | ||
infile = test_data(fname='anatomical.nii') | ||
outfile = tmp_path / 'output.nii.gz' | ||
|
||
orig = nib.load(infile) | ||
assert not outfile.exists() | ||
|
||
# np.dtype() will give us the dtype for the system endianness if that | ||
# mismatches the data file, we will fail equality, so get the dtype that | ||
# matches the requested precision but in the endianness of the file | ||
expected_dtype = np.dtype(data_dtype).newbyteorder(orig.header.endianness) | ||
|
||
convert.main([str(infile), str(outfile), '--out-dtype', data_dtype]) | ||
assert outfile.is_file() | ||
|
||
converted = nib.load(outfile) | ||
assert np.allclose(converted.affine, orig.affine) | ||
assert converted.shape == orig.shape | ||
assert converted.get_data_dtype() == expected_dtype | ||
|
||
|
||
@pytest.mark.parametrize('ext,img_class', [ | ||
('mgh', nib.MGHImage), | ||
('img', nib.Nifti1Pair), | ||
]) | ||
def test_convert_by_extension(tmp_path, ext, img_class): | ||
infile = test_data(fname='anatomical.nii') | ||
outfile = tmp_path / f'output.{ext}' | ||
|
||
orig = nib.load(infile) | ||
assert not outfile.exists() | ||
|
||
convert.main([str(infile), str(outfile)]) | ||
assert outfile.is_file() | ||
|
||
converted = nib.load(outfile) | ||
assert np.allclose(converted.affine, orig.affine) | ||
assert converted.shape == orig.shape | ||
assert converted.__class__ == img_class | ||
|
||
|
||
@pytest.mark.parametrize('ext,img_class', [ | ||
('mgh', nib.MGHImage), | ||
('img', nib.Nifti1Pair), | ||
('nii', nib.Nifti2Image), | ||
]) | ||
def test_convert_imgtype(tmp_path, ext, img_class): | ||
infile = test_data(fname='anatomical.nii') | ||
outfile = tmp_path / f'output.{ext}' | ||
|
||
orig = nib.load(infile) | ||
assert not outfile.exists() | ||
|
||
convert.main([str(infile), str(outfile), '--image-type', img_class.__name__]) | ||
assert outfile.is_file() | ||
|
||
converted = nib.load(outfile) | ||
assert np.allclose(converted.affine, orig.affine) | ||
assert converted.shape == orig.shape | ||
assert converted.__class__ == img_class | ||
|
||
|
||
def test_convert_nifti_int_fail(tmp_path): | ||
infile = test_data(fname='anatomical.nii') | ||
outfile = tmp_path / f'output.nii' | ||
|
||
orig = nib.load(infile) | ||
assert not outfile.exists() | ||
|
||
with pytest.raises(ValueError): | ||
convert.main([str(infile), str(outfile), '--out-dtype', 'int']) | ||
assert not outfile.exists() | ||
|
||
with pytest.warns(UserWarning): | ||
convert.main([str(infile), str(outfile), '--out-dtype', 'int', '--force']) | ||
assert outfile.is_file() | ||
|
||
converted = nib.load(outfile) | ||
assert np.allclose(converted.affine, orig.affine) | ||
assert converted.shape == orig.shape | ||
# Note: '--force' ignores the error, but can't interpret it enough to do | ||
# the cast anyway | ||
assert converted.get_data_dtype() == orig.get_data_dtype() | ||
|
||
|
||
@pytest.mark.parametrize('orig_dtype,alias,expected_dtype', [ | ||
('int64', 'mask', 'uint8'), | ||
('int64', 'compat', 'int32'), | ||
('int64', 'smallest', 'uint8'), | ||
('float64', 'mask', 'uint8'), | ||
('float64', 'compat', 'float32'), | ||
]) | ||
def test_convert_aliases(tmp_path, orig_dtype, alias, expected_dtype): | ||
orig_fname = tmp_path / 'orig.nii' | ||
out_fname = tmp_path / 'out.nii' | ||
|
||
arr = np.arange(24).reshape((2, 3, 4)) | ||
img = nib.Nifti1Image(arr, np.eye(4), dtype=orig_dtype) | ||
img.to_filename(orig_fname) | ||
|
||
assert orig_fname.exists() | ||
assert not out_fname.exists() | ||
|
||
convert.main([str(orig_fname), str(out_fname), '--out-dtype', alias]) | ||
assert out_fname.is_file() | ||
|
||
expected_dtype = np.dtype(expected_dtype).newbyteorder(img.header.endianness) | ||
|
||
converted = nib.load(out_fname) | ||
assert converted.get_data_dtype() == expected_dtype |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters