diff --git a/nitransforms/base.py b/nitransforms/base.py index 466019d4..9e43c190 100644 --- a/nitransforms/base.py +++ b/nitransforms/base.py @@ -225,7 +225,7 @@ def apply(self, spatialimage, reference=None, order : int, optional The order of the spline interpolation, default is 3. The order has to be in the range 0-5. - mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional + mode : {'constant', 'reflect', 'nearest', 'mirror', 'wrap'}, optional Determines how the input image is extended when the resamplings overflows a border. Default is 'constant'. cval : float, optional diff --git a/nitransforms/cli.py b/nitransforms/cli.py new file mode 100644 index 00000000..0e82ece2 --- /dev/null +++ b/nitransforms/cli.py @@ -0,0 +1,134 @@ +from argparse import ArgumentParser, RawDescriptionHelpFormatter +import os +from textwrap import dedent + + +from .linear import load as linload +from .nonlinear import load as nlinload + + +def cli_apply(pargs): + """ + Apply a transformation to an image, resampling on the reference. + + Sample usage: + + $ nt apply xform.fsl moving.nii.gz --ref reference.nii.gz --out moved.nii.gz + + $ nt apply warp.nii.gz moving.nii.gz --fmt afni --nonlinear + + """ + fmt = pargs.fmt or pargs.transform.split('.')[-1] + if fmt in ('tfm', 'mat', 'h5', 'x5'): + fmt = 'itk' + elif fmt == 'lta': + fmt = 'fs' + + if fmt not in ('fs', 'itk', 'fsl', 'afni', 'x5'): + raise ValueError( + "Cannot determine transformation format, manually set format with the `--fmt` flag" + ) + + if pargs.nonlinear: + xfm = nlinload(pargs.transform, fmt=fmt) + else: + xfm = linload(pargs.transform, fmt=fmt) + + # ensure a reference is set + xfm.reference = pargs.ref or pargs.moving + + moved = xfm.apply( + pargs.moving, + order=pargs.order, + mode=pargs.mode, + cval=pargs.cval, + prefilter=pargs.prefilter + ) + moved.to_filename( + pargs.out or "nt_{}".format(os.path.basename(pargs.moving)) + ) + + +def get_parser(): + desc = dedent(""" + NiTransforms command-line utility. + + Commands: + + apply Apply a transformation to an image + + For command specific information, use 'nt -h'. + """) + + parser = ArgumentParser( + description=desc, formatter_class=RawDescriptionHelpFormatter + ) + subparsers = parser.add_subparsers(dest='command') + + def _add_subparser(name, description): + subp = subparsers.add_parser( + name, + description=dedent(description), + formatter_class=RawDescriptionHelpFormatter, + ) + return subp + + applyp = _add_subparser('apply', cli_apply.__doc__) + applyp.set_defaults(func=cli_apply) + applyp.add_argument('transform', help='The transform file') + applyp.add_argument( + 'moving', help='The image containing the data to be resampled' + ) + applyp.add_argument('--ref', help='The reference space to resample onto') + applyp.add_argument( + '--fmt', + choices=('itk', 'fsl', 'afni', 'fs', 'x5'), + help='Format of transformation. If no option is passed, nitransforms will ' + 'estimate based on the transformation file extension.' + ) + applyp.add_argument( + '--out', help="The transformed image. If not set, will be set to `nt_{moving}`" + ) + applyp.add_argument( + '--nonlinear', action='store_true', help='Transformation is nonlinear (default: False)' + ) + applykwargs = applyp.add_argument_group('Apply customization') + applykwargs.add_argument( + '--order', + type=int, + default=3, + choices=range(6), + help='The order of the spline transformation (default: 3)' + ) + applykwargs.add_argument( + '--mode', + choices=('constant', 'reflect', 'nearest', 'mirror', 'wrap'), + default='constant', + help='Determines how the input image is extended when the resampling overflows a border ' + '(default: constant)' + ) + applykwargs.add_argument( + '--cval', + type=float, + default=0.0, + help='Constant used when using "constant" mode (default: 0.0)' + ) + applykwargs.add_argument( + '--prefilter', + action='store_false', + help="Determines if the image's data array is prefiltered with a spline filter before " + "interpolation (default: True)" + ) + return parser, subparsers + + +def main(pargs=None): + parser, subparsers = get_parser() + pargs = parser.parse_args(pargs) + + try: + pargs.func(pargs) + except Exception as e: + subparser = subparsers.choices[pargs.command] + subparser.print_help() + raise(e) diff --git a/nitransforms/tests/test_cli.py b/nitransforms/tests/test_cli.py new file mode 100644 index 00000000..071bf5ab --- /dev/null +++ b/nitransforms/tests/test_cli.py @@ -0,0 +1,68 @@ +from textwrap import dedent + +import pytest + +from ..cli import cli_apply, main as ntcli + + +def test_cli(capsys): + # empty command + with pytest.raises(SystemExit): + ntcli() + # invalid command + with pytest.raises(SystemExit): + ntcli(['idk']) + + with pytest.raises(SystemExit) as sysexit: + ntcli(['-h']) + console = capsys.readouterr() + assert sysexit.value.code == 0 + # possible commands + assert r"{apply}" in console.out + + with pytest.raises(SystemExit): + ntcli(['apply', '-h']) + console = capsys.readouterr() + assert dedent(cli_apply.__doc__) in console.out + assert sysexit.value.code == 0 + + +def test_apply_linear(tmpdir, data_path, get_testdata): + tmpdir.chdir() + img = 'img.nii.gz' + get_testdata['RAS'].to_filename(img) + lin_xform = str(data_path / 'affine-RAS.itk.tfm') + lin_xform2 = str(data_path / 'affine-RAS.fs.lta') + + # unknown transformation format + with pytest.raises(ValueError): + ntcli(['apply', 'unsupported.xform', 'img.nii.gz']) + + # linear transform arguments + output = tmpdir / 'nt_img.nii.gz' + ntcli(['apply', lin_xform, img, '--ref', img]) + assert output.check() + output.remove() + ntcli(['apply', lin_xform2, img, '--ref', img]) + assert output.check() + + +def test_apply_nl(tmpdir, data_path): + tmpdir.chdir() + img = str(data_path / 'tpl-OASIS30ANTs_T1w.nii.gz') + nl_xform = str(data_path / 'ds-005_sub-01_from-OASIS_to-T1_warp_afni.nii.gz') + + nlargs = ['apply', nl_xform, img] + # format not specified + with pytest.raises(ValueError): + ntcli(nlargs) + + nlargs.extend(['--fmt', 'afni']) + # no linear afni support + with pytest.raises(NotImplementedError): + ntcli(nlargs) + + output = 'moved_from_warp.nii.gz' + nlargs.extend(['--nonlinear', '--out', output]) + ntcli(nlargs) + assert (tmpdir / output).check() diff --git a/setup.cfg b/setup.cfg index 45b3b5cd..43d0bfb7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,10 @@ tests = all = %(test)s +[options.entry_points] +console_scripts = + nt = nitransforms.cli:main + [flake8] max-line-length = 100 ignore = D100,D101,D102,D103,D104,D105,D200,D201,D202,D204,D205,D208,D209,D210,D300,D301,D400,D401,D403,E24,E121,E123,E126,E226,E266,E402,E704,E731,F821,I100,I101,I201,N802,N803,N804,N806,W503,W504,W605