Imagine writing a Computer Vision library using OpenCV. You want your code to be easily portable to Linux, Mac, Windows, iOS, Android and even embedded devices. So you choose to build your library in C++ using OpenCV. Excellent choice!
Along comes a client who wants to license your entire library but they want it delivered as a Python module. You say, “No problem!” and search the internet for a solution. BOOM! you land on this post! Awesome! We are going to learn how to build a Python module from your OpenCV C++ code.
Python Bindings for C++ code
The neat thing about a library written in a system programming language like C++ is that there are standard ways of creating a binding for this library in a higher level language like Python. Before we jump into our solution, I want to briefly explain how to create Python bindings for your C++ code. If you want to understand the technical nitty gritty of how your generic C++ code can be used to build a python module, check out this tutorial. To summarize the steps, you need the following pieces
- Write a Python callable wrapper function: This function parses the arguments and calls the actual C/C++ function. It also handles any errors.
- Register functions and methods : Next you need use
PyMethodDef
to register the function in the module’s symbol table. - Create an init function: Finally, we need use
Py_InitModule
to create an initialization function for the module.
This is all fine and dandy but if you have a large library, doing this by hand is cumbersome and error prone. So we will generate most of this code automatically using a python script.
Python bindings for OpenCV based C++ code
As you may know, OpenCV is written in C++. The good folks at OpenCV have created bindings for Python which enables us to compile OpenCV into a Python module (cv2).
Wouldn’t it be cool if we could piggyback on the work already done by the community? So we took inspiration from this tutorial and created a simplified example.
We will use the same scripts used by OpenCV to generate their Python module and minimally change their main wrapper file (cv2.cpp) to create our own module. We will call this module bv after my consulting company Big Vision LLC.
There are huge benefits to this approach.
- Efficient and consistent mapping: Having efficient Python types mapped to efficient C++ types consistent with OpenCV’s mappings.
- Suitable idioms: Having idioms suitable to the library means we are leveraging the hard work done by the community. For instance, in OpenCV using the argument type to be
OutputArray
instead ofMat
in a function definition automatically makes it an a output returned by the function in the exported Python module. - Easy function and class definition : This allows easy mapping of classes and functions. We have to define them only once in C++. If done by hand you will end up doing work equivalent to defining a class again in Python.
The code is inside the pymodule directory of the code base. The directory structure is shown on the left.
It has the following files
- bv.cpp, pycompat.hpp : bv.cpp is a slightly modified version of the wrapper file (cv2.cpp) that comes with OpenCV. It uses pycompat.hpp for Python 2 / 3 compatibility checks.
- bvtest.py : Python code for testing our module (bv) once we have built it. In our example, we have a function
bv.fillHoles
and a classbv.bv_Filters
exposed in the Python module. The C++ implementations offillHoles
andFilters
are in src/bvmodule.cpp.
import cv2
import sys sys.path.append('build')
import bv im = cv2.imread('holes.jpg', cv2.IMREAD_GRAYSCALE)
imfilled = im.copy()
bv.fillHoles(imfilled) filters = bv.bv_Filters()
imedge = filters.edge(im)
cv2.imshow("Original image", im)
cv2.imshow("Python Module Function Example", imfilled)
cv2.imshow("Python Module Class Example", imedge)
cv2.waitKey(0)
3. gen2.py, hdr_parser.py : The Python bindings generator script (gen2.py) calls the header parser script (hdr_parser.py). These files are provided as part of the OpenCV source files. According to the OpenCV tutorial, “this header parser splits the complete header file into small Python lists. So these lists contain all details about a particular function, class etc.” In other words, these scripts automatically parse the header files and register the functions, classes, methods etc. with the module.
4. headers.txt: A text file containing all the header files to be compiled into the module. In our example, it contains just one line src/bvmodule.hpp.
5. holes.jpg: Example image used by our Python module test script bvtest.py
6. src/bvmodule.cpp: This cpp file contains the functions and class definitions. In this example, we implemented a function fillHoles
and a class Filters
with just one method called edge
. The function fillHoles takes a gray scale image and fills any holes (dark regions surround by white areas). The method edge
simply performs Canny edge detection.
#include"bvmodule.hpp"
namespace bv
{
void fillHoles(Mat &im)
{
Mat im_th;
// Binarize the image by thresholding
threshold(im, im_th, 128, 255, THRESH_BINARY);
// Flood fill
Mat im_floodfill = im_th.clone();
floodFill(im_floodfill, cv::Point(0,0), Scalar(255));
// Invert floodfilled image
Mat im_floodfill_inv;
bitwise_not(im_floodfill, im_floodfill_inv);
// Combine the two images to fill holes
im = (im_th | im_floodfill_inv);
}
void Filters::edge(InputArray im, OutputArray imedge)
{
// Perform canny edge detection
Canny(im,imedge,100,200);
}
Filters::Filters()
{
}
}
7. src/bvmodule.hpp: Not all functions and methods in your code need to be exposed to the Python module. This header file explictly mentions which ones we want to export.
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
namespace bv
{
CV_EXPORTS_W void fillHoles(Mat &mat);
class CV_EXPORTS_W Filters
{
public:
CV_WRAP Filters();
CV_WRAP void edge(InputArray im, OutputArray imedge);
};
}
Note: We used InputArray and OutputArray instead of Mat in the edge method of the class. Doing so makes imedge the output in the exported Python code and we are able to use this line in the Python example above.
imedge = filters.edge(im)
Steps for building the Python module
We are now ready to go over the steps for building our Python module.
- Step 1: Put your c++ source code and header files inside the src directory.
- Step 2: Include your header file in headers.txt
- Step 3: Make a build directory.
mkdir build
Step 4: Use gen2.py to generate the Python binding files. You need to specify the prefix (pybv), the location of the temporary files (build) and the location of the header files (headers.txt).
python3 gen2.py pybv build headers.txt
This should generate a whole bunch of header files with prefix pybv_*.h. If you are curious, feel free to inspect the generated files.
Step 5: Compile the module
g++ -shared -rdynamic -g -O3 -Wall -fPIC \ bv.cpp src/bvmodule.cpp \ -DMODULE_STR=bv -DMODULE_PREFIX=pybv \ -DNDEBUG -DPY_MAJOR_VERSION=3 \ `pkg-config --cflags --libs opencv` \ `python3-config --includes --ldflags` \ -I . -I/usr/local/lib/python3.5/dist-packages/numpy/core/include \ -o build/bv.so
In Line 2 we specify the source files. In Line 3, we set the module name using MODULE_STR and MODULE_PREFIX (pybv) used in the previous step. In Line 4 we specify the Python version. In Line 5 we include OpenCV library and the header files and in Line 6 we include Python 3 related header files and some standard libraries. In my machine, numpy was not in the included path and so I had to add an extra Line 7 for numpy. Your location for numpy may be different. Finally, in Line 8 we specify the location of the output module (build/bv.so).
Testing Python Module
The script bvtest.py load the module and uses the function bv.fillHoles
and the exported class bv.bv_Filters
. If you compile everything correctly and run the python script, you will see the result shown below.
Great! we have built module and are ready to test.