Skip to content

Commit 8165242

Browse files
committed
enh: add a few new utilities to ANTs
1 parent 909301e commit 8165242

File tree

3 files changed

+390
-7
lines changed

3 files changed

+390
-7
lines changed

nipype/interfaces/ants/utils.py

Lines changed: 339 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,341 @@
1-
# -*- coding: utf-8 -*-
2-
"""ANTS Apply Transforms interface
3-
"""
4-
1+
"""ANTs' utilities."""
52
import os
3+
from ...utils.imagemanip import copy_header as _copy_header
4+
from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiPath
5+
from .base import ANTSCommandInputSpec, ANTSCommand
6+
7+
8+
class _ImageMathInputSpec(ANTSCommandInputSpec):
9+
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
10+
desc='dimension of output image')
11+
output_image = File(position=2, argstr='%s', name_source=['op1'],
12+
name_template='%s_maths', desc='output image file',
13+
keep_extension=True)
14+
operation = traits.Enum(
15+
'm', 'vm', '+', 'v+', '-', 'v-', '/', '^', 'max', 'exp', 'addtozero',
16+
'overadd', 'abs', 'total', 'mean', 'vtotal', 'Decision', 'Neg',
17+
'Project', 'G', 'MD', 'ME', 'MO', 'MC', 'GD', 'GE', 'GO', 'GC',
18+
mandatory=True, position=3, argstr='%s',
19+
desc='mathematical operations')
20+
op1 = File(exists=True, mandatory=True, position=-2, argstr='%s',
21+
desc='first operator')
22+
op2 = traits.Either(File(exists=True), Str, position=-1,
23+
argstr='%s', desc='second operator')
24+
copy_header = traits.Bool(
25+
True, usedefault=True,
26+
desc='copy headers of the original image into the output (corrected) file')
27+
28+
29+
class _ImageMathOuputSpec(TraitedSpec):
30+
output_image = File(exists=True, desc='output image file')
31+
32+
33+
class ImageMath(ANTSCommand):
34+
"""
35+
Operations over images.
36+
37+
Example
38+
-------
39+
>>> ImageMath(
40+
... op1='structural.nii',
41+
... operation='+',
42+
... op2='2').cmdline
43+
'ImageMath 3 structural_maths.nii + structural.nii 2'
44+
45+
>>> ImageMath(
46+
... op1='structural.nii',
47+
... operation='Project',
48+
... op2='1 2').cmdline
49+
'ImageMath 3 structural_maths.nii Project structural.nii 1 2'
50+
51+
>>> ImageMath(
52+
... op1='structural.nii',
53+
... operation='G',
54+
... op2='4').cmdline
55+
'ImageMath 3 structural_maths.nii G structural.nii 4'
56+
57+
"""
58+
59+
_cmd = 'ImageMath'
60+
input_spec = _ImageMathInputSpec
61+
output_spec = _ImageMathOuputSpec
62+
63+
def _list_outputs(self):
64+
outputs = super(ImageMath, self)._list_outputs()
65+
if self.inputs.copy_header: # Fix headers
66+
_copy_header(self.inputs.op1, outputs['output_image'],
67+
keep_dtype=True)
68+
return outputs
69+
70+
71+
class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec):
72+
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
73+
desc='dimension of output image')
74+
input_image = File(exists=True, mandatory=True, position=2, argstr='%s',
75+
desc='input image file')
76+
output_image = File(position=3, argstr='%s', name_source=['input_image'],
77+
name_template='%s_resampled', desc='output image file',
78+
keep_extension=True)
79+
out_spacing = traits.Either(
80+
traits.List(traits.Float, minlen=2, maxlen=3),
81+
traits.Tuple(traits.Float, traits.Float, traits.Float),
82+
traits.Tuple(traits.Float, traits.Float),
83+
position=4, argstr='%s', mandatory=True, desc='output spacing'
84+
)
85+
apply_smoothing = traits.Bool(False, argstr='%d', position=5,
86+
desc='smooth before resampling')
87+
addvox = traits.Int(argstr='%d', position=6, requires=['apply_smoothing'],
88+
desc='addvox pads each dimension by addvox')
89+
nn_interp = traits.Bool(argstr='%d', desc='nn interpolation',
90+
position=-1, requires=['addvox'])
91+
92+
93+
class _ResampleImageBySpacingOutputSpec(TraitedSpec):
94+
output_image = File(exists=True, desc='resampled file')
95+
96+
97+
class ResampleImageBySpacing(ANTSCommand):
98+
"""
99+
Resample an image with a given spacing.
100+
101+
Examples
102+
--------
103+
>>> res = ResampleImageBySpacing(dimension=3)
104+
>>> res.inputs.input_image = 'structural.nii'
105+
>>> res.inputs.output_image = 'output.nii.gz'
106+
>>> res.inputs.out_spacing = (4, 4, 4)
107+
>>> res.cmdline #doctest: +ELLIPSIS
108+
'ResampleImageBySpacing 3 structural.nii output.nii.gz 4 4 4'
109+
110+
>>> res = ResampleImageBySpacing(dimension=3)
111+
>>> res.inputs.input_image = 'structural.nii'
112+
>>> res.inputs.output_image = 'output.nii.gz'
113+
>>> res.inputs.out_spacing = (4, 4, 4)
114+
>>> res.inputs.apply_smoothing = True
115+
>>> res.cmdline #doctest: +ELLIPSIS
116+
'ResampleImageBySpacing 3 structural.nii output.nii.gz 4 4 4 1'
117+
118+
>>> res = ResampleImageBySpacing(dimension=3)
119+
>>> res.inputs.input_image = 'structural.nii'
120+
>>> res.inputs.output_image = 'output.nii.gz'
121+
>>> res.inputs.out_spacing = (0.4, 0.4, 0.4)
122+
>>> res.inputs.apply_smoothing = True
123+
>>> res.inputs.addvox = 2
124+
>>> res.inputs.nn_interp = False
125+
>>> res.cmdline #doctest: +ELLIPSIS
126+
'ResampleImageBySpacing 3 structural.nii output.nii.gz 0.4 0.4 0.4 1 2 0'
127+
128+
"""
129+
130+
_cmd = 'ResampleImageBySpacing'
131+
input_spec = _ResampleImageBySpacingInputSpec
132+
output_spec = _ResampleImageBySpacingOutputSpec
133+
134+
def _format_arg(self, name, trait_spec, value):
135+
if name == 'out_spacing':
136+
if len(value) != self.inputs.dimension:
137+
raise ValueError('out_spacing dimensions should match dimension')
138+
139+
value = ' '.join(['%g' % d for d in value])
140+
141+
return super(ResampleImageBySpacing, self)._format_arg(
142+
name, trait_spec, value)
143+
144+
145+
class _ThresholdImageInputSpec(ANTSCommandInputSpec):
146+
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
147+
desc='dimension of output image')
148+
input_image = File(exists=True, mandatory=True, position=2, argstr='%s',
149+
desc='input image file')
150+
output_image = File(position=3, argstr='%s', name_source=['input_image'],
151+
name_template='%s_resampled', desc='output image file',
152+
keep_extension=True)
6153

7-
from ..base import TraitedSpec, File, traits, InputMultiPath
8-
from .base import ANTSCommand, ANTSCommandInputSpec
154+
mode = traits.Enum('Otsu', 'Kmeans', argstr='%s', position=4,
155+
requires=['num_thresholds'], xor=['th_low', 'th_high'],
156+
desc='whether to run Otsu / Kmeans thresholding')
157+
num_thresholds = traits.Int(position=5, argstr='%d',
158+
desc='number of thresholds')
159+
input_mask = File(exists=True, requires=['num_thresholds'], argstr='%s',
160+
desc='input mask for Otsu, Kmeans')
161+
162+
th_low = traits.Float(position=4, argstr='%f', xor=['mode'],
163+
desc='lower threshold')
164+
th_high = traits.Float(position=5, argstr='%f', xor=['mode'],
165+
desc='upper threshold')
166+
inside_value = traits.Float(1, position=6, argstr='%f', requires=['th_low'],
167+
desc='inside value')
168+
outside_value = traits.Float(0, position=7, argstr='%f', requires=['th_low'],
169+
desc='outside value')
170+
copy_header = traits.Bool(
171+
True, mandatory=True, usedefault=True,
172+
desc='copy headers of the original image into the output (corrected) file')
173+
174+
175+
class _ThresholdImageOutputSpec(TraitedSpec):
176+
output_image = File(exists=True, desc='resampled file')
177+
178+
179+
class ThresholdImage(ANTSCommand):
180+
"""
181+
Apply thresholds on images.
182+
183+
Examples
184+
--------
185+
>>> thres = ThresholdImage(dimension=3)
186+
>>> thres.inputs.input_image = 'structural.nii'
187+
>>> thres.inputs.output_image = 'output.nii.gz'
188+
>>> thres.inputs.th_low = 0.5
189+
>>> thres.inputs.th_high = 1.0
190+
>>> thres.inputs.inside_value = 1.0
191+
>>> thres.inputs.outside_value = 0.0
192+
>>> thres.cmdline #doctest: +ELLIPSIS
193+
'ThresholdImage 3 structural.nii output.nii.gz 0.500000 1.000000 1.000000 0.000000'
194+
195+
>>> thres = ThresholdImage(dimension=3)
196+
>>> thres.inputs.input_image = 'structural.nii'
197+
>>> thres.inputs.output_image = 'output.nii.gz'
198+
>>> thres.inputs.mode = 'Kmeans'
199+
>>> thres.inputs.num_thresholds = 4
200+
>>> thres.cmdline #doctest: +ELLIPSIS
201+
'ThresholdImage 3 structural.nii output.nii.gz Kmeans 4'
202+
203+
"""
204+
205+
_cmd = 'ThresholdImage'
206+
input_spec = _ThresholdImageInputSpec
207+
output_spec = _ThresholdImageOutputSpec
208+
209+
def _list_outputs(self):
210+
outputs = super(ThresholdImage, self)._list_outputs()
211+
if self.inputs.copy_header: # Fix headers
212+
_copy_header(self.inputs.input_image, outputs['output_image'],
213+
keep_dtype=True)
214+
return outputs
215+
216+
217+
class _AIInputSpec(ANTSCommandInputSpec):
218+
dimension = traits.Enum(3, 2, usedefault=True, argstr='-d %d',
219+
desc='dimension of output image')
220+
verbose = traits.Bool(False, usedefault=True, argstr='-v %d',
221+
desc='enable verbosity')
222+
223+
fixed_image = File(
224+
exists=True, mandatory=True,
225+
desc='Image to which the moving_image should be transformed')
226+
moving_image = File(
227+
exists=True, mandatory=True,
228+
desc='Image that will be transformed to fixed_image')
229+
230+
fixed_image_mask = File(
231+
exists=True, argstr='-x %s', desc='fixed mage mask')
232+
moving_image_mask = File(
233+
exists=True, requires=['fixed_image_mask'],
234+
desc='moving mage mask')
235+
236+
metric_trait = (
237+
traits.Enum("Mattes", "GC", "MI"),
238+
traits.Int(32),
239+
traits.Enum('Regular', 'Random', 'None'),
240+
traits.Range(value=0.2, low=0.0, high=1.0)
241+
)
242+
metric = traits.Tuple(*metric_trait, argstr='-m %s', mandatory=True,
243+
desc='the metric(s) to use.')
244+
245+
transform = traits.Tuple(
246+
traits.Enum('Affine', 'Rigid', 'Similarity'),
247+
traits.Range(value=0.1, low=0.0, exclude_low=True),
248+
argstr='-t %s[%g]', usedefault=True,
249+
desc='Several transform options are available')
250+
251+
principal_axes = traits.Bool(False, usedefault=True, argstr='-p %d', xor=['blobs'],
252+
desc='align using principal axes')
253+
search_factor = traits.Tuple(
254+
traits.Float(20), traits.Range(value=0.12, low=0.0, high=1.0),
255+
usedefault=True, argstr='-s [%g,%g]', desc='search factor')
256+
257+
search_grid = traits.Either(
258+
traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float, traits.Float)),
259+
traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float)),
260+
argstr='-g %s', desc='Translation search grid in mm')
261+
262+
convergence = traits.Tuple(
263+
traits.Range(low=1, high=10000, value=10),
264+
traits.Float(1e-6),
265+
traits.Range(low=1, high=100, value=10),
266+
usedefault=True, argstr='-c [%d,%g,%d]', desc='convergence')
267+
268+
output_transform = File(
269+
'initialization.mat', usedefault=True, argstr='-o %s',
270+
desc='output file name')
271+
272+
273+
class _AIOuputSpec(TraitedSpec):
274+
output_transform = File(exists=True, desc='output file name')
275+
276+
277+
class AI(ANTSCommand):
278+
"""
279+
Calculate the optimal linear transform parameters for aligning two images.
280+
281+
Examples
282+
--------
283+
>>> AI(
284+
... fixed_image='structural.nii',
285+
... moving_image='epi.nii',
286+
... metric=('Mattes', 32, 'Regular', 1),
287+
... ).cmdline
288+
'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1]
289+
-o initialization.mat -p 0 -s [20,0.12] -t Affine[0.1] -v 0'
290+
291+
>>> AI(
292+
... fixed_image='structural.nii',
293+
... moving_image='epi.nii',
294+
... metric=('Mattes', 32, 'Regular', 1),
295+
... search_grid=(12, (1, 1, 1)),
296+
... ).cmdline
297+
'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1]
298+
-o initialization.mat -p 0 -s [20,0.12] -g [12.0,1x1x1] -t Affine[0.1] -v 0'
299+
300+
"""
301+
302+
_cmd = 'antsAI'
303+
input_spec = _AIInputSpec
304+
output_spec = _AIOuputSpec
305+
306+
def _run_interface(self, runtime, correct_return_codes=(0, )):
307+
runtime = super(AI, self)._run_interface(
308+
runtime, correct_return_codes)
309+
310+
setattr(self, '_output', {
311+
'output_transform': os.path.join(
312+
runtime.cwd,
313+
os.path.basename(self.inputs.output_transform))
314+
})
315+
return runtime
316+
317+
def _format_arg(self, opt, spec, val):
318+
if opt == 'metric':
319+
val = '%s[{fixed_image},{moving_image},%d,%s,%g]' % val
320+
val = val.format(
321+
fixed_image=self.inputs.fixed_image,
322+
moving_image=self.inputs.moving_image)
323+
return spec.argstr % val
324+
325+
if opt == 'search_grid':
326+
val1 = 'x'.join(['%g' % v for v in val[1]])
327+
fmtval = '[%s]' % ','.join([str(val[0]), val1])
328+
return spec.argstr % fmtval
329+
330+
if opt == 'fixed_image_mask':
331+
if isdefined(self.inputs.moving_image_mask):
332+
return spec.argstr % ('[%s,%s]' % (
333+
val, self.inputs.moving_image_mask))
334+
335+
return super(AI, self)._format_arg(opt, spec, val)
336+
337+
def _list_outputs(self):
338+
return getattr(self, '_output')
9339

10340

11341
class AverageAffineTransformInputSpec(ANTSCommandInputSpec):
@@ -42,6 +372,7 @@ class AverageAffineTransform(ANTSCommand):
42372
>>> avg.inputs.output_affine_transform = 'MYtemplatewarp.mat'
43373
>>> avg.cmdline
44374
'AverageAffineTransform 3 MYtemplatewarp.mat trans.mat func_to_struct.mat'
375+
45376
"""
46377

47378
_cmd = "AverageAffineTransform"
@@ -343,7 +674,8 @@ class ComposeMultiTransform(ANTSCommand):
343674
>>> compose_transform.inputs.dimension = 3
344675
>>> compose_transform.inputs.transforms = ['struct_to_template.mat', 'func_to_struct.mat']
345676
>>> compose_transform.cmdline
346-
'ComposeMultiTransform 3 struct_to_template_composed.mat struct_to_template.mat func_to_struct.mat'
677+
'ComposeMultiTransform 3 struct_to_template_composed.mat
678+
struct_to_template.mat func_to_struct.mat'
347679
348680
"""
349681

nipype/utils/imagemanip.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Image manipulation utilities (mostly, NiBabel manipulations)."""
2+
import nibabel as nb
3+
4+
5+
def copy_header(header_file, in_file, keep_dtype=True):
6+
"""Copy header from a reference image onto another image."""
7+
hdr_img = nb.load(header_file)
8+
out_img = nb.load(in_file, mmap=False)
9+
hdr = hdr_img.header.copy()
10+
if keep_dtype:
11+
hdr.set_data_dtype(out_img.get_data_dtype())
12+
13+
new_img = out_img.__class__(out_img.dataobj, None, hdr)
14+
if not keep_dtype:
15+
new_img.set_data_dtype(hdr_img.get_data_dtype())
16+
17+
new_img.to_filename(in_file)
18+
return in_file

0 commit comments

Comments
 (0)