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.
This is amazing! Very useful, thanks for sharing, Sir!
Thanks, Kushashwa.
Hi Satya;
which parts of the cv2.cpp are modified in bv.cpp?
thanks
Hi Mehmet,
There are a few lines ( < 50 ). You can use the **diff** command to spot the differences.
Hi Satya, what version of opencv was ‘cv2.cpp’ modified from? 3.4.x? thanks
Thank you always.
Is it a technology that can also be used for Dlib?
I think so but I have not tried it. Most of Dlib code is in header files and I think it should work if you include them.
Hello Satya. Tell me in what there can be a problem?
/usr/local/include/opencv2/core/core_c.h:97:1: error: ‘IplImage’ does not name a type
CVAPI(IplImage*) cvCreateImageHeader( CvSize size, int depth, int channels );
OpenCV_VERSION: 3.4.0
What is your Python version? It has to be > 3
Python version 3.7. I figured out the problem. The problem was that in the header, the class enum variable was declared in addition to the main class. Because of this, the python script did not correctly make files.
hi, I use your code in windows with cmake and python27, and there is an error:
static function ‘bool pybv_to(PyObject *,T &,const char *)’ declared but not defined bv E:workspacepythonpymodulebv.cpp 1885
what there might be a problem?
python3 still raising the problem
show more details
error C2129: static function ‘bool pybv_to(PyObject *,T &,const char *)’ declared but not defined
with
[
T=cv::KeyPoint
]
Can I convert Python code to a C++ module ?
Hi Sir,
Very nice tutorial.
Actually I am working for same kind of problem but I am capturing video from ToF camera in opencv C++ and doing some processing in opencv python code. Can you provide some help in terms of calling this C++ video capture function in python module?
Hey thanks for the good tutorial 🙂 I am very close to make it work.. but I get the following compiling error:
bv.cpp:1293:20: error: use of undeclared identifier ‘traits’
hey i am getting the same error , did you solve it ?
it’s great!
Thanks Satya for the tutorial, I followed it up and at the end getting following error while running the bvtest.py:
—————————————————————————————
Traceback (most recent call last):
File “bvtest.py”, line 4, in
import bv
ImportError: build/bv.so: undefined symbol: _ZN2cv3Mat20updateContinuityFlagEv
—————————————————————————————
Sir, thank you for your tutorials. However, after I downloaded your code and ran exactly the above commands. I got this error: ImportError: dynamic module does not define init function (initbv). May i ask you know why?
It will be possible rewrite the command to CMAKE?