Page MenuHomePhabricator

Create a basic Python-Wrapping for MITK using SWIG
Closed, ResolvedPublic

Description

Add the option to use SWIG to create bindings for common scripting languages. The idea behind this project is to build a infastructure for a Python-wrapping of the most basic MITK features using SWIG. This structure might then later be used to provide more complex wrapping or the wrapping of additional features.

Revisions and Commits

Event Timeline

goetzm triaged this task as High priority.Jan 11 2018, 4:31 PM
goetzm created this task.

It is now theoretically possible to load the created module:

  1. Open a shell in the output folder (for windows: ../MITK_build/lib/Release)
  2. Append the PATH-Variables so that the created linking objects (For windows *.dll's) are found if necessary. For windows, this can be done by calling one of the starting scripts that are created by default.
  3. Start python and import the new module.

Right now there are some errors:

  • The name of the module is "_PythonMITK" - i don't know how to get rid of the underscore. But this is a minor problem.
  • Importing the library fails with the message: "ImportError: dynamic module does not define module export function (PyInit__PythonMITK). This is the current point of work.

Solved both problems:

  • The created project must have the same name in cmake and the corresponding .i - file.
  • A .py-wrapper is created in the folder MITK-build/Wrapping/Python (for windows). Copy this wrapper file to the library folder (Where the pyd -object is)
  • Execture steps 1-3 from above. The module can then be loaded without the the underscore, for now it is PythonMITK.

Updated current implemented. Successfully wrapped mitk::IOUtil , but there are still some bugfixes, workarounds. The most limiting so far are:

  • Saving is not possible, because mitk::IOUtil::Load () returns a Smartpointer, while mitk::IOUtil::Save expects a standard pointer.
  • The Include-Path for mitkIOUtil.h is hard coded right now. It is necessary to add this include-path to the swig-file. I added it as a hard-coded command as a workaround. Better solutions might be possible / need to be evaluated.

It is now possible to load and save a BaseData-object. This required the following changes:

  • Wrapping the mitk::BaseData : In Order to be able to handle these types of Data
  • Helping SWIG dealing with Vectors: IOUtil::Load returns a vector of BaseData. Therefore swig had to know how to handle those vectors. The way to do it is to include std_vector.i in the i-file. Then all instanciations of std::vector<a> needs to be initialized using a %template construction. Did this so far for BaseData and the standard c types.
  • Wrap itk::SmartPointer: While IOUtil::Load returns SmartPointers, IOUtil::Save requires Basic Pointer. It is therefore necessary to be able to handle the Smartpointer-Structure directly from python. Luckely, Swig has the ability to wrap Smartpointers rather easy. I.e. it detects overloaded -> Operators (as for our smartpointers) and wraps the functions calls directly so that there is nearly no difference between Natural Pointers and Smartpointers within the scripting language. It further allows to access all SmartPointer-methods. For more information refer to the links at the end.
  • mitkIOUtil, mitkBaseData, and itkSmartPointer do contain some macros. As only parts of the C++ code are evaluated in order to create the Scripting interface, it is neccesary to redefine these macros (either fully or as "dummy" version).

http://swig.10945.n7.nabble.com/Wrapping-a-custom-smart-pointer-td14899.html
http://www.swig.org/Doc3.0/SWIGDocumentation.html#SWIGPlus_smart_pointers
http://quantlib.org/reposit/docs/swig/SWIGDocumentation.html

Wheel-generation under linux is now possible. There were basically three reasons that it didn't worked from the beginning:

  1. The default size of ints is different from Visual Studio to GNU GCC. For Swig, this is somewhat a problem, and the default is using VS. It is therefore necessary so set an additional switch to get SWIG working under GCC. (See mitkSwigAddLibraries etc.. in source/CMake/
  2. During the build-process of python, a external cmake-script is called. This basically copies some build information to the setup.py-file (Needs to be done after building, therefore an external cmake script.) In order to have the right call, under linux one of the parameters needed to be quoted.
  3. The setup.py.in file needed to be adapted in order to find linux libraries and not only dll's.

Building Linux-Wheels isn't simple and straightforward. The wheels that have been build so far do not work due to the included dynamic libraries.

Auditwheel seems to be a solution for this (https://stackoverflow.com/questions/23916186/how-to-include-external-library-with-python-wheel-package) - I wasn't able to use it since the installation failed on my systems.
Another solution is to build the wheel in a manylinux1-docker container (based on centOS 5.11). This offers the advantage that only a limited number of shared system libararies are used and that the wheels that are created by this process are more easily sharable (at least from what i read). It is therefore the recommended way to go (https://www.python.org/dev/peps/pep-0513/) . Suiteable docker containers can be found at: https://github.com/pypa/manylinux/tree/master/docker .

In order to use this container, cmake and openssl needs to be manually build and installed. This seems to work so far, building on the manylinux1 solution of simpleITK.
A bigger issue here is that the docker file comes with gcc 4.8 and gcc 4.9 is needed for MITK. I am currently trying to build gcc 4.9, based on the solution found here: https://github.com/g1o/gcc-4.9.3-boost-1.58 . The build Process is still going on, so no results so far.

The benefit of this solution is that we could use Travis CI for the automated wheel building and testing for linux (and in theory mac) either directly (https://travis-ci.org/) by using an suiteable wrapper: https://github.com/joerick/cibuildwheel .

Managed to successfully compile GCC 4.9.4 and MITK.
We need to install some additional packages and manually build libtiff , but now MITK is compiling.

Right now, there is a linker problem with Python ... Stay tuned, the fun continues...

Ok, so a lot of fun later....

It is recommended not link agaings the python libraries (https://gitlab.kitware.com/vtk/vtk/merge_requests/1511 , https://github.com/pypa/manylinux/blob/master/pep-513.rst ) because they could different if someone build his own python. Due to that, the manylinux1 -Docker container does not ship the python libs with it.

This was the reason, why the python-wrapping didn't build so far. There seems to be a workaround (inspired by simpleITK and the file sitkTargetLinkLibrariesWithDynamicLookup.cmake) but it didn't worked. I now found out that this is due to the fact that we add the linker flag "-Wl,--no-undefined" which broke the flag "-Wl,--unresolved-symbols=ignore-all". So now i'm looking for a way to remove that linker flag...

It is .. difficult ... to build a manylinux1 -compatible linux wheel, as it allows the use of glibc++ up to the marker GLIBCXX_3.4.8 and gcc 4.9 has GLIBCXX_3.4.20 .

One solution is to create non-manylinux-compatible wheels. Downside, they cannot be distributed...

It might be possible to build sufficient wheels by linking against the system glibc-library, more information for this can be found at:

https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html option -fabi-version
https://gcc.gnu.org/onlinedocs/gcc/Compatibility.html

In the manylinux-doker file, the system includes are at /usr/include/c++/4.1.2

But i won't look at this option at the moment. Maybe later?

A problem with the python wrapping was the casting. For example: IOUtil::Load loaded a vector of BaseData::Pointer. If the underlying objects are images, you need to cast those pointers to Image::Pointer. Similar problems occure whenever a more specialised type is used. Since each target language handles casting in a different way, this problem needs to be handled for every target language individually. Nevertheless, there are some common methods to do this.

I implemented a solution for python, although some parts of the solution might be used for different languages as well

  1. I implemented a template method that allows to cast any BaseData-object to the template class pointer. The interface is shown below this point, and also an example for the conversion to an mitk::Image. This method will now be available for all target languages!
template<class T> 
typename T::Pointer ConvertTo(mitk::BaseData::Pointer base)

mitk::Image::Pointer ConvertTo(mitk::BaseData::Pointer base)
  1. The SWIG_ADD_MITK_CLASS macro is extended using the %template-directive. The macro now automatically instanciates a conversion method for each MITK-class that is added. This function will appear as a normal function with a name following the naming scheme "ConvertTO<ClassName>", for example "ConvertToImage" for the MITK Image. Again, this part of the solution can be used in all target languages.

Step 1 and 2 are enough to have a working solution , although it is not very comfortable. For example, the user might call a cast that is not working, resulting in a Null-Pointer. Or the user might be unexperienced and won't know which type of data he has. To overcome the previous mentioned problems i added a python-specific solution part.

  1. The SWIG_ADD_MITK_CLASS -macro add a entry to the "convertion_list" dictionary, that maps a string with the name of the class to the python version of the corresponding ConvertTo-Function. As a result, the convertion_list-dictionary contains all known convertions methods.
  1. A _ _getattr_ _ Method is given for each class. This method is called whenever a unknown attribute is called in python with the name of the attribute. The method then returns this attribute. With the given version of this method the Conversion Method to each class that is a) in the class hierarchy of the underlying object and b) a ConvertTo method is wrapped. This is done by:
    1. Removes the starting "ConvertTo" from the given attribute name to obtain the class name.
    2. Gets the string names of each class in the hirarchy by calling GetClassHierarchy
    3. Reduce the list to those elements which are also in "convertion_list"
    4. Return the "convertion_list" entry for the given name
  1. A _ _dir_ _ Method is passed. It appends the original dir-method and adds for each class that is in the hierarchy of the current object and that has a ConvertTo method a "ConvertToClass" entry. This method is usually used to get all methods for a given object, for example for the autocompletion of ipython.

The result of step 4 and 5 is that each object has now a bunch of "ConvertTo" methods that act like all other methods. But only those "ConvertTo" methods are available that make actually sense on the currenct project, e.g. that are in the hierarchy of the current object. This mechanism allows to up and downcast classes. The downside is that it only works for MITK-classes, but that should be ok.

(For further information on those methods see:https://amir.rachum.com/blog/2016/10/05/python-dynamic-attributes/ )

A conversion of mitk::Images from / to numpy error is possible. I've added three methods for this using a combination of python / c++ code. The first method,

GetArrayFromImage(image)

takes a python mitk::Image object and returns a numpy array. This method gives a copy of the underlying memory, so no in-place adding. If copying the memory is too expensive, the method

GetArrayViewFromImage

might be called which gives a read-only view of the image memory.

To convert the memory back to an image, the method

GetImageFromArray

might be used which creates a new image from a given array. This image can then be further initialized using a different, existing image.

This solution is inspired by the behavior of SimpleITK. I think it might be improved at some point, but right now it is working.

Decision: The ConvertTo-Method will not be portet to the core (at this moment)

goetzm added a revision: Restricted Differential Revision.Mar 23 2018, 1:41 PM
goetzm added a revision: Restricted Differential Revision.Mar 26 2018, 9:43 AM