-
Notifications
You must be signed in to change notification settings - Fork 16
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
ENH: command line interface #55
Changes from all commits
ee5f8ef
a61fcfe
ce59572
8bedfa4
6a06cdc
eacc366
3ae1642
e8aa182
63d08ac
5d5f5e6
5835a97
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
mgxd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
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(""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does not argparse compose a good description for commands? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i find it's nice to have a little blurb describing the subparsers at the top level (argparse doesn't do this) |
||
NiTransforms command-line utility. | ||
|
||
Commands: | ||
|
||
apply Apply a transformation to an image | ||
|
||
For command specific information, use 'nt <command> -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
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.
click is pretty lightweight - @effigies do you think would be some resistance in nibabel to migrate
nib-ls
and other command line scripts I might not be aware of?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.
In general, nibabel has tried to avoid mandatory dependencies besides
numpy
. You might get some pushback on click, but might not, given it doesn't have any dependencies of its own.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.
i've made the
main()
allow supplemental args to be passed in toargparse.ArgumentParser.parse_args()
- hopefully that is sufficient for testing purposes