Skip to content

Commit 81e7a67

Browse files
authored
Merge pull request #55 from mgxd/enh/cli
ENH: command line interface
2 parents 76f525e + 5835a97 commit 81e7a67

File tree

4 files changed

+207
-1
lines changed

4 files changed

+207
-1
lines changed

nitransforms/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def apply(self, spatialimage, reference=None,
225225
order : int, optional
226226
The order of the spline interpolation, default is 3.
227227
The order has to be in the range 0-5.
228-
mode : {'reflect', 'constant', 'nearest', 'mirror', 'wrap'}, optional
228+
mode : {'constant', 'reflect', 'nearest', 'mirror', 'wrap'}, optional
229229
Determines how the input image is extended when the resamplings overflows
230230
a border. Default is 'constant'.
231231
cval : float, optional

nitransforms/cli.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from argparse import ArgumentParser, RawDescriptionHelpFormatter
2+
import os
3+
from textwrap import dedent
4+
5+
6+
from .linear import load as linload
7+
from .nonlinear import load as nlinload
8+
9+
10+
def cli_apply(pargs):
11+
"""
12+
Apply a transformation to an image, resampling on the reference.
13+
14+
Sample usage:
15+
16+
$ nt apply xform.fsl moving.nii.gz --ref reference.nii.gz --out moved.nii.gz
17+
18+
$ nt apply warp.nii.gz moving.nii.gz --fmt afni --nonlinear
19+
20+
"""
21+
fmt = pargs.fmt or pargs.transform.split('.')[-1]
22+
if fmt in ('tfm', 'mat', 'h5', 'x5'):
23+
fmt = 'itk'
24+
elif fmt == 'lta':
25+
fmt = 'fs'
26+
27+
if fmt not in ('fs', 'itk', 'fsl', 'afni', 'x5'):
28+
raise ValueError(
29+
"Cannot determine transformation format, manually set format with the `--fmt` flag"
30+
)
31+
32+
if pargs.nonlinear:
33+
xfm = nlinload(pargs.transform, fmt=fmt)
34+
else:
35+
xfm = linload(pargs.transform, fmt=fmt)
36+
37+
# ensure a reference is set
38+
xfm.reference = pargs.ref or pargs.moving
39+
40+
moved = xfm.apply(
41+
pargs.moving,
42+
order=pargs.order,
43+
mode=pargs.mode,
44+
cval=pargs.cval,
45+
prefilter=pargs.prefilter
46+
)
47+
moved.to_filename(
48+
pargs.out or "nt_{}".format(os.path.basename(pargs.moving))
49+
)
50+
51+
52+
def get_parser():
53+
desc = dedent("""
54+
NiTransforms command-line utility.
55+
56+
Commands:
57+
58+
apply Apply a transformation to an image
59+
60+
For command specific information, use 'nt <command> -h'.
61+
""")
62+
63+
parser = ArgumentParser(
64+
description=desc, formatter_class=RawDescriptionHelpFormatter
65+
)
66+
subparsers = parser.add_subparsers(dest='command')
67+
68+
def _add_subparser(name, description):
69+
subp = subparsers.add_parser(
70+
name,
71+
description=dedent(description),
72+
formatter_class=RawDescriptionHelpFormatter,
73+
)
74+
return subp
75+
76+
applyp = _add_subparser('apply', cli_apply.__doc__)
77+
applyp.set_defaults(func=cli_apply)
78+
applyp.add_argument('transform', help='The transform file')
79+
applyp.add_argument(
80+
'moving', help='The image containing the data to be resampled'
81+
)
82+
applyp.add_argument('--ref', help='The reference space to resample onto')
83+
applyp.add_argument(
84+
'--fmt',
85+
choices=('itk', 'fsl', 'afni', 'fs', 'x5'),
86+
help='Format of transformation. If no option is passed, nitransforms will '
87+
'estimate based on the transformation file extension.'
88+
)
89+
applyp.add_argument(
90+
'--out', help="The transformed image. If not set, will be set to `nt_{moving}`"
91+
)
92+
applyp.add_argument(
93+
'--nonlinear', action='store_true', help='Transformation is nonlinear (default: False)'
94+
)
95+
applykwargs = applyp.add_argument_group('Apply customization')
96+
applykwargs.add_argument(
97+
'--order',
98+
type=int,
99+
default=3,
100+
choices=range(6),
101+
help='The order of the spline transformation (default: 3)'
102+
)
103+
applykwargs.add_argument(
104+
'--mode',
105+
choices=('constant', 'reflect', 'nearest', 'mirror', 'wrap'),
106+
default='constant',
107+
help='Determines how the input image is extended when the resampling overflows a border '
108+
'(default: constant)'
109+
)
110+
applykwargs.add_argument(
111+
'--cval',
112+
type=float,
113+
default=0.0,
114+
help='Constant used when using "constant" mode (default: 0.0)'
115+
)
116+
applykwargs.add_argument(
117+
'--prefilter',
118+
action='store_false',
119+
help="Determines if the image's data array is prefiltered with a spline filter before "
120+
"interpolation (default: True)"
121+
)
122+
return parser, subparsers
123+
124+
125+
def main(pargs=None):
126+
parser, subparsers = get_parser()
127+
pargs = parser.parse_args(pargs)
128+
129+
try:
130+
pargs.func(pargs)
131+
except Exception as e:
132+
subparser = subparsers.choices[pargs.command]
133+
subparser.print_help()
134+
raise(e)

nitransforms/tests/test_cli.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from textwrap import dedent
2+
3+
import pytest
4+
5+
from ..cli import cli_apply, main as ntcli
6+
7+
8+
def test_cli(capsys):
9+
# empty command
10+
with pytest.raises(SystemExit):
11+
ntcli()
12+
# invalid command
13+
with pytest.raises(SystemExit):
14+
ntcli(['idk'])
15+
16+
with pytest.raises(SystemExit) as sysexit:
17+
ntcli(['-h'])
18+
console = capsys.readouterr()
19+
assert sysexit.value.code == 0
20+
# possible commands
21+
assert r"{apply}" in console.out
22+
23+
with pytest.raises(SystemExit):
24+
ntcli(['apply', '-h'])
25+
console = capsys.readouterr()
26+
assert dedent(cli_apply.__doc__) in console.out
27+
assert sysexit.value.code == 0
28+
29+
30+
def test_apply_linear(tmpdir, data_path, get_testdata):
31+
tmpdir.chdir()
32+
img = 'img.nii.gz'
33+
get_testdata['RAS'].to_filename(img)
34+
lin_xform = str(data_path / 'affine-RAS.itk.tfm')
35+
lin_xform2 = str(data_path / 'affine-RAS.fs.lta')
36+
37+
# unknown transformation format
38+
with pytest.raises(ValueError):
39+
ntcli(['apply', 'unsupported.xform', 'img.nii.gz'])
40+
41+
# linear transform arguments
42+
output = tmpdir / 'nt_img.nii.gz'
43+
ntcli(['apply', lin_xform, img, '--ref', img])
44+
assert output.check()
45+
output.remove()
46+
ntcli(['apply', lin_xform2, img, '--ref', img])
47+
assert output.check()
48+
49+
50+
def test_apply_nl(tmpdir, data_path):
51+
tmpdir.chdir()
52+
img = str(data_path / 'tpl-OASIS30ANTs_T1w.nii.gz')
53+
nl_xform = str(data_path / 'ds-005_sub-01_from-OASIS_to-T1_warp_afni.nii.gz')
54+
55+
nlargs = ['apply', nl_xform, img]
56+
# format not specified
57+
with pytest.raises(ValueError):
58+
ntcli(nlargs)
59+
60+
nlargs.extend(['--fmt', 'afni'])
61+
# no linear afni support
62+
with pytest.raises(NotImplementedError):
63+
ntcli(nlargs)
64+
65+
output = 'moved_from_warp.nii.gz'
66+
nlargs.extend(['--nonlinear', '--out', output])
67+
ntcli(nlargs)
68+
assert (tmpdir / output).check()

setup.cfg

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ tests =
2828
all =
2929
%(test)s
3030

31+
[options.entry_points]
32+
console_scripts =
33+
nt = nitransforms.cli:main
34+
3135
[flake8]
3236
max-line-length = 100
3337
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

0 commit comments

Comments
 (0)