Create interfaces

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:

  1. Example of an already implemented interface
  2. What are the main parts of a Nipype interface?
  3. How to wrap a CommandLine interface?
  4. How to wrap a Python interface?
  5. How to wrap a MATLAB interface?

But before we can start, let's recap again the difference between interfaces and workflows.

Interfaces vs. 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
  • implemented with nipype interfaces wrapped inside ``Node`` objects
  • subworkflows can also be added to a workflow without any wrapping
  • 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

    Example of an already implemented interface

    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:

    In [ ]:
    from nilearn.plotting import plot_anat
    %matplotlib inline
    
    In [ ]:
    plot_anat('/data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz', dim=-1);
    

    Example of interface: FSL's 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:

    In [ ]:
    %%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:

    In [ ]:
    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:

    • line 1: The first line imports the interface
    • line 2: Then, the interface is instantiated. We provide here the input file.
    • line 3: Finally, we run the interface
    • line 4: The output file name can be automatically handled by nipype, and we will use that feature here
    In [ ]:
    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)
    
    /home/neuro/nipype_tutorial/notebooks/sub-01_ses-test_T1w_brain.nii.gz
    

    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:

    In [ ]:
    plot_anat(res.outputs.out_file, dim=-1);
    

    What are the main parts of a Nipype interface?

    Nipype is designed to ease writing interfaces for new software. Nipype interfaces are designed with three elements that are intuitive:

    • A specification of inputs (or the InputSpec)
    • A specification of outputs (or the OutputSpec)
    • An interface core which implements the run() method we've seen before for BET, and which puts together inputs and outputs.

    The CommandLine interface

    A quick example

    The easiest and quickest way to run any command line is the CommandLine interface, which has a very simple specification of inputs ready to use:

    In [ ]:
    from nipype.interfaces.base import CommandLine
    CommandLine.help()
    
    Wraps command **None**
    
    Implements functionality to interact with command line programs
    class must be instantiated with a command argument
    
    Parameters
    ----------
    
    command : string
        define base immutable `command` you wish to run
    
    args : string, optional
        optional arguments passed to base `command`
    
    
    Examples
    --------
    >>> import pprint
    >>> from nipype.interfaces.base import CommandLine
    >>> cli = CommandLine(command='ls', environ={'DISPLAY': ':1'})
    >>> cli.inputs.args = '-al'
    >>> cli.cmdline
    'ls -al'
    
    # Use get_traitsfree() to check all inputs set
    >>> pprint.pprint(cli.inputs.get_traitsfree())  # doctest:
    {'args': '-al',
     'environ': {'DISPLAY': ':1'},
     'ignore_exception': False}
    
    >>> cli.inputs.get_hashval()[0][0]
    ('args', '-al')
    >>> cli.inputs.get_hashval()[1]
    '11c37f97649cd61627f4afe5136af8c0'
    
    Inputs::
    
    	[Mandatory]
    
    	[Optional]
    	args: (a unicode string)
    		Additional parameters to the command
    		flag: %s
    	environ: (a dictionary with keys which are a bytes or None or a value
    		 of class 'str' and with values which are a bytes or None or a value
    		 of class 'str', nipype default value: {})
    		Environment variables
    	ignore_exception: (a boolean, nipype default value: False)
    		Print an error message instead of throwing an exception in case the
    		interface fails to run
    	terminal_output: ('stream' or 'allatonce' or 'file' or 'none')
    		Control terminal output: `stream` - displays to terminal immediately
    		(default), `allatonce` - waits till command is finished to display
    		output, `file` - writes output to file, `none` - output is ignored
    
    Outputs::
    
    	None
    
    
    

    As a quick example, let's wrap bash's ls with Nipype:

    In [ ]:
    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:

    In [ ]:
    result = nipype_ls.run()
    print(result.runtime.stdout)
    
    total 96K
    -rw-r--r-- 1 neuro root  127 May  1 08:21 CHANGES
    -rw-r--r-- 1 neuro root  319 May  1 08:21 dataset_description.json
    drwxr-sr-x 7 neuro root 4.0K May 14 09:07 derivatives
    lrwxrwxrwx 1 neuro root  122 May  1 08:21 dwi.bval -> .git/annex/objects/JX/4K/MD5E-s335--5bd6fa32ccd0c79e79f9ac63a2c09c1a.bval/MD5E-s335--5bd6fa32ccd0c79e79f9ac63a2c09c1a.bval
    lrwxrwxrwx 1 neuro root  124 May  1 08:21 dwi.bvec -> .git/annex/objects/Pg/wk/MD5E-s1248--0641c68ff6ee6164928c984541653430.bvec/MD5E-s1248--0641c68ff6ee6164928c984541653430.bvec
    drwxr-sr-x 5 neuro root 4.0K May 14 09:10 sub-01
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-02
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-03
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-04
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-05
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-06
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-07
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-08
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-09
    drwxr-sr-x 4 neuro root 4.0K May  1 08:21 sub-10
    -rw-r--r-- 1 neuro root  905 May  1 08:21 task-covertverbgeneration_bold.json
    -rw-r--r-- 1 neuro root  143 May  1 08:21 task-covertverbgeneration_events.tsv
    -rw-r--r-- 1 neuro root  899 May  1 08:21 task-fingerfootlips_bold.json
    -rw-r--r-- 1 neuro root  280 May  1 08:21 task-fingerfootlips_events.tsv
    -rw-r--r-- 1 neuro root  897 May  1 08:21 task-linebisection_bold.json
    -rw-r--r-- 1 neuro root  904 May  1 08:21 task-overtverbgeneration_bold.json
    -rw-r--r-- 1 neuro root  143 May  1 08:21 task-overtverbgeneration_events.tsv
    -rw-r--r-- 1 neuro root  904 May  1 08:21 task-overtwordrepetition_bold.json
    -rw-r--r-- 1 neuro root  127 May  1 08:21 task-overtwordrepetition_events.tsv
    

    Create your own CommandLine interface

    Let's create a Nipype Interface for a very simple tool called antsTransformInfo from the ANTs package. This tool is so simple it does not even have a usage description for bash. Using it with a file, gives us the following result:

    In [ ]:
    %%bash
    antsTransformInfo /home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm
    
    Transform file: /home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm
    AffineTransform (0x55d8723769d0)
      RTTI typeinfo:   itk::AffineTransform<double, 3u>
      Reference Count: 3
      Modified Time: 660
      Debug: Off
      Object Name:
      Observers:
        none
      Matrix:
        1.0201 -0.00984231 0.00283729
        -0.245557 0.916396 0.324585
        -0.0198016 -0.00296066 0.988634
      Offset: [2.00569, -15.15, -1.26341]
      Center: [-3.37801, 17.4338, 8.46811]
      Translation: [1.79024, -13.0295, -1.34439]
      Inverse:
        0.982713 0.0105343 -0.00627888
        0.256084 1.09282 -0.359526
        0.0204499 0.00348366 1.01029
      Singular: 0
    
    

    So let's plan our implementation:

    1. The command line name is antsTransformInfo.
    2. It only accepts one text file (containing an ITK transform file) as input, and it is a positional argument.
    3. It prints out the properties of the transform in the input file. For the purpose of this notebook, we are only interested in extracting the translation values.

    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:

    In [ ]:
    class TransformInfo(CommandLine):
        _cmd = 'antsTransformInfo'
    

    This is enough to have a nipype compatible interface for this tool:

    In [ ]:
    TransformInfo.help()
    
    Wraps command **antsTransformInfo**
    
    
    Inputs::
    
    	[Mandatory]
    
    	[Optional]
    	args: (a unicode string)
    		Additional parameters to the command
    		flag: %s
    	environ: (a dictionary with keys which are a bytes or None or a value
    		 of class 'str' and with values which are a bytes or None or a value
    		 of class 'str', nipype default value: {})
    		Environment variables
    	ignore_exception: (a boolean, nipype default value: False)
    		Print an error message instead of throwing an exception in case the
    		interface fails to run
    	terminal_output: ('stream' or 'allatonce' or 'file' or 'none')
    		Control terminal output: `stream` - displays to terminal immediately
    		(default), `allatonce` - waits till command is finished to display
    		output, `file` - writes output to file, `none` - output is ignored
    
    Outputs::
    
    	None
    
    
    

    Specifying the inputs

    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:

    In [ ]:
    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 set
    • mandatory=True checks that this input was set before running because the program would crash otherwise
    • argstr='%s' indicates how this input parameter should be formatted
    • position=0 indicates that this is the first positional argument

    We can now decorate our TransformInfo core class with its input, by setting the input_spec member:

    In [ ]:
    class TransformInfo(CommandLine):
        _cmd = 'antsTransformInfo'
        input_spec = TransformInfoInputSpec
    

    Our interface now has one mandatory input, and inherits some optional inputs from the CommandLineInputSpec:

    In [ ]:
    TransformInfo.help()
    
    Wraps command **antsTransformInfo**
    
    
    Inputs::
    
    	[Mandatory]
    	in_file: (an existing file name)
    		the input transform file
    		flag: %s, position: 0
    
    	[Optional]
    	args: (a unicode string)
    		Additional parameters to the command
    		flag: %s
    	environ: (a dictionary with keys which are a bytes or None or a value
    		 of class 'str' and with values which are a bytes or None or a value
    		 of class 'str', nipype default value: {})
    		Environment variables
    	ignore_exception: (a boolean, nipype default value: False)
    		Print an error message instead of throwing an exception in case the
    		interface fails to run
    	terminal_output: ('stream' or 'allatonce' or 'file' or 'none')
    		Control terminal output: `stream` - displays to terminal immediately
    		(default), `allatonce` - waits till command is finished to display
    		output, `file` - writes output to file, `none` - output is ignored
    
    Outputs::
    
    	None
    
    
    

    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:

    In [ ]:
    my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm')
    print(my_info_interface.cmdline)
    
    antsTransformInfo /home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm
    

    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:

    In [ ]:
    try:
        TransformInfo().cmdline
    
    except(ValueError) as err:
        print('It crashed with...')
        print("ValueError:", err)
    else:
        raise
    
    It crashed with...
    ValueError: TransformInfo requires a value for input 'in_file'. For a list of required inputs, see TransformInfo.help()
    

    It will also complain if we try to set a non-existent file:

    In [ ]:
    try:
        my_info_interface.inputs.in_file = 'idontexist.tfm'
    
    except(Exception) as err:
        print('It crashed with...')
        print("TraitError:", err)
    else:
        raise
    
    It crashed with...
    TraitError: The trait 'in_file' of a TransformInfoInputSpec instance is an existing file name, but the path  'idontexist.tfm' does not exist.
    

    Specifying the outputs

    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:

    In [ ]:
    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:

    In [ ]:
    TransformInfo.help()
    
    Wraps command **antsTransformInfo**
    
    
    Inputs::
    
    	[Mandatory]
    	in_file: (an existing file name)
    		the input transform file
    		flag: %s, position: 0
    
    	[Optional]
    	args: (a unicode string)
    		Additional parameters to the command
    		flag: %s
    	environ: (a dictionary with keys which are a bytes or None or a value
    		 of class 'str' and with values which are a bytes or None or a value
    		 of class 'str', nipype default value: {})
    		Environment variables
    	ignore_exception: (a boolean, nipype default value: False)
    		Print an error message instead of throwing an exception in case the
    		interface fails to run
    	terminal_output: ('stream' or 'allatonce' or 'file' or 'none')
    		Control terminal output: `stream` - displays to terminal immediately
    		(default), `allatonce` - waits till command is finished to display
    		output, `file` - writes output to file, `none` - output is ignored
    
    Outputs::
    
    	translation: (a list of items which are a float)
    		the translation component of the input transform
    
    
    

    We are almost there - final needs

    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.

    In [ ]:
    my_info_interface = TransformInfo(in_file='/home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm',
                                      terminal_output='allatonce')
    result = my_info_interface.run()
    
    In [ ]:
    print(result.runtime.stdout)
    
    Transform file: /home/neuro/nipype_tutorial/notebooks/scripts/transform.tfm
    AffineTransform (0x5577045809d0)
      RTTI typeinfo:   itk::AffineTransform<double, 3u>
      Reference Count: 3
      Modified Time: 660
      Debug: Off
      Object Name:
      Observers:
        none
      Matrix:
        1.0201 -0.00984231 0.00283729
        -0.245557 0.916396 0.324585
        -0.0198016 -0.00296066 0.988634
      Offset: [2.00569, -15.15, -1.26341]
      Center: [-3.37801, 17.4338, 8.46811]
      Translation: [1.79024, -13.0295, -1.34439]
      Inverse:
        0.982713 0.0105343 -0.00627888
        0.256084 1.09282 -0.359526
        0.0204499 0.00348366 1.01029
      Singular: 0
    
    

    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:

    In [ ]:
    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'):

    In [ ]:
    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:

    In [ ]:
    result.outputs.translation
    
    Out[ ]:
    [1.79024, -13.0295, -1.34439]

    Summary of a CommandLine interface

    Now putting it all togehter, it looks as follows:

    In [ ]:
    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')}
    
    In [ ]:
    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
    
    Out[ ]:
    [1.79024, -13.0295, -1.34439]

    Wrapping up - fast use case for simple 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:

    In [ ]:
    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')}
    
    In [ ]:
    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()
    
    In [ ]:
    plot_anat(result.outputs.out_file, dim=-1);
    
    Out[ ]:
    <OrthoSlicer3D: sub-01_T1w_brain.nii.gz (256, 156, 256)>

    Create your own 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:

    In [ ]:
    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:

    In [ ]:
    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)
    
    Translated file now is here: translated.nii.gz
    

    Now that the function was executed, let's plot the original and the translated image.

    In [ ]:
    plot_anat(orig_image, dim=-1);
    
    Out[ ]:
    <OrthoSlicer3D: /data/ds000114/sub-01/ses-test/anat/sub-01_ses-test_T1w.nii.gz (256, 156, 256)>
    In [ ]:
    plot_anat('translated.nii.gz', dim=-1);
    
    Out[ ]:
    <OrthoSlicer3D: translated.nii.gz (256, 156, 256)>