The beauty of ANTsPy is the ease at which users can incorporate (i.e. wrap) their existing C++ code into Python while simultaneously leveraging the IO, visualization, and greater python inter-operability provided by ANTsPy. This short tutorial demonstrates how you can wrap your ITK C++ functions in 15 minutes or less.
You'll notice how seamless the wrapping is between C++ and Python due to implicit
casting of standard types by pybind11
- making the process immensely smoother than
an Rcpp equivalent, for instance.
I have a function that takes an ITK image and rescales the intensity. I want to use this function in Python, but don't want to go about 1) learning a new framework for wrapping my C++ code, 2) learning how to build ITK and my python code at the same time, 3) building an entire set of IO and plotting tools just for using my function.
Here is my original code in a file called "rescaleImage.cxx":
#include "itkImage.h"
#include "itkRescaleIntensityImageFilter.h"
template <typename ImageType>
ImageType::Pointer rescaleAntsImage( typename ImageType::Pointer itkImage,
float outputMinimum, float outputMaximum )
{
typedef itk::RescaleIntensityImageFilter< ImageType, ImageType > RescaleFilterType;
typename RescaleFilterType::Pointer rescaleFilter = RescaleFilterType::New();
rescaleFilter->SetInput( itkImage );
rescaleFilter->SetOutputMinimum( outputMinimum );
rescaleFilter->SetOutputMaximum( outputMaximum );
rescaleFilter->Update();
return rescaleFilter->GetOutput();
}
Starting from the pure C++ ITK function above, we will demonstrate the sequential steps needed to integrate this code into ANTsPy.
The general steps are as follows:
- alter function arguments & add a few headers
- unwrap ANTsPy images & wrap ITK images
- declare the c++ function with pybind11
- build the function in CMakeLists.txt
- optionally add an abstract python interface
- re-build the ANTsPy package
If you haven't already, build the ANTsPy package from source. A simple way to do this is the following:
git clone https://github.com/ANTsX/ANTsPy.git
cd ANTsPy
python setup.py develop
This step will generally take ~1 hour - during which time you can easily become an expert at integrating your C++ code into the ANTsPy package.
The first thing to do is to convert the Input and Output argument types from ImageType::Pointer
to
py::capsule
. Py::capsules are containers which hold the underlying ITK smartpointer and let
us pass them around in Python. This needs to be done ONLY for input and output arguments.
We also need to add in our pybind11 headers which let us implicitly cast between C++
#include <pybind11/pybind11.h> // header for wrapping code
#include <pybind11/stl.h> // header for implicitly casting btwn python-cpp types
#include "itkImage.h"
#include "itkRescaleIntensityImageFilter.h"
namespace py = pybind11; // needed to use "py:"
ttemplate <typename ImageType>
py::capsule rescaleAntsImage( py::capsule & antsImage,
float outputMinimum, float outputMaximum )
{
typedef itk::RescaleIntensityImageFilter< ImageType, ImageType > RescaleFilterType;
typename RescaleFilterType::Pointer rescaleFilter = RescaleFilterType::New();
rescaleFilter->SetInput( itkImage );
rescaleFilter->SetOutputMinimum( outputMinimum );
rescaleFilter->SetOutputMaximum( outputMaximum );
rescaleFilter->Update();
return rescaleFilter->GetOutput();
}
Now, since we are passing in py::capsule types, we have to add the simple functions for
converting py::capsules to ITK smartpointers and vice-versa. NOTE how we
add another header LOCAL_antsImage.h
for doing this.
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "itkImage.h"
#include "itkRescaleIntensityImageFilter.h"
// NEED THIS INCLUDE FOR WRAPPING/UNWRAPPING
#include "LOCAL_antsImage.h"
namespace py = pybind11;
template <typename ImageType>
py::capsule rescaleAntsImage( py::capsule & antsImage,
float outputMinimum, float outputMaximum )
{
// cast from py::capsule to ITK smartpointer (unwrapping)
typename ImageType::Pointer itkImage = as< ImageType >( antsImage );
typedef itk::RescaleIntensityImageFilter< ImageType, ImageType > RescaleFilterType;
typename RescaleFilterType::Pointer rescaleFilter = RescaleFilterType::New();
rescaleFilter->SetInput( itkImage );
rescaleFilter->SetOutputMinimum( outputMinimum );
rescaleFilter->SetOutputMaximum( outputMaximum );
rescaleFilter->Update();
// cast from ITK smartpointer to py::capsule (wrapping)
return wrap< ImageType >( rescaleFilter->GetOutput() );
}
Now, in the file, we need to declare the function for pybind11
to wrap it in python.
This is a simple process, but can be tedious since every image type needs to be declared
explicitly. However, this tediousness caused by template usage leads to cleaner code
and better maintainability in the long run.
After declaring your function, your code will look like this (see bottom):
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "itkImage.h"
#include "itkRescaleIntensityImageFilter.h"
// NEED THIS INCLUDE FOR WRAPPING/UNWRAPPING
#include "LOCAL_antsImage.h"
// NEED THIS FOR PYBIND11
namespace py = pybind11;
template <typename ImageType>
py::capsule rescaleAntsImage( py::capsule & antsImage,
float outputMinimum, float outputMaximum )
{
typename ImageType::Pointer itkImage = as< ImageType >( antsImage );
typedef itk::RescaleIntensityImageFilter< ImageType, ImageType > RescaleFilterType;
typename RescaleFilterType::Pointer rescaleFilter = RescaleFilterType::New();
rescaleFilter->SetInput( itkImage );
rescaleFilter->SetOutputMinimum( outputMinimum );
rescaleFilter->SetOutputMaximum( outputMaximum );
rescaleFilter->Update();
return wrap< ImageType >( rescaleFilter->GetOutput() );
}
// DECLARE OUR FUNCTION FOR APPROPRIATE TYPES
PYBIND11_MODULE(rescaleImageModule, m)
{
m.def("rescaleImageF2", &rescaleAntsImage<itk::Image<float,2>>);
m.def("rescaleImageF3", &rescaleAntsImage<itk::Image<float,3>>);
}
The wrap declaration process is second nature after a while. Notice how
we named our module rescaleImageModule
- this is what our python module will also be called.
You can also see how I named the functions based on the type of input image. This will lead to
TWO functions available in python - rescaleImageModule.rescaleImageF2 and rescaleImagemodule.rescaleImageF3.
Next (almost done), we need to actually build our function during the build process by adding
it to the CMakeLists.txt file in the antspy/ants/lib/
directory. We only need to add two
lines:
pybind11_add_module(rescaleImageModule rescaleImage.cxx)
target_link_libraries(rescaleImageModule PRIVATE ${ITK_LIBRARIES})
Our module will now be built into a python-compatible shared object library when
you run python setup.py develop
or python setup.py install
.
The rest of the steps involve making sure our functions are imported into
the package namespace - we need to import it in the __init__.py
file found in the
antspy/ants/lib/
directory. Simply add this line:
from .rescaleImageModule import *
Now, we have full access to our C++ function in the python/ANTsPY namespace. We can use this function directly if we want, but for users it's better to have a small python interface. This is very easy to implement, and mostly involves argument checking and such.
# relative imports in the package
from .. import utils, core
# could use the following if not in the package:
# from ants import utils, core
def rescale_image(image, min_val, max_val):
"""
Rescale the intensity of an ANTsImage to be between
min_val and max_val
Arguments
---------
image : ANTsImage type
image to be scaled
min_val : float
min value for rescaling
max_val : float
max value for rescaling
Example
-------
>>> import ants
>>> img = ants.image_read(ants.get_data('r16'))
>>> img_rescaled = ants.rescale_image(img, 0., 1.)
>>> print(img.min(), img.max())
>>> print(img_rescaled.min(), img_rescaled.max())
"""
image = image.clone('float')
# get function from its name
lib_fn = utils.get_lib_fn('rescaleAntsImage%s' % (image._libsuffix))
# apply the function to my image
# remember this function returns a py::capsule
rescaled_img_ptr = lib_fn(image.pointer, min_val, max_val)
# wrap the py::capsule back in ANTsImage class
rescaled_img = core.ANTsImage(pixeltype=image.pixeltype,
dimension=image.dimension,
components=image.components,
pointer=rescaled_img_ptr)
return rescaled_img
We also need to import this function in the ANTsPy namespace. If we put the above
python code in a file called rescale_image.py
in the antspy/ants/utils/
directory,
then we would add the following line to the __init__.py
file in the
antspy/ants/utils/
directory:
from .rescale_image import *
Finally, we rebuild the ANTsPy package by going to the main directory and running setup.py again:
python setup.py develop
This should be very quick if you've built ANTsPy before, since only the altered C++ code will need to be rebuilt.
Now we can use this function quite easily:
import ants
img = ants.image_read(ants.get_data('r16'))
rescaled_img = ants.rescale_image(img, 0., 1.) # rescale between 0 and 1
And that's it - a painless 15 minute process that gives you access to all of the ANTsPy functionality such as IO, visualization, numpy-conversion, etc.