Skip to content

Commit 6601b00

Browse files
committed
Merge pull request #1042 from dmwelch/ants_JointFusion
ENH: ANTs JointFusion with nosetests
2 parents 98498da + 032c462 commit 6601b00

File tree

5 files changed

+168
-1
lines changed

5 files changed

+168
-1
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Next release
2222
* FIX: OpenfMRI support and FSL 5.0.7 changes (https://github.com/nipy/nipype/pull/1006)
2323
* FIX: Output prefix in SPM Normalize with modulation (https://github.com/nipy/nipype/pull/1023)
2424
* ENH: Usability improvements in cluster environments (https://github.com/nipy/nipype/pull/1025)
25+
* ENH: ANTs JointFusion() (https://github.com/nipy/nipype/pull/1042)
2526

2627
Release 0.10.0 (October 10, 2014)
2728
============

nipype/interfaces/ants/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212

1313
# Segmentation Programs
14-
from .segmentation import Atropos, LaplacianThickness, N4BiasFieldCorrection
14+
from .segmentation import Atropos, LaplacianThickness, N4BiasFieldCorrection, JointFusion
1515

1616
# Visualization Programs
1717
from .visualization import ConvertScalarImageToRGB, CreateTiledMosaic

nipype/interfaces/ants/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ def _num_threads_update(self):
6868
self.inputs.environ.update({PREFERED_ITKv4_THREAD_LIMIT_VARIABLE:
6969
'%s' % self.inputs.num_threads})
7070

71+
@staticmethod
72+
def _format_xarray(val):
73+
""" Convenience method for converting input arrays [1,2,3] to commandline format '1x2x3' """
74+
return 'x'.join([str(x) for x in val])
75+
7176
@classmethod
7277
def set_default_num_threads(cls, num_threads):
7378
"""Set the default number of threads for ITK calls

nipype/interfaces/ants/segmentation.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,3 +570,87 @@ def _list_outputs(self):
570570
'subjectToTemplateLogJacobian.' +
571571
self.inputs.image_suffix)
572572
return outputs
573+
574+
class JointFusionInputSpec(ANTSCommandInputSpec):
575+
dimension = traits.Enum(3, 2, 4, argstr='%d', position=0, usedefault=True, mandatory=True,
576+
desc='image dimension (2, 3, or 4)')
577+
modalities = traits.Int(argstr='%d', position=1, mandatory=True, desc='Number of modalities or features')
578+
warped_intensity_images = InputMultiPath(File(exists=True), argstr="-g %s...", mandatory=True, desc='Warped atlas images')
579+
target_image = InputMultiPath(File(exists=True), argstr='-tg %s...', mandatory=True, desc='Target image(s)')
580+
warped_label_images = InputMultiPath(File(exists=True), argstr="-l %s...", mandatory=True, desc='Warped atlas segmentations')
581+
method = traits.Str(default='Joint', argstr='-m %s', usedefault=True, desc='Select voting method. Options: Joint (Joint Label Fusion). May be followed by optional parameters in brackets, e.g., -m Joint[0.1,2]')
582+
alpha = traits.Float(default=0.1, usedefault=True, requires=['method'], desc='Regularization term added to matrix Mx for inverse')
583+
beta = traits.Int(default=2, usedefault=True, requires=['method'], desc='Exponent for mapping intensity difference to joint error')
584+
output_label_image = File(argstr='%s', mandatory=True, position=-1, desc='Output fusion label map image')
585+
patch_radius = traits.ListInt(minlen=3, maxlen=3, argstr='-rp %s', desc='Patch radius for similarity measures, scalar or vector. Default: 2x2x2')
586+
search_radius = traits.ListInt(minlen=3, maxlen=3, argstr='-rs %s', desc='Local search radius. Default: 3x3x3')
587+
exclusion_region = File(exists=True, argstr='-x %s', desc='Specify an exclusion region for the given label.')
588+
output_posteriors_name_template = traits.Str('POSTERIOR_%02d.nii.gz', argstr='-p %s',
589+
desc="Save the posterior maps (probability that each voxel belongs to each " +\
590+
"label) as images. The number of images saved equals the number of labels. " +\
591+
"The filename pattern must be in C printf format, e.g. posterior%04d.nii.gz")
592+
output_voting_weights_name_template = traits.Str('WEIGHTED_%04d.nii.gz', argstr='-w %s', desc="Save the voting weights as " +\
593+
"images. The number of images saved equals the number of atlases. The " +\
594+
"filename pattern must be in C printf format, e.g. weight%04d.nii.gz")
595+
atlas_group_id = traits.ListInt(argstr='-gp %d...', desc='Assign a group ID for each atlas')
596+
atlas_group_weights = traits.ListInt(argstr='-gpw %d...', desc='Assign the voting weights to each atlas group')
597+
598+
599+
class JointFusionOutputSpec(TraitedSpec):
600+
output_label_image = File(exists=True)
601+
# TODO: optional outputs - output_posteriors, output_voting_weights
602+
603+
604+
class JointFusion(ANTSCommand):
605+
"""
606+
Examples
607+
--------
608+
609+
>>> from nipype.interfaces.ants import JointFusion
610+
>>> at = JointFusion()
611+
>>> at.inputs.dimension = 3
612+
>>> at.inputs.modalities = 1
613+
>>> at.inputs.method = 'Joint[0.1,2]'
614+
>>> at.inputs.output_label_image ='fusion_labelimage_output.nii'
615+
>>> at.inputs.warped_intensity_images = ['im1.nii',
616+
... 'im2.nii']
617+
>>> at.inputs.warped_label_images = ['segmentation0.nii.gz',
618+
... 'segmentation1.nii.gz']
619+
>>> at.inputs.target_image = 'T1.nii'
620+
>>> at.inputs.patch_radius = [3,2,1]
621+
>>> at.inputs.search_radius = [1,2,3]
622+
>>> at.cmdline
623+
'jointfusion 3 1 -m Joint[0.1,2] -rp 3x2x1 -rs 1x2x3 -tg T1.nii -g im1.nii -g im2.nii -l segmentation0.nii.gz -l segmentation1.nii.gz fusion_labelimage_output.nii'
624+
625+
Alternately, you can specify the voting method and parameters more 'Pythonically':
626+
627+
>>> at.inputs.method = 'Joint'
628+
>>> at.inputs.alpha = 0.5
629+
>>> at.inputs.beta = 1
630+
>>> at.cmdline
631+
'jointfusion 3 1 -m Joint[0.5,1] -rp 3x2x1 -rs 1x2x3 -tg T1.nii -g im1.nii -g im2.nii -l segmentation0.nii.gz -l segmentation1.nii.gz fusion_labelimage_output.nii'
632+
"""
633+
input_spec = JointFusionInputSpec
634+
output_spec = JointFusionOutputSpec
635+
_cmd = 'jointfusion'
636+
637+
def _format_arg(self, opt, spec, val):
638+
if opt == 'method':
639+
if '[' in val:
640+
retval = '-m {0}'.format(val)
641+
else:
642+
retval = '-m {0}[{1},{2}]'.format(self.inputs.method, self.inputs.alpha, self.inputs.beta)
643+
elif opt == 'patch_radius':
644+
retval = '-rp {0}'.format(self._format_xarray(val))
645+
elif opt == 'search_radius':
646+
retval = '-rs {0}'.format(self._format_xarray(val))
647+
else:
648+
if opt == 'warped_intensity_images':
649+
assert len(val) == len(self.inputs.warped_label_images), "Number of intensity images and label maps must be the same"
650+
return super(ANTSCommand, self)._format_arg(opt, spec, val)
651+
return retval
652+
653+
def _list_outputs(self):
654+
outputs = self._outputs().get()
655+
outputs['output_label_image'] = os.path.abspath(self.inputs.output_label_image)
656+
return outputs
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from nose import with_setup
2+
from nipype.testing import assert_equal, assert_raises
3+
from nipype.interfaces.base import InputMultiPath
4+
from traits.trait_errors import TraitError
5+
from nipype.interfaces.ants import JointFusion
6+
7+
8+
def test_JointFusion_dimension():
9+
at = JointFusion()
10+
set_dimension = lambda d: setattr(at.inputs, 'dimension', int(d))
11+
for d in range(2, 5):
12+
set_dimension(d)
13+
yield assert_equal, at.inputs.dimension, int(d)
14+
for d in [0, 1, 6, 7]:
15+
yield assert_raises, TraitError, set_dimension, d
16+
17+
18+
def test_JointFusion_modalities():
19+
at = JointFusion()
20+
set_modalities = lambda m: setattr(at.inputs, 'modalities', int(m))
21+
for m in range(1, 5):
22+
set_modalities(m)
23+
yield assert_equal, at.inputs.modalities, int(m)
24+
25+
26+
def test_JointFusion_method():
27+
at = JointFusion()
28+
set_method = lambda a, b: setattr(at.inputs, 'method', 'Joint[%.1f,%d]'.format(a, b))
29+
for a in range(10):
30+
_a = a / 10.0
31+
for b in range(10):
32+
set_method(_a, b)
33+
# set directly
34+
yield assert_equal, at.inputs.method, 'Joint[%.1f,%d]'.format(_a, b)
35+
aprime = _a + 0.1
36+
bprime = b + 1
37+
at.inputs.alpha = aprime
38+
at.inputs.beta = bprime
39+
# set with alpha/beta
40+
yield assert_equal, at.inputs.method, 'Joint[%.1f,%d]'.format(aprime, bprime)
41+
42+
43+
def test_JointFusion_radius():
44+
at = JointFusion()
45+
set_radius = lambda attr,x,y,z: setattr(at.inputs, attr, [x, y, z])
46+
for attr in ['patch_radius', 'search_radius']:
47+
for x in range(5):
48+
set_radius(attr, x, x + 1, x**x)
49+
yield assert_equal, at._format_arg(attr, None, getattr(at.inputs, attr))[4:], '{0}x{1}x{2}'.format(x, x + 1, x**x)
50+
51+
52+
def test_JointFusion_cmd():
53+
at = JointFusion()
54+
at.inputs.dimension = 3
55+
at.inputs.modalities = 1
56+
at.inputs.method = 'Joint[0.1,2]'
57+
at.inputs.output_label_image ='fusion_labelimage_output.nii'
58+
at.inputs.warped_intensity_images = ['im1.nii',
59+
'im2.nii']
60+
at.inputs.warped_label_images = ['segmentation0.nii.gz',
61+
'segmentation1.nii.gz']
62+
at.inputs.target_image = 'T1.nii'
63+
at.inputs.patch_radius = [3,2,1]
64+
at.inputs.search_radius = [1,2,3]
65+
yield assert_equal, at.cmdline, 'jointfusion 3 1' + \
66+
' -m Joint[0.1,2]' + \
67+
' -rp 3x2x1' + \
68+
' -rs 1x2x3' + \
69+
' -tg T1.nii' + \
70+
' -g im1.nii' + \
71+
' -g im2.nii' + \
72+
' -l segmentation0.nii.gz' + \
73+
' -l segmentation1.nii.gz' + \
74+
' fusion_labelimage_output.nii',
75+
"Command line generation failure", True
76+
# setting intensity or labels with unequal lengths raises error
77+
yield assert_raises, AssertionError, at._format_arg, 'warped_intensity_images', InputMultiPath, ['im1.nii', 'im2.nii', 'im3.nii']

0 commit comments

Comments
 (0)