Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
enh: add a few new utilities to ANTs
  • Loading branch information
oesteban committed Feb 27, 2020
commit 8165242e61d605029b52d2f1a0189e7270148e90
346 changes: 339 additions & 7 deletions nipype/interfaces/ants/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,341 @@
# -*- coding: utf-8 -*-
"""ANTS Apply Transforms interface
"""

"""ANTs' utilities."""
import os
from ...utils.imagemanip import copy_header as _copy_header
from ..base import traits, isdefined, TraitedSpec, File, Str, InputMultiPath
from .base import ANTSCommandInputSpec, ANTSCommand


class _ImageMathInputSpec(ANTSCommandInputSpec):
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
desc='dimension of output image')
output_image = File(position=2, argstr='%s', name_source=['op1'],
name_template='%s_maths', desc='output image file',
keep_extension=True)
operation = traits.Enum(
'm', 'vm', '+', 'v+', '-', 'v-', '/', '^', 'max', 'exp', 'addtozero',
'overadd', 'abs', 'total', 'mean', 'vtotal', 'Decision', 'Neg',
'Project', 'G', 'MD', 'ME', 'MO', 'MC', 'GD', 'GE', 'GO', 'GC',
mandatory=True, position=3, argstr='%s',
desc='mathematical operations')
op1 = File(exists=True, mandatory=True, position=-2, argstr='%s',
desc='first operator')
op2 = traits.Either(File(exists=True), Str, position=-1,
argstr='%s', desc='second operator')
copy_header = traits.Bool(
True, usedefault=True,
desc='copy headers of the original image into the output (corrected) file')


class _ImageMathOuputSpec(TraitedSpec):
output_image = File(exists=True, desc='output image file')


class ImageMath(ANTSCommand):
"""
Operations over images.

Example
-------
>>> ImageMath(
... op1='structural.nii',
... operation='+',
... op2='2').cmdline
'ImageMath 3 structural_maths.nii + structural.nii 2'

>>> ImageMath(
... op1='structural.nii',
... operation='Project',
... op2='1 2').cmdline
'ImageMath 3 structural_maths.nii Project structural.nii 1 2'

>>> ImageMath(
... op1='structural.nii',
... operation='G',
... op2='4').cmdline
'ImageMath 3 structural_maths.nii G structural.nii 4'

"""

_cmd = 'ImageMath'
input_spec = _ImageMathInputSpec
output_spec = _ImageMathOuputSpec

def _list_outputs(self):
outputs = super(ImageMath, self)._list_outputs()
if self.inputs.copy_header: # Fix headers
_copy_header(self.inputs.op1, outputs['output_image'],
keep_dtype=True)
return outputs


class _ResampleImageBySpacingInputSpec(ANTSCommandInputSpec):
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
desc='dimension of output image')
input_image = File(exists=True, mandatory=True, position=2, argstr='%s',
desc='input image file')
output_image = File(position=3, argstr='%s', name_source=['input_image'],
name_template='%s_resampled', desc='output image file',
keep_extension=True)
out_spacing = traits.Either(
traits.List(traits.Float, minlen=2, maxlen=3),
traits.Tuple(traits.Float, traits.Float, traits.Float),
traits.Tuple(traits.Float, traits.Float),
position=4, argstr='%s', mandatory=True, desc='output spacing'
)
apply_smoothing = traits.Bool(False, argstr='%d', position=5,
desc='smooth before resampling')
addvox = traits.Int(argstr='%d', position=6, requires=['apply_smoothing'],
desc='addvox pads each dimension by addvox')
nn_interp = traits.Bool(argstr='%d', desc='nn interpolation',
position=-1, requires=['addvox'])


class _ResampleImageBySpacingOutputSpec(TraitedSpec):
output_image = File(exists=True, desc='resampled file')


class ResampleImageBySpacing(ANTSCommand):
"""
Resample an image with a given spacing.

Examples
--------
>>> res = ResampleImageBySpacing(dimension=3)
>>> res.inputs.input_image = 'structural.nii'
>>> res.inputs.output_image = 'output.nii.gz'
>>> res.inputs.out_spacing = (4, 4, 4)
>>> res.cmdline #doctest: +ELLIPSIS
'ResampleImageBySpacing 3 structural.nii output.nii.gz 4 4 4'

>>> res = ResampleImageBySpacing(dimension=3)
>>> res.inputs.input_image = 'structural.nii'
>>> res.inputs.output_image = 'output.nii.gz'
>>> res.inputs.out_spacing = (4, 4, 4)
>>> res.inputs.apply_smoothing = True
>>> res.cmdline #doctest: +ELLIPSIS
'ResampleImageBySpacing 3 structural.nii output.nii.gz 4 4 4 1'

>>> res = ResampleImageBySpacing(dimension=3)
>>> res.inputs.input_image = 'structural.nii'
>>> res.inputs.output_image = 'output.nii.gz'
>>> res.inputs.out_spacing = (0.4, 0.4, 0.4)
>>> res.inputs.apply_smoothing = True
>>> res.inputs.addvox = 2
>>> res.inputs.nn_interp = False
>>> res.cmdline #doctest: +ELLIPSIS
'ResampleImageBySpacing 3 structural.nii output.nii.gz 0.4 0.4 0.4 1 2 0'

"""

_cmd = 'ResampleImageBySpacing'
input_spec = _ResampleImageBySpacingInputSpec
output_spec = _ResampleImageBySpacingOutputSpec

def _format_arg(self, name, trait_spec, value):
if name == 'out_spacing':
if len(value) != self.inputs.dimension:
raise ValueError('out_spacing dimensions should match dimension')

value = ' '.join(['%g' % d for d in value])

return super(ResampleImageBySpacing, self)._format_arg(
name, trait_spec, value)


class _ThresholdImageInputSpec(ANTSCommandInputSpec):
dimension = traits.Int(3, usedefault=True, position=1, argstr='%d',
desc='dimension of output image')
input_image = File(exists=True, mandatory=True, position=2, argstr='%s',
desc='input image file')
output_image = File(position=3, argstr='%s', name_source=['input_image'],
name_template='%s_resampled', desc='output image file',
keep_extension=True)

from ..base import TraitedSpec, File, traits, InputMultiPath
from .base import ANTSCommand, ANTSCommandInputSpec
mode = traits.Enum('Otsu', 'Kmeans', argstr='%s', position=4,
requires=['num_thresholds'], xor=['th_low', 'th_high'],
desc='whether to run Otsu / Kmeans thresholding')
num_thresholds = traits.Int(position=5, argstr='%d',
desc='number of thresholds')
input_mask = File(exists=True, requires=['num_thresholds'], argstr='%s',
desc='input mask for Otsu, Kmeans')

th_low = traits.Float(position=4, argstr='%f', xor=['mode'],
desc='lower threshold')
th_high = traits.Float(position=5, argstr='%f', xor=['mode'],
desc='upper threshold')
inside_value = traits.Float(1, position=6, argstr='%f', requires=['th_low'],
desc='inside value')
outside_value = traits.Float(0, position=7, argstr='%f', requires=['th_low'],
desc='outside value')
copy_header = traits.Bool(
True, mandatory=True, usedefault=True,
desc='copy headers of the original image into the output (corrected) file')


class _ThresholdImageOutputSpec(TraitedSpec):
output_image = File(exists=True, desc='resampled file')


class ThresholdImage(ANTSCommand):
"""
Apply thresholds on images.

Examples
--------
>>> thres = ThresholdImage(dimension=3)
>>> thres.inputs.input_image = 'structural.nii'
>>> thres.inputs.output_image = 'output.nii.gz'
>>> thres.inputs.th_low = 0.5
>>> thres.inputs.th_high = 1.0
>>> thres.inputs.inside_value = 1.0
>>> thres.inputs.outside_value = 0.0
>>> thres.cmdline #doctest: +ELLIPSIS
'ThresholdImage 3 structural.nii output.nii.gz 0.500000 1.000000 1.000000 0.000000'

>>> thres = ThresholdImage(dimension=3)
>>> thres.inputs.input_image = 'structural.nii'
>>> thres.inputs.output_image = 'output.nii.gz'
>>> thres.inputs.mode = 'Kmeans'
>>> thres.inputs.num_thresholds = 4
>>> thres.cmdline #doctest: +ELLIPSIS
'ThresholdImage 3 structural.nii output.nii.gz Kmeans 4'

"""

_cmd = 'ThresholdImage'
input_spec = _ThresholdImageInputSpec
output_spec = _ThresholdImageOutputSpec

def _list_outputs(self):
outputs = super(ThresholdImage, self)._list_outputs()
if self.inputs.copy_header: # Fix headers
_copy_header(self.inputs.input_image, outputs['output_image'],
keep_dtype=True)
return outputs


class _AIInputSpec(ANTSCommandInputSpec):
dimension = traits.Enum(3, 2, usedefault=True, argstr='-d %d',
desc='dimension of output image')
verbose = traits.Bool(False, usedefault=True, argstr='-v %d',
desc='enable verbosity')

fixed_image = File(
exists=True, mandatory=True,
desc='Image to which the moving_image should be transformed')
moving_image = File(
exists=True, mandatory=True,
desc='Image that will be transformed to fixed_image')

fixed_image_mask = File(
exists=True, argstr='-x %s', desc='fixed mage mask')
moving_image_mask = File(
exists=True, requires=['fixed_image_mask'],
desc='moving mage mask')

metric_trait = (
traits.Enum("Mattes", "GC", "MI"),
traits.Int(32),
traits.Enum('Regular', 'Random', 'None'),
traits.Range(value=0.2, low=0.0, high=1.0)
)
metric = traits.Tuple(*metric_trait, argstr='-m %s', mandatory=True,
desc='the metric(s) to use.')

transform = traits.Tuple(
traits.Enum('Affine', 'Rigid', 'Similarity'),
traits.Range(value=0.1, low=0.0, exclude_low=True),
argstr='-t %s[%g]', usedefault=True,
desc='Several transform options are available')

principal_axes = traits.Bool(False, usedefault=True, argstr='-p %d', xor=['blobs'],
desc='align using principal axes')
search_factor = traits.Tuple(
traits.Float(20), traits.Range(value=0.12, low=0.0, high=1.0),
usedefault=True, argstr='-s [%g,%g]', desc='search factor')

search_grid = traits.Either(
traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float, traits.Float)),
traits.Tuple(traits.Float, traits.Tuple(traits.Float, traits.Float)),
argstr='-g %s', desc='Translation search grid in mm')

convergence = traits.Tuple(
traits.Range(low=1, high=10000, value=10),
traits.Float(1e-6),
traits.Range(low=1, high=100, value=10),
usedefault=True, argstr='-c [%d,%g,%d]', desc='convergence')

output_transform = File(
'initialization.mat', usedefault=True, argstr='-o %s',
desc='output file name')


class _AIOuputSpec(TraitedSpec):
output_transform = File(exists=True, desc='output file name')


class AI(ANTSCommand):
"""
Calculate the optimal linear transform parameters for aligning two images.

Examples
--------
>>> AI(
... fixed_image='structural.nii',
... moving_image='epi.nii',
... metric=('Mattes', 32, 'Regular', 1),
... ).cmdline
'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1]
-o initialization.mat -p 0 -s [20,0.12] -t Affine[0.1] -v 0'

>>> AI(
... fixed_image='structural.nii',
... moving_image='epi.nii',
... metric=('Mattes', 32, 'Regular', 1),
... search_grid=(12, (1, 1, 1)),
... ).cmdline
'antsAI -c [10,1e-06,10] -d 3 -m Mattes[structural.nii,epi.nii,32,Regular,1]
-o initialization.mat -p 0 -s [20,0.12] -g [12.0,1x1x1] -t Affine[0.1] -v 0'

"""

_cmd = 'antsAI'
input_spec = _AIInputSpec
output_spec = _AIOuputSpec

def _run_interface(self, runtime, correct_return_codes=(0, )):
runtime = super(AI, self)._run_interface(
runtime, correct_return_codes)

setattr(self, '_output', {
'output_transform': os.path.join(
runtime.cwd,
os.path.basename(self.inputs.output_transform))
})
return runtime

def _format_arg(self, opt, spec, val):
if opt == 'metric':
val = '%s[{fixed_image},{moving_image},%d,%s,%g]' % val
val = val.format(
fixed_image=self.inputs.fixed_image,
moving_image=self.inputs.moving_image)
return spec.argstr % val

if opt == 'search_grid':
val1 = 'x'.join(['%g' % v for v in val[1]])
fmtval = '[%s]' % ','.join([str(val[0]), val1])
return spec.argstr % fmtval

if opt == 'fixed_image_mask':
if isdefined(self.inputs.moving_image_mask):
return spec.argstr % ('[%s,%s]' % (
val, self.inputs.moving_image_mask))

return super(AI, self)._format_arg(opt, spec, val)

def _list_outputs(self):
return getattr(self, '_output')


class AverageAffineTransformInputSpec(ANTSCommandInputSpec):
Expand Down Expand Up @@ -42,6 +372,7 @@ class AverageAffineTransform(ANTSCommand):
>>> avg.inputs.output_affine_transform = 'MYtemplatewarp.mat'
>>> avg.cmdline
'AverageAffineTransform 3 MYtemplatewarp.mat trans.mat func_to_struct.mat'

"""

_cmd = "AverageAffineTransform"
Expand Down Expand Up @@ -343,7 +674,8 @@ class ComposeMultiTransform(ANTSCommand):
>>> compose_transform.inputs.dimension = 3
>>> compose_transform.inputs.transforms = ['struct_to_template.mat', 'func_to_struct.mat']
>>> compose_transform.cmdline
'ComposeMultiTransform 3 struct_to_template_composed.mat struct_to_template.mat func_to_struct.mat'
'ComposeMultiTransform 3 struct_to_template_composed.mat
struct_to_template.mat func_to_struct.mat'

"""

Expand Down
18 changes: 18 additions & 0 deletions nipype/utils/imagemanip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Image manipulation utilities (mostly, NiBabel manipulations)."""
import nibabel as nb


def copy_header(header_file, in_file, keep_dtype=True):
"""Copy header from a reference image onto another image."""
hdr_img = nb.load(header_file)
out_img = nb.load(in_file, mmap=False)
hdr = hdr_img.header.copy()
if keep_dtype:
hdr.set_data_dtype(out_img.get_data_dtype())

new_img = out_img.__class__(out_img.dataobj, None, hdr)
if not keep_dtype:
new_img.set_data_dtype(hdr_img.get_data_dtype())

new_img.to_filename(in_file)
return in_file
Loading