-
Notifications
You must be signed in to change notification settings - Fork 263
NF: nib-convert CLI tool #1113
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
NF: nib-convert CLI tool #1113
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
0932788
ENH: Add simple nib-convert tool
effigies d33336a
TEST: Test nib-convert functionality
effigies 3751f61
FIX: Finalize NIfTI dtype before calling Analyze.to_file_map
effigies ddce800
TEST: Verify --force overwrites
effigies 9a9a590
UI: Better describe --force option
effigies File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Not too important, but is there a way to make sure that this is the file that was created in the forced run, and not the one that existed there before? You might worry that the
force
flag would somehow make the program do nothing.