2012-10-18 2 views
17

Sto provando a scrivere un wrapper python per alcuni codici C++ che fanno uso di OpenCV ma ho difficoltà a restituire il risultato, che è un oggetto OpenCV C++ Mat, al interprete python.Scrittura di collegamenti Python per codice C++ che usano OpenCV

Ho guardato il codice sorgente di OpenCV e ho trovato il file cv2.cpp che ha funzioni di conversione per eseguire conversioni avanti e indietro tra PyObject * e OpenCV's Mat. Ho fatto uso di quelle funzioni di conversione ma ho avuto un errore di segmentazione quando ho provato a usarle.

Ho bisogno di alcuni suggerimenti/codice di esempio/riferimenti online su come interfacciare Python e codice C++ che fanno uso di OpenCV, in particolare con la possibilità di restituire il C++ Mat di OpenCV all'interprete python o forse suggerimenti su come/dove iniziare a indagare sulla causa dell'errore di segmentazione.

Attualmente sto usando Boost Python per avvolgere il codice.

Grazie in anticipo a qualsiasi risposta.

Il codice rilevante:

// This is the function that is giving the segmentation fault. 
PyObject* ABC::doSomething(PyObject* image) 
{ 
    Mat m; 
    pyopencv_to(image, m); // This line gives segmentation fault. 

    // Some code to create cppObj from CPP library that uses OpenCV 
    cv::Mat processedImage = cppObj->align(m); 

    return pyopencv_from(processedImage); 
} 

Le funzioni di conversione presi da fonte di OpenCV segue. Il codice di conversione fornisce un errore di segmentazione nella riga commentata con "if (! PyArray_Check (o)) ...".

static int pyopencv_to(const PyObject* o, Mat& m, const char* name = "<unknown>", bool allowND=true) 
{ 
    if(!o || o == Py_None) 
    { 
     if(!m.data) 
      m.allocator = &g_numpyAllocator; 
     return true; 
    } 

    if(!PyArray_Check(o)) // Segmentation fault inside PyArray_Check(o) 
    { 
     failmsg("%s is not a numpy array", name); 
     return false; 
    } 

    int typenum = PyArray_TYPE(o); 
    int type = typenum == NPY_UBYTE ? CV_8U : typenum == NPY_BYTE ? CV_8S : 
       typenum == NPY_USHORT ? CV_16U : typenum == NPY_SHORT ? CV_16S : 
       typenum == NPY_INT || typenum == NPY_LONG ? CV_32S : 
       typenum == NPY_FLOAT ? CV_32F : 
       typenum == NPY_DOUBLE ? CV_64F : -1; 

    if(type < 0) 
    { 
     failmsg("%s data type = %d is not supported", name, typenum); 
     return false; 
    } 

    int ndims = PyArray_NDIM(o); 
    if(ndims >= CV_MAX_DIM) 
    { 
     failmsg("%s dimensionality (=%d) is too high", name, ndims); 
     return false; 
    } 

    int size[CV_MAX_DIM+1]; 
    size_t step[CV_MAX_DIM+1], elemsize = CV_ELEM_SIZE1(type); 
    const npy_intp* _sizes = PyArray_DIMS(o); 
    const npy_intp* _strides = PyArray_STRIDES(o); 
    bool transposed = false; 

    for(int i = 0; i < ndims; i++) 
    { 
     size[i] = (int)_sizes[i]; 
     step[i] = (size_t)_strides[i]; 
    } 

    if(ndims == 0 || step[ndims-1] > elemsize) { 
     size[ndims] = 1; 
     step[ndims] = elemsize; 
     ndims++; 
    } 

    if(ndims >= 2 && step[0] < step[1]) 
    { 
     std::swap(size[0], size[1]); 
     std::swap(step[0], step[1]); 
     transposed = true; 
    } 

    if(ndims == 3 && size[2] <= CV_CN_MAX && step[1] == elemsize*size[2]) 
    { 
     ndims--; 
     type |= CV_MAKETYPE(0, size[2]); 
    } 

    if(ndims > 2 && !allowND) 
    { 
     failmsg("%s has more than 2 dimensions", name); 
     return false; 
    } 

    m = Mat(ndims, size, type, PyArray_DATA(o), step); 

    if(m.data) 
    { 
     m.refcount = refcountFromPyObject(o); 
     m.addref(); // protect the original numpy array from deallocation 
        // (since Mat destructor will decrement the reference counter) 
    }; 
    m.allocator = &g_numpyAllocator; 

    if(transposed) 
    { 
     Mat tmp; 
     tmp.allocator = &g_numpyAllocator; 
     transpose(m, tmp); 
     m = tmp; 
    } 
    return true; 
} 

static PyObject* pyopencv_from(const Mat& m) 
{ 
    if(!m.data) 
     Py_RETURN_NONE; 
    Mat temp, *p = (Mat*)&m; 
    if(!p->refcount || p->allocator != &g_numpyAllocator) 
    { 
     temp.allocator = &g_numpyAllocator; 
     m.copyTo(temp); 
     p = &temp; 
    } 
    p->addref(); 
    return pyObjectFromRefcount(p->refcount); 
} 

mio pitone programma di test:

import pysomemodule # My python wrapped library. 
import cv2 

def main(): 
    myobj = pysomemodule.ABC("faces.train") # Create python object. This works. 
    image = cv2.imread('61.jpg') 
    processedImage = myobj.doSomething(image) 
    cv2.imshow("test", processedImage) 
    cv2.waitKey() 

if __name__ == "__main__": 
    main() 

risposta

29

ho risolto il problema così ho pensato io condividere qui con altri che possono avere lo stesso problema.

Fondamentalmente, per eliminare l'errore di segmentazione, devo chiamare la funzione import_array() di numpy.

Il "alto livello" vista per l'esecuzione di codice C++ in pitone è questo:

Supponiamo di avere una funzione foo(arg) in python che è un vincolante per qualche funzione C++. Quando chiami foo(myObj), ci deve essere del codice per convertire l'oggetto python "myObj" in una forma su cui il tuo codice C++ può agire. Questo codice è generalmente creato in modo semi-automatico usando strumenti come SWIG o Boost :: Python. (Io uso Boost :: Python negli esempi di seguito.)

Ora, foo(arg) è un collegamento Python per alcune funzioni C++. Questa funzione C++ riceverà un puntatore generico PyObject come argomento. Sarà necessario disporre di codice C++ per convertire questo puntatore PyObject in un oggetto C++ "equivalente". Nel mio caso, il mio codice python passa un array numpy OpenCV per un'immagine OpenCV come argomento della funzione. La forma "equivalente" in C++ è un oggetto OpenCV C++ Mat. OpenCV fornisce un codice in cv2.cpp (riprodotto di seguito) per convertire il puntatore PyObject (che rappresenta l'array numpy) in un Mat C++. Tipi di dati più semplici come int e string non hanno bisogno che l'utente scriva queste funzioni di conversione man mano che vengono convertite automaticamente da Boost :: Python.

Dopo che il puntatore PyObject viene convertito in un modulo C++ adatto, il codice C++ può agire su di esso. Quando i dati devono essere restituiti da C++ a python, si verifica una situazione analoga in cui il codice C++ è necessario per convertire la rappresentazione C++ dei dati in una qualche forma di PyObject. Boost :: Python si prenderà cura di tutto il resto convertendo lo PyObject in un modulo python corrispondente. Quando foo(arg) restituisce il risultato in python, è in una forma utilizzabile da python. Questo è tutto.

Il codice seguente mostra come avvolgere una classe C++ "ABC" ed esporre il suo metodo "doSomething" che accetta una matrice numpy (per un'immagine) da python, convertirla in C++ Mat di OpenCV, fare qualche elaborazione, convertire il risultato in PyObject * e lo restituisce all'interprete python. È possibile esporre tutte le funzioni/metodi desiderati (vedere i commenti nel codice seguente).

abc.hpp:

#ifndef ABC_HPP 
#define ABC_HPP 

#include <Python.h> 
#include <string> 

class ABC 
{ 
    // Other declarations 
    ABC(); 
    ABC(const std::string& someConfigFile); 
    virtual ~ABC(); 
    PyObject* doSomething(PyObject* image); // We want our python code to be able to call this function to do some processing using OpenCV and return the result. 
    // Other declarations 
}; 

#endif 

abc.cpp:

#include "abc.hpp" 
#include "my_cpp_library.h" // This is what we want to make available in python. It uses OpenCV to perform some processing. 

#include "numpy/ndarrayobject.h" 
#include "opencv2/core/core.hpp" 

// The following conversion functions are taken from OpenCV's cv2.cpp file inside modules/python/src2 folder. 
static PyObject* opencv_error = 0; 

static int failmsg(const char *fmt, ...) 
{ 
    char str[1000]; 

    va_list ap; 
    va_start(ap, fmt); 
    vsnprintf(str, sizeof(str), fmt, ap); 
    va_end(ap); 

    PyErr_SetString(PyExc_TypeError, str); 
    return 0; 
} 

class PyAllowThreads 
{ 
public: 
    PyAllowThreads() : _state(PyEval_SaveThread()) {} 
    ~PyAllowThreads() 
    { 
     PyEval_RestoreThread(_state); 
    } 
private: 
    PyThreadState* _state; 
}; 

class PyEnsureGIL 
{ 
public: 
    PyEnsureGIL() : _state(PyGILState_Ensure()) {} 
    ~PyEnsureGIL() 
    { 
     PyGILState_Release(_state); 
    } 
private: 
    PyGILState_STATE _state; 
}; 

#define ERRWRAP2(expr) \ 
try \ 
{ \ 
    PyAllowThreads allowThreads; \ 
    expr; \ 
} \ 
catch (const cv::Exception &e) \ 
{ \ 
    PyErr_SetString(opencv_error, e.what()); \ 
    return 0; \ 
} 

using namespace cv; 

static PyObject* failmsgp(const char *fmt, ...) 
{ 
    char str[1000]; 

    va_list ap; 
    va_start(ap, fmt); 
    vsnprintf(str, sizeof(str), fmt, ap); 
    va_end(ap); 

    PyErr_SetString(PyExc_TypeError, str); 
    return 0; 
} 

static size_t REFCOUNT_OFFSET = (size_t)&(((PyObject*)0)->ob_refcnt) + 
    (0x12345678 != *(const size_t*)"\x78\x56\x34\x12\0\0\0\0\0")*sizeof(int); 

static inline PyObject* pyObjectFromRefcount(const int* refcount) 
{ 
    return (PyObject*)((size_t)refcount - REFCOUNT_OFFSET); 
} 

static inline int* refcountFromPyObject(const PyObject* obj) 
{ 
    return (int*)((size_t)obj + REFCOUNT_OFFSET); 
} 

class NumpyAllocator : public MatAllocator 
{ 
public: 
    NumpyAllocator() {} 
    ~NumpyAllocator() {} 

    void allocate(int dims, const int* sizes, int type, int*& refcount, 
        uchar*& datastart, uchar*& data, size_t* step) 
    { 
     PyEnsureGIL gil; 

     int depth = CV_MAT_DEPTH(type); 
     int cn = CV_MAT_CN(type); 
     const int f = (int)(sizeof(size_t)/8); 
     int typenum = depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE : 
         depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT : 
         depth == CV_32S ? NPY_INT : depth == CV_32F ? NPY_FLOAT : 
         depth == CV_64F ? NPY_DOUBLE : f*NPY_ULONGLONG + (f^1)*NPY_UINT; 
     int i; 
     npy_intp _sizes[CV_MAX_DIM+1]; 
     for(i = 0; i < dims; i++) 
     { 
      _sizes[i] = sizes[i]; 
     } 

     if(cn > 1) 
     { 
      /*if(_sizes[dims-1] == 1) 
       _sizes[dims-1] = cn; 
      else*/ 
       _sizes[dims++] = cn; 
     } 

     PyObject* o = PyArray_SimpleNew(dims, _sizes, typenum); 

     if(!o) 
     { 
      CV_Error_(CV_StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims)); 
     } 
     refcount = refcountFromPyObject(o); 

     npy_intp* _strides = PyArray_STRIDES(o); 
     for(i = 0; i < dims - (cn > 1); i++) 
      step[i] = (size_t)_strides[i]; 
     datastart = data = (uchar*)PyArray_DATA(o); 
    } 

    void deallocate(int* refcount, uchar*, uchar*) 
    { 
     PyEnsureGIL gil; 
     if(!refcount) 
      return; 
     PyObject* o = pyObjectFromRefcount(refcount); 
     Py_INCREF(o); 
     Py_DECREF(o); 
    } 
}; 

NumpyAllocator g_numpyAllocator; 

enum { ARG_NONE = 0, ARG_MAT = 1, ARG_SCALAR = 2 }; 

static int pyopencv_to(const PyObject* o, Mat& m, const char* name = "<unknown>", bool allowND=true) 
{ 
    //NumpyAllocator g_numpyAllocator; 
    if(!o || o == Py_None) 
    { 
     if(!m.data) 
      m.allocator = &g_numpyAllocator; 
     return true; 
    } 

    if(!PyArray_Check(o)) 
    { 
     failmsg("%s is not a numpy array", name); 
     return false; 
    } 

    int typenum = PyArray_TYPE(o); 
    int type = typenum == NPY_UBYTE ? CV_8U : typenum == NPY_BYTE ? CV_8S : 
       typenum == NPY_USHORT ? CV_16U : typenum == NPY_SHORT ? CV_16S : 
       typenum == NPY_INT || typenum == NPY_LONG ? CV_32S : 
       typenum == NPY_FLOAT ? CV_32F : 
       typenum == NPY_DOUBLE ? CV_64F : -1; 

    if(type < 0) 
    { 
     failmsg("%s data type = %d is not supported", name, typenum); 
     return false; 
    } 

    int ndims = PyArray_NDIM(o); 
    if(ndims >= CV_MAX_DIM) 
    { 
     failmsg("%s dimensionality (=%d) is too high", name, ndims); 
     return false; 
    } 

    int size[CV_MAX_DIM+1]; 
    size_t step[CV_MAX_DIM+1], elemsize = CV_ELEM_SIZE1(type); 
    const npy_intp* _sizes = PyArray_DIMS(o); 
    const npy_intp* _strides = PyArray_STRIDES(o); 
    bool transposed = false; 

    for(int i = 0; i < ndims; i++) 
    { 
     size[i] = (int)_sizes[i]; 
     step[i] = (size_t)_strides[i]; 
    } 

    if(ndims == 0 || step[ndims-1] > elemsize) { 
     size[ndims] = 1; 
     step[ndims] = elemsize; 
     ndims++; 
    } 

    if(ndims >= 2 && step[0] < step[1]) 
    { 
     std::swap(size[0], size[1]); 
     std::swap(step[0], step[1]); 
     transposed = true; 
    } 

    if(ndims == 3 && size[2] <= CV_CN_MAX && step[1] == elemsize*size[2]) 
    { 
     ndims--; 
     type |= CV_MAKETYPE(0, size[2]); 
    } 

    if(ndims > 2 && !allowND) 
    { 
     failmsg("%s has more than 2 dimensions", name); 
     return false; 
    } 

    m = Mat(ndims, size, type, PyArray_DATA(o), step); 

    if(m.data) 
    { 
     m.refcount = refcountFromPyObject(o); 
     m.addref(); // protect the original numpy array from deallocation 
        // (since Mat destructor will decrement the reference counter) 
    }; 
    m.allocator = &g_numpyAllocator; 

    if(transposed) 
    { 
     Mat tmp; 
     tmp.allocator = &g_numpyAllocator; 
     transpose(m, tmp); 
     m = tmp; 
    } 
    return true; 
} 

static PyObject* pyopencv_from(const Mat& m) 
{ 
    if(!m.data) 
     Py_RETURN_NONE; 
    Mat temp, *p = (Mat*)&m; 
    if(!p->refcount || p->allocator != &g_numpyAllocator) 
    { 
     temp.allocator = &g_numpyAllocator; 
     m.copyTo(temp); 
     p = &temp; 
    } 
    p->addref(); 
    return pyObjectFromRefcount(p->refcount); 
} 

ABC::ABC() {} 
ABC::~ABC() {} 
// Note the import_array() from NumPy must be called else you will experience segmentation faults. 
ABC::ABC(const std::string &someConfigFile) 
{ 
    // Initialization code. Possibly store someConfigFile etc. 
    import_array(); // This is a function from NumPy that MUST be called. 
    // Do other stuff 
} 

// The conversions functions above are taken from OpenCV. The following function is 
// what we define to access the C++ code we are interested in. 
PyObject* ABC::doSomething(PyObject* image) 
{ 
    cv::Mat cvImage; 
    pyopencv_to(image, cvImage); // From OpenCV's source 

    MyCPPClass obj; // Some object from the C++ library. 
    cv::Mat processedImage = obj.process(cvImage); 

    return pyopencv_from(processedImage); // From OpenCV's source 
} 

il codice per utilizzare Boost Python per creare il modulo python. Ho preso questo e il seguente Makefile da http://jayrambhia.wordpress.com/tag/boost/:

pysomemodule.cpp:

#include <string>  
#include<boost/python.hpp> 
#include "abc.hpp" 

using namespace boost::python; 

BOOST_PYTHON_MODULE(pysomemodule) 
{ 
    class_<ABC>("ABC", init<const std::string &>()) 
     .def(init<const std::string &>()) 
     .def("doSomething", &ABC::doSomething) // doSomething is the method in class ABC you wish to expose. One line for each method (or function depending on how you structure your code). Note: You don't have to expose everything in the library, just the ones you wish to make available to python. 
    ; 
} 

E, infine, il Makefile (compilato su Ubuntu ma dovrebbe funzionare altrove, eventualmente, con aggiustamenti minimi).

PYTHON_VERSION = 2.7 
PYTHON_INCLUDE = /usr/include/python$(PYTHON_VERSION) 

# location of the Boost Python include files and library 
BOOST_INC = /usr/local/include/boost 
BOOST_LIB = /usr/local/lib 

OPENCV_LIB = `pkg-config --libs opencv` 
OPENCV_CFLAGS = `pkg-config --cflags opencv` 

MY_CPP_LIB = lib_my_cpp_library.so 

TARGET = pysomemodule 
SRC = pysomemodule.cpp abc.cpp 
OBJ = pysomemodule.o abc.o 

$(TARGET).so: $(OBJ) 
    g++ -shared $(OBJ) -L$(BOOST_LIB) -lboost_python -L/usr/lib/python$(PYTHON_VERSION)/config -lpython$(PYTHON_VERSION) -o $(TARGET).so $(OPENCV_LIB) $(MY_CPP_LIB) 

$(OBJ): $(SRC) 
    g++ -I$(PYTHON_INCLUDE) -I$(BOOST_INC) $(OPENCV_CFLAGS) -fPIC -c $(SRC) 

clean: 
    rm -f $(OBJ) 
    rm -f $(TARGET).so 

Dopo aver compilato con successo la biblioteca, si dovrebbe avere un file "pysomemodule.so" nella directory. Metti questo file lib in un posto accessibile dal tuo interprete python. È quindi possibile importare questo modulo e creare un'istanza della classe "ABC" di cui sopra come segue:

import pysomemodule 

foo = pysomemodule.ABC("config.txt") # This will create an instance of ABC 

Ora, data un'immagine serie NumPy OpenCV, possiamo chiamare la funzione C++ utilizzando:

processedImage = foo.doSomething(image) # Where the argument "image" is a OpenCV numpy image. 

Nota che avrai bisogno di Boost Python, Numpy dev e della libreria di sviluppo Python per creare i binding.

I documenti NumPy nei seguenti due collegamenti sono particolarmente utili per aiutare a comprendere i metodi utilizzati nel codice di conversione e perché deve essere chiamato import_array(). In particolare, il doc numpy ufficiale è utile per dare un senso al codice di binding Python di OpenCV.

http://dsnra.jpl.nasa.gov/software/Python/numpydoc/numpy-13.html http://docs.scipy.org/doc/numpy/user/c-info.how-to-extend.html

Spero che questo aiuti.

+0

Ciao, lightalchemist, grazie per aver postato la tua soluzione.Sono arrivato ad una soluzione simile allo stesso probem con OpenCV 2.4.3 (prendendo le funzioni pyopencv_to e pyopencv_from da cv2.cpp), ed esponendo una funzione. Il modulo si carica bene in ipython, la funzione è visibile lì, analizza gli argomenti, ma si blocca non appena raggiunge PyEnsureGIL. Ho provato la tua soluzione, e la vecchia funzione pyopencv_to ha funzionato (viene eseguita), ma si arresta in modo anomalo al tentativo di output. Inserirò una domanda separata e inserirò un link per te in un secondo nel caso in cui pensi di poter vedere qual è il problema. –

+0

Ecco il link alla mia domanda: http://stackoverflow.com/questions/13745265/exposing-opencv-based-c-function-with-mat-numpy-conversion-to-python –

5

Spero che questo aiuti le persone a cercare un modo facile e veloce.

Ecco lo github repo con il codice C++ aperto che ho scritto per esporre il codice usando la classe Mat di OpenCV con il minor dolore possibile.

[Update] Questo codice ora lavora per OpenCV 2.X e OpenCV 3.X. Sono ora disponibili anche CMake e il supporto sperimentale per Python 3.X.

+1

Il codice per la versione 3.x è grande! All'inizio pensavo che fosse memleak, ma alla fine si è scoperto che il bug era nel mio codice;) E per la versione 2.xi raccomandiamo questo https://github.com/spillai/numpy-opencv-converter - good codice con convertitori per molti tipi (come Mat, Mat3f, Point2d ecc. - God bless templates :)). – cyriel

+0

Ho anche riscontrato un errore di segmentazione utilizzando il codice. Aggiungendo un 'import pbcvt' nel mio codice Python _before_ l'importazione della mia libreria l'ha finalmente risolto. (Non è intuitivo per me il motivo per cui è necessario, tuttavia, lo è.) –

+0

@ManuCJ, hai ragione, è davvero strano. pbcvt è davvero progettato come un modello di codice per la tua libreria. Quindi, vorrei vedere che cosa ha il codice pbcvt che il tuo codice non ha (alcune di quelle definite nei file header e cpp, forse?). Se sei ancora interessato da questo problema, ti consiglio di inviarlo su GitHub con un esempio di codice minimo. –