Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Adding python bindings to your cpp catkin package

Paul Furgale edited this page May 13, 2014 · 2 revisions

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!

Prerequisites

This tutorial assumes the following:

  1. You are using catkin to build your packages.
  2. You are using the Eigen linear algebra library.

Step 1. Create a new package

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

Step 2. Create the files needed

package.xml

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.

CMakeLists.txt

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:

  1. 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.
  2. 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.
  3. The add_python_export_library() function makes sure that your package gets installed correctly together with the pure python part (in PY_PACKAGE_DIR) and the C++ binding part (the library specified by PY_PROJECT_NAME).
  4. 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.

src/module.cpp

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", &ethz::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.

python/robot_awesome/init.py

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 *

setup.py

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.

Using your library

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

Helpful Links

Some under-documented syntax hints for tricky cases

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.

Document a non-default constructor

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)
  ;

Disambiguate an overloaded function

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");

Disambiguate an overloaded member function

void(PoseGraph::*addVertex2)(VertexId)= &PoseGraph::addVertex;
VertexId(PoseGraph::*addVertex1)()=&PoseGraph::addVertex;
class_<PoseGraph>("PoseGraph",init<>())
  .def("addVertex",addVertex1)
  .def("addVertex",addVertex2)
  ;

Deal with a const_reference return type

def("T_to_from", getTFromEdge, return_value_policy<copy_const_reference>())

Create a tuple

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);

Create a list

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)
  ;

Add a property

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)
  ;

Using the Indexing Suite

#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
//...
class_<std::vector<DenseBlockJacobian>> ("DenseBlockJacobianVec")
  def(vector_indexing_suite<std::vector<DenseBlockJacobian> > ())
  ;

Adding a read/write object member

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)
  ;

Return an internal reference

class_<ScRelaxationEngine,bases<asrl::sc_solver::SolverV2>> ("ScRelaxationEngine",init<>())
  .def("relax",&ScRelaxationEngine::relax)
  .def("poseGraph",&ScRelaxationEngine::poseGraph, return_internal_reference<>())
  ;

Define a subclass to a class that already has a boost::python interface

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

Wrap an enum

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_);
}

Start Using it

In a python shell type (e.g. use ipython):

> import roslib
> import MYPACKAGE