-
Notifications
You must be signed in to change notification settings - Fork 38
Adding python bindings to your cpp catkin package
So you have a great C++ library and you want to run some experiments and make some plots. Don't develop new code to export CSV files, don't fire up MATLAB, export your library to python!
This tutorial assumes the following:
- You are using catkin to build your packages.
- You are using the Eigen linear algebra library.
We generally don't like to mix pure C++ code with the code that exports the python interface. The first thing we usually do is create a new package. In this tutorial, I'm going to assume that you have a C++ package called robot_awesome
. We will therefore call our python export package robot_awesome_python
.
So, go to your catkin workspace and create the package directory structure:
cd catkin_workspace
cd src
mkdir robot_awesome_python
cd robot_awesome_python
mkdir src
mkdir -p python/robot_awesome
This structure looks like this.
.
+-- src <-- C++ wrapper .cpp files go in here
+-- python
| +-- robot_awesome <-- python initialization files go in here
The package.xml
file identifies this as a catkin project
<?xml version="1.0"?>
<package>
<name>robot_awesome_python</name>
<version>0.0.1</version>
<description>
robot_awesome_python exports python bindings for robot_awesome!
</description>
<maintainer email="[email protected]">Me Me</maintainer>
<license>New BSD</license>
<buildtool_depend>catkin</buildtool_depend>
<buildtool_depend>catkin_simple</buildtool_depend>
<build_depend>python_module</build_depend>
<build_depend>numpy_eigen</build_depend>
<build_depend>eigen_catkin</build_depend>
</package>
The dependency on python_module
gets us a handy CMake macro that we will use next. The dependency on numpy_eigen
allows us to communicate matrices across the python C++ border.
The CMakeLists.txt is pretty simple
cmake_minimum_required(VERSION 2.8.3)
project(robot_awesome_python)
find_package(catkin_simple REQUIRED)
catkin_simple()
# Set up the python exports.
SET(PY_PROJECT_NAME ${PROJECT_NAME})
SET(PY_PACKAGE_DIR python/robot_awesome)
add_python_export_library(${PY_PROJECT_NAME} ${PY_PACKAGE_DIR}
src/module.cpp
)
cs_install()
cs_export()
There are a few things to note here:
- The variable
PY_PACKAGE_DIR
points to the directory you created in step 1. This is the directory that you will have your__init__.py
file in for initializing the python module. - The variable
PY_PROJECT_NAME
defines the final name of your library file. It is extremely important to remember this name as it will be used in the next step. - The
add_python_export_library()
function makes sure that your package gets installed correctly together with the pure python part (inPY_PACKAGE_DIR
) and the C++ binding part (the library specified byPY_PROJECT_NAME
). - The
add_python_export_library()
includes a list of source files to be compiled into your python library. Here we just have module.cpp but larger libraries will have many source files.
This is the file that contains the C++ bindings. If you have multiple structures that you are exporting, please break this up into multiple files. See the sm_python library for an example.
// It is very important that this is the first header in every file.
// This includes some modifications to the boost::python headers that
// make them work with memory-aligned Eigen types.
#include <numpy_eigen/boost_python_headers.hpp>
// Now bring in the headers of functions and classes you are wrapping.
#include <robot_awesome/AwesomeRobot.hpp>
// The module name here *must* match the name of the python project
// specified in the CMakeLists.txt file with lib pasted on the front:
// as in libPY_PROJECT_NAME
BOOST_PYTHON_MODULE(librobot_awesome_python)
{
// This using statement is just for convenience
using namespace boost::python;
// Export functions and documentation strings
def("processAwesome", ðz::processAwesome, "things = processAwesome( robots )");
// Export classes
class_< AwesomeRobot, boost::shared_ptr<AwesomeRobot> >( "AwesomeRobot", init<>() )
.def("isAwesome", &AwesomeRobot::isAwesome, "bool robot.isAwesome() returns true if this robot is awesome")
;
}
The above is a bit silly. To fill something in for real, look at the examples in the sm_python library library, read this tutorial, or see the tips and tricks at the bottom of this tutorial.
The __init__.py
file identifies a directory as a python module. This is the code that is run when you type import robot_awesome
at a python interpreter.
# Import the numpy to Eigen type conversion.
import numpy_eigen
# Import the C++ wrappers. This must be the same name you have in your
# module.cpp file.
from librobot_awesome_python import *
The last thing you have to do is get catkin to install your python library where it can be found:
## ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD
from distutils.core import setup
from catkin_pkg.python_setup import generate_distutils_setup
# fetch values from package.xml
setup_args = generate_distutils_setup(
packages=['robot_awesome'],
package_dir={'':'python'})
setup(**setup_args)
This file tells catkin to install your python package from python/robot_awesome
to your catkin develspace where python can find it.
After building your library with catkin, it will be available to python if you have invoked your develspace setup.bash
file.
cd catkin_workspace
catkin build --merge-devel robot_awesome_python
source devel/setup.bash
Now you can boot up your python interpreter and test our your library.
ipython --pylab
In [1]: import robot_awesome
In [2]: robot = robot_awesome.AwesomeRobot()
In [3]: robot.isAwesome()
True
- http://wiki.python.org/moin/boost.python/CallPolicy
- http://wiki.python.org/moin/boost.python/HowTo
- http://boost.cppll.jp/HEAD/libs/python/doc/tutorial/
Note that there are some tricky cases. For example, if you are using numpy_eigen
, you cannot pass an matrix into C++ by reference (const references are okay but modifying a matrix or vector reference argument will not work). Below are some hints for how to deal with tricky cases. Most of these were taken from the tutorials above or from experience.
class_<Gps>("Gps",init<>())
.def(init<double,double,double,int>("Gps(latitudeDegrees, longitudeDegrees, heightMeters, rtkCode)"))
.def_readwrite("latitudeDegrees",&Gps::latitudeDegrees)
.def_readwrite("longitudeDegrees",&Gps::longitudeDegrees)
.def_readwrite("heightMeters",&Gps::heightMeters)
.def_readwrite("rtkCode",&Gps::rtkCode)
.def("toUtm",&Gps::toUtm)
;
Eigen3::Matrix4d(*toTEuler1)(double,double,double,double, double, double) = &toTEuler;
def("toTEuler",toTEuler1,"Create a 4x4 transformation matrix from 6 parameters");
Eigen3::Matrix4d(*toTEuler2)(Eigen3::Matrix<double,6,1>const &) = &toTEuler;
def("toTEuler",toTEuler2,"Create a 4x4 transformation matrix from a 6x1 column of parameters");
void(PoseGraph::*addVertex2)(VertexId)= &PoseGraph::addVertex;
VertexId(PoseGraph::*addVertex1)()=&PoseGraph::addVertex;
class_<PoseGraph>("PoseGraph",init<>())
.def("addVertex",addVertex1)
.def("addVertex",addVertex2)
;
def("T_to_from", getTFromEdge, return_value_policy<copy_const_reference>())
using namespace boost::python;
tuple inverseTransformationAndJacobianTuple(Eigen3::Matrix4d const & T_ba, Eigen3::Vector4d const & v_b)
{
Eigen3::Vector4d out_v_a;
Eigen3::Matrix<double,4,6> out_B;
inverseTransformationAndJacobian(T_ba, v_b, out_v_a, out_B);
return make_tuple(out_v_a, out_B);
}
// ...
def("inverseTransformationAndJacobian",inverseTransformationAndJacobi anTuple);
list allVertices(PoseGraph*pg)
{
list t;
VertexSet vs = pg->allVertices();
for(VertexSet::const_iterator v = vs.begin(); v != vs.end();
{
t.append(*v);
}
return t;
}
//...
class_<PoseGraph>("PoseGraph",init<>())
.def("allVertices",&allVertices)
;
class_<ImuCameraCalibratorOptions> ("ImuCameraCalibrationOptions",init<>())
.add_property("doGravity",&ImuCameraCalibratorOptions::get_doGravity, &ImuCameraCalibratorOptions::set_doGravity)
.add_property("doCalibration",&ImuCameraCalibratorOptions::get_doCali bration,&ImuCameraCalibratorOptions::set_doCalibration)
.add_property("doBias",&ImuCameraCalibratorOptions::get_doBias,&ImuCameraCalibratorOptions::set_doBias)
;
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
//...
class_<std::vector<DenseBlockJacobian>> ("DenseBlockJacobianVec")
def(vector_indexing_suite<std::vector<DenseBlockJacobian> > ())
;
This is useful when the member is an exposed python object
classA{
public:
//...
}
classB{
public:
//...
A a;
int c;
};
//...
class_<A>("A");
class_<B>("B")
.def_readwrite("a",&B::a)
.def_readonly("c",&B::c)
;
class_<ScRelaxationEngine,bases<asrl::sc_solver::SolverV2>> ("ScRelaxationEngine",init<>())
.def("relax",&ScRelaxationEngine::relax)
.def("poseGraph",&ScRelaxationEngine::poseGraph, return_internal_reference<>())
;
http://wiki.python.org/moin/boost.python/OverridableVirtualFunction
// SolverV2 is pure virual... I'm not totally sure how this all works.
// Now subclasses should be declared as
//class_<XXX,bases<asrl::sc_solver::Setup>>("XXX")
class_<SolverV2,boost::noncopyable>("SolverV2",no_init)
.def("numSparseBlocks",&SolverV2::numSparseBlocks)
.def("getDenseBlockDimension",&SolverV2::getDenseBlockDimension)
.def("getSparseBlockDimension",&SolverV2::getSparseBlockDimension)
;
class_<ScRelaxationEngine,bases<asrl::sc_solver::SolverV2>> ("ScRelaxationEngine",init<>())
.def("relax",&ScRelaxationEngine::relax)
.def("poseGraph",&ScRelaxationEngine::poseGraph, return_internal_reference<>())
;
For some unknown reason, when I extended a non-abstract base class, I got an error like:
ArgumentError: Python argument types in
BaseClass.functionName(DerivedClass)
did not match C++ signature:
functionName(aslam::BaseClass*)
To fix it, I had to add
boost::python::implicitly_convertible<DerivedClass,BaseClass>();
to the wrapper
using namespace boost::python;
enum color { red = 1, green = 2, blue = 4 };
color identity_(color x) { return x; }
BOOST_PYTHON_MODULE(enum_ext)
{
enum_<color>("color")
.value("red", red)
.value("green", green)
.value("blue", blue)
;
def("identity", identity_);
}
In a python shell type (e.g. use ipython):
> import roslib
> import MYPACKAGE