Sep 23, 2013

Calling OpenCV functions via Cython from Python 3.X.

I have managed to run OpenCV function from Python3 via Cython today, so I want to write about it. Before you start something like this, you should know you have to read at least header files of C library to write right definition in Cython,because Cython does not do that for you. I am using virtualenv, python from pythonz, and home-brewed OpenCV. Almost everything of C++ works just fine with Cython, but still, you should do some Voodoo things like for integer template argument.
ctypedef void* int_parameter
ctypedef int_parameter two "2"
ctypedef Point_[float, two] Point2f
Now, main sample Cython code is following.
#===== testopencv.pyx ========
import numpy as np
cimport numpy as np # for np.ndarray
from libcpp.string cimport string
from libc.string cimport memcpy

cdef extern from "opencv2/core/core.hpp":
  cdef int  CV_WINDOW_AUTOSIZE
  cdef int CV_8UC3

cdef extern from "opencv2/core/core.hpp" namespace "cv":
  cdef cppclass Mat:
    Mat() except +
    void create(int, int, int)
    void* data

cdef extern from "opencv2/highgui/highgui.hpp" namespace "cv":
  void namedWindow(const string, int flag)
  void imshow(const string, Mat)
  int  waitKey(int delay)


cdef void ary2cvMat(np.ndarray ary, Mat& out):
  assert(ary.ndim==3 and ary.shape[2]==3, "ASSERT::3channel RGB only!!")
  ary = np.dstack((ary[...,2], ary[...,1], ary[...,0])) #RGB -> BGR
  
  cdef np.ndarray[np.uint8_t, ndim=3, mode = 'c'] np_buff = np.ascontiguousarray(ary, dtype = np.uint8)
  cdef unsigned int* im_buff = <unsigned int*> np_buff.data
  cdef int r = ary.shape[0]
  cdef int c = ary.shape[1]
  out.create(r, c, CV_8UC3)
  memcpy(out.data, im_buff, r*c*3)


cdef showMat(Mat m):
  namedWindow( "WIN", CV_WINDOW_AUTOSIZE )
  imshow( "WIN", m )
  waitKey(0)

def openImage(pil_img):
  cdef Mat m
  ary2cvMat(np.array(pil_img), m)
  showMat(m)
The last function openImage(pil_image) will open pil_image in different window. (I know you can do this without OpenCV. Just for a sample.) This sample changes pil_image to numpy array and then convert it to OpenCV's cv::Mat. To compile this, you need a setup file like this.
#======== setup.py ===========
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
from Cython.Build import cythonize
import numpy
import subprocess

proc_libs = subprocess.check_output("pkg-config --libs opencv".split())
proc_incs = subprocess.check_output("pkg-config --cflags opencv".split())
libs = [lib for lib in str(proc_libs, "utf-8").split()]
#print("OPENCV LIBS=", libs)

setup(
  cmdclass = {'build_ext': build_ext},
  ext_modules = cythonize(Extension("opencvsample",
    sources = ["opencvsample.pyx"],
    language = "c++",
    include_dirs=[numpy.get_include(), "/usr/local/Cellar/opencv/2.4.5/include/"],
    extra_link_args=libs
    )
  )
)
To compile and execute, you should do
python setup.py build_ext --inplace
python -c "import testopencv as to; from PIL import Image; to.openImage(Image.open('test.jpg'))"
I spent several days to work this all through, so I hope this might be helpful to somebody. Feedbacks are always appreciated.
 ---------
update: Mar 3 '14 <unsinged int> was missing. Thank you, Andrey.

3 comments:

Andrey said...

Thanks for this. I followed your tutorial, and this shows error when compiling:

testopencv.pyx:28:38: Cannot assign type 'char *' to 'unsigned int *'

at:
cdef unsigned int* im_buff = np_buff.data

Do you know how I can fix this error?

Tomo Iida said...

You are right.
The correct code should be
cdef unsigned int* im_buff = <unsigned int*> np_buff.data

<...> was omitted by tag-thing. I'll fix it.
Thanks for the comment!

Andy Jones said...

Just posting to say thanks, this is extremely useful!