This section is meant for the more advanced user. In it we will discuss how you can create your own interface, i.e. wrapping your own code, so that you can use it with Nipype.
In this notebook we will show you:
But before we can start, let's recap again the difference between interfaces and workflows.
Interfaces are the building blocks that solve well-defined tasks. We solve more complex tasks by combining interfaces with workflows:
Interfaces | Workflows |
---|---|
Wrap *unitary* tasks | Wrap *meta*-tasks
|
Keep track of the inputs and outputs, and check their expected types | Do not have inputs/outputs, but expose them from the interfaces wrapped inside |
Do not cache results (unless you use [interface caching](advanced_interfaces_caching.ipynb)) | Cache results |
Run by a nipype plugin | Run by a nipype plugin |
For this notebook, we'll work on the following T1-weighted dataset located in /data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz
:
from nilearn.plotting import plot_anat
%matplotlib inline
plot_anat('/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz', dim=-1);
BET
¶Nipype offers a series of Python interfaces to various external packages (e.g. FSL, SPM or FreeSurfer) even if they themselves are written in programming languages other than python. Such interfaces know what sort of options their corresponding tool has and how to execute it.
To illustrate why interfaces are so useful, let's have a look at the brain extraction algorithm BET from FSL. Once in its original framework and once in the Nipype framework.
The tool can be run directly in a bash shell using the following command line:
%%bash
bet /data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz \
/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w_bet.nii.gz
... which yields the following:
plot_anat('/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w_bet.nii.gz', dim=-1);
Using nipype, the equivalent is a bit more verbose:
from nipype.interfaces.fsl import BET
skullstrip = BET(in_file='/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz')
res = skullstrip.run()
print(res.outputs.out_file)
Now we can verify that the result is exactly the same as before. Please note that, since we are using a Python environment, we use the result of the execution to point our plot_anat
function to the output image of running BET:
plot_anat(res.outputs.out_file, dim=-1);
Nipype is designed to ease writing interfaces for new software. Nipype interfaces are designed with three elements that are intuitive:
InputSpec
)OutputSpec
)run()
method we've seen before for BET, and which puts together inputs and outputs.from nipype.interfaces.base import CommandLine
CommandLine.help()
As a quick example, let's wrap bash's ls
with Nipype:
nipype_ls = CommandLine('ls', args='-lh', terminal_output='allatonce')
Now, we have a Python object nipype_ls
that is a runnable nipype interface. After execution, Nipype interface returns a result object. We can retrieve the output of our ls
invocation from the result.runtime
property:
result = nipype_ls.run()
print(result.runtime.stdout)
%%bash
antsTransformInfo /home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm
antsTransformInfo
.For the first item of this roadmap, we will just need to derive a new Python class from the nipype.interfaces.base.CommandLine
base. To indicate the appropriate command line, we set the member _cmd
:
class TransformInfo(CommandLine):
_cmd = 'antsTransformInfo'
This is enough to have a nipype compatible interface for this tool:
TransformInfo.help()
However, the args
argument is too generic and does not deviate much from just running it in bash, or directly using subprocess.Popen
. Let's define the inputs specification for the interface, extending the nipype.interfaces.base.CommandLineInputSpec
class.
The inputs are implemented using the Enthought traits package. For now, we'll use the File
trait extension of nipype:
from nipype.interfaces.base import CommandLineInputSpec, File
class TransformInfoInputSpec(CommandLineInputSpec):
in_file = File(exists=True, mandatory=True, argstr='%s',
position=0, desc='the input transform file')
Some settings are done for this File
object:
exists=True
indicates Nipype that the file must exist when it is setmandatory=True
checks that this input was set before running because the program would crash otherwiseargstr='%s'
indicates how this input parameter should be formattedposition=0
indicates that this is the first positional argumentWe can now decorate our TransformInfo
core class with its input, by setting the input_spec
member:
class TransformInfo(CommandLine):
_cmd = 'antsTransformInfo'
input_spec = TransformInfoInputSpec
Our interface now has one mandatory input, and inherits some optional inputs from the CommandLineInputSpec
:
TransformInfo.help()
One interesting feature of the Nipype interface is that the underlying command line can be checked using the object property cmdline
. The command line can only be built when the mandatory inputs are set, so let's instantiate our new Interface for the first time, and check the underlying command line:
my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm')
print(my_info_interface.cmdline)
Nipype will make sure that the parameters fulfill their prescribed attributes. For instance, in_file
is mandatory. An error is issued if we build the command line or try to run this interface without it:
try:
TransformInfo().cmdline
except(ValueError) as err:
print('It crashed with...')
print("ValueError:", err)
else:
raise
It will also complain if we try to set a non-existent file:
try:
my_info_interface.inputs.in_file = 'idontexist.tfm'
except(Exception) as err:
print('It crashed with...')
print("TraitError:", err)
else:
raise
The outputs are defined in a similar way. Let's define a custom output for our interface which is a list of three float element. The output traits are derived from a simpler base class called TraitedSpec
. We also import the two data representations we need List
and Float
:
from nipype.interfaces.base import TraitedSpec, traits
class TransformInfoOutputSpec(TraitedSpec):
translation = traits.List(traits.Float, desc='the translation component of the input transform')
class TransformInfo(CommandLine):
_cmd = 'antsTransformInfo'
input_spec = TransformInfoInputSpec
output_spec = TransformInfoOutputSpec
And now, our new output is in place:
TransformInfo.help()
If we run the interface, we'll be able to see that this tool only writes some text to the standard output, but we just want to extract the Translation
field and generate a Python object from it.
my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm',
terminal_output='allatonce')
result = my_info_interface.run()
print(result.runtime.stdout)
We need to complete the functionality of the run()
member of our interface to parse the standard output. This is done extending its _run_interface()
member.
When we define outputs, generally they need to be explicitly wired in the _list_outputs()
member of the core class. Let's see how we can complete those:
class TransformInfo(CommandLine):
_cmd = 'antsTransformInfo'
input_spec = TransformInfoInputSpec
output_spec = TransformInfoOutputSpec
def _run_interface(self, runtime):
import re
# Run the command line as a natural CommandLine interface
runtime = super(TransformInfo, self)._run_interface(runtime)
# Search transform in the standard output
expr_tra = re.compile('Translation:\s+\[(?P<translation>[0-9\.-]+,\s[0-9\.-]+,\s[0-9\.-]+)\]')
trans = [float(v) for v in expr_tra.search(runtime.stdout).group('translation').split(', ')]
# Save it for later use in _list_outputs
setattr(self, '_result', trans)
# Good to go
return runtime
def _list_outputs(self):
# Get the attribute saved during _run_interface
return {'translation': getattr(self, '_result')}
Let's run this interface (we set terminal_output='allatonce'
to reduce the length of this manual, default would otherwise be 'stream'
):
my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm',
terminal_output='allatonce')
result = my_info_interface.run()
Now we can retrieve our outcome of interest as an output:
result.outputs.translation
CommandLine
interface¶Now putting it all togehter, it looks as follows:
from nipype.interfaces.base import (CommandLine, CommandLineInputSpec,
TraitedSpec, traits, File)
class TransformInfoInputSpec(CommandLineInputSpec):
in_file = File(exists=True, mandatory=True, argstr='%s', position=0,
desc='the input transform file')
class TransformInfoOutputSpec(TraitedSpec):
translation = traits.List(traits.Float, desc='the translation component of the input transform')
class TransformInfo(CommandLine):
_cmd = 'antsTransformInfo'
input_spec = TransformInfoInputSpec
output_spec = TransformInfoOutputSpec
def _run_interface(self, runtime):
import re
# Run the command line as a natural CommandLine interface
runtime = super(TransformInfo, self)._run_interface(runtime)
# Search transform in the standard output
expr_tra = re.compile('Translation:\s+\[(?P<translation>[0-9\.-]+,\s[0-9\.-]+,\s[0-9\.-]+)\]')
trans = [float(v) for v in expr_tra.search(runtime.stdout).group('translation').split(', ')]
# Save it for later use in _list_outputs
setattr(self, '_result', trans)
# Good to go
return runtime
def _list_outputs(self):
# Get the attribute saved during _run_interface
return {'translation': getattr(self, '_result')}
my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm',
terminal_output='allatonce')
result = my_info_interface.run()
result.outputs.translation
CommandLine
wrapper¶For more standard neuroimaging software, generally we will just have to specify simple flags, i.e. input and output images and some additional parameters. If that is the case, then there is no need to extend the run()
method.
Let's look at a quick, partial, implementation of FSL's BET:
from nipype.interfaces.base import CommandLineInputSpec, File, TraitedSpec
class CustomBETInputSpec(CommandLineInputSpec):
in_file = File(exists=True, mandatory=True, argstr='%s', position=0, desc='the input image')
mask = traits.Bool(mandatory=False, argstr='-m', position=2, desc='create binary mask image')
# Do not set exists=True for output files!
out_file = File(mandatory=True, argstr='%s', position=1, desc='the output image')
class CustomBETOutputSpec(TraitedSpec):
out_file = File(desc='the output image')
mask_file = File(desc="path/name of binary brain mask (if generated)")
class CustomBET(CommandLine):
_cmd = 'bet'
input_spec = CustomBETInputSpec
output_spec = CustomBETOutputSpec
def _list_outputs(self):
# Get the attribute saved during _run_interface
return {'out_file': self.inputs.out_file,
'mask_file': self.inputs.out_file.replace('brain', 'brain_mask')}
my_custom_bet = CustomBET()
my_custom_bet.inputs.in_file = '/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz'
my_custom_bet.inputs.out_file = 'sub-01_T1w_brain.nii.gz'
my_custom_bet.inputs.mask = True
result = my_custom_bet.run()
plot_anat(result.outputs.out_file, dim=-1);
Python
interface¶CommandLine
interface is great, but my tool is already in Python - can I wrap it natively?
Sure. Let's solve the following problem: Let's say we have a Python function that takes an input image and a list of three translations (x, y, z) in mm, and then writes a resampled image after the translation has been applied:
def translate_image(img, translation, out_file):
import nibabel as nb
import numpy as np
from scipy.ndimage.interpolation import affine_transform
# Load the data
nii = nb.load(img)
data = nii.get_data()
# Create the transformation matrix
matrix = np.eye(3)
trans = (np.array(translation) / nii.header.get_zooms()[:3]) * np.array([1.0, -1.0, -1.0])
# Apply the transformation matrix
newdata = affine_transform(data, matrix=matrix, offset=trans)
# Save the new data in a new NIfTI image
nb.Nifti1Image(newdata, nii.affine, nii.header).to_filename(out_file)
print('Translated file now is here: %s' % out_file)
Let's see how this function operates:
orig_image = '/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz'
translation = [20.0, -20.0, -20.0]
translated_image = 'translated.nii.gz'
# Let's run the translate_image function on our inputs
translate_image(orig_image,
translation,
translated_image)
Now that the function was executed, let's plot the original and the translated image.
plot_anat(orig_image, dim=-1);
plot_anat('translated.nii.gz', dim=-1);