Skip to content

Commit

Permalink
Merge pull request #1113 from effigies/enh/nib-convert
Browse files Browse the repository at this point in the history
NF: nib-convert CLI tool
  • Loading branch information
arokem authored Jun 13, 2022
2 parents b7bbf0e + 9a9a590 commit 669a030
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
66 changes: 66 additions & 0 deletions nibabel/cmdline/convert.py
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)
162 changes: 162 additions & 0 deletions nibabel/cmdline/tests/test_convert.py
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
18 changes: 18 additions & 0 deletions nibabel/nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,24 @@ def get_data_dtype(self, finalize=False):
self.set_data_dtype(datatype) # Clears the alias
return super().get_data_dtype()

def to_file_map(self, file_map=None, dtype=None):
""" Write image to `file_map` or contained ``self.file_map``
Parameters
----------
file_map : None or mapping, optional
files mapping. If None (default) use object's ``file_map``
attribute instead
dtype : dtype-like, optional
The on-disk data type to coerce the data array.
"""
img_dtype = self.get_data_dtype()
self.get_data_dtype(finalize=True)
try:
super().to_file_map(file_map, dtype)
finally:
self.set_data_dtype(img_dtype)

def as_reoriented(self, ornt):
"""Apply an orientation change and return a new image
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ all =
[options.entry_points]
console_scripts =
nib-conform=nibabel.cmdline.conform:main
nib-convert=nibabel.cmdline.convert:main
nib-ls=nibabel.cmdline.ls:main
nib-dicomfs=nibabel.cmdline.dicomfs:main
nib-diff=nibabel.cmdline.diff:main
Expand Down

0 comments on commit 669a030

Please sign in to comment.