python c++ opencv cuda

Acceso a las funciones OpenCV CUDA desde Python(No PyCUDA)



c++ (4)

Estoy escribiendo una aplicación Python que utiliza los enlaces Python de OpenCV para hacer la detección de marcadores y otro procesamiento de imágenes. Me gustaría usar los módulos CUDA de OpenCV para acelerar CUDA ciertas partes de mi aplicación, y noté en sus archivos .hpp que parecen estar usando las macros de exportación de OpenCV para Python y Java. Sin embargo, parece que no puedo acceder a esas funciones de CUDA, aunque estoy construyendo OpenCV WITH_CUDA=ON .

¿Es necesario usar un contenedor como PyCUDA para acceder a las funciones de la GPU, como el umbral en el cudaaritmo? ¿O ya se están utilizando estas funciones aceleradas por CUDA si llamo a cv2.threshold () en mi código de Python (en lugar de la implementación normal basada en CPU)?

CV_EXPORTS double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type, Stream& stream = Stream::Null());

Los submódulos que veo para cv2 son los siguientes:

  • Error
  • aruco
  • detalle
  • ojo de pez
  • flann
  • instr
  • ml
  • ocl
  • ogl
  • videostab

cv2.cuda , cv2.gpu y cv2.cudaarithm regresan con un AttributeError .

La instrucción CMake que estoy ejecutando para construir OpenCV es la siguiente:

cmake -DOPENCV_EXTRA_MODULES_PATH=/usr/local/lib/opencv_contrib/modules/ / -D WITH_CUDA=ON -D CUDA_FAST_MATH=1 / -D ENABLE_PRECOMPILED_HEADERS=OFF / -D BUILD_TESTS=OFF -D BUILD_PERF_TESTS=OFF -D BUILD_EXAMPLES=OFF / -D BUILD_opencv_java=OFF / -DBUILD_opencv_bgsegm=OFF -DBUILD_opencv_bioinspired=OFF -DBUILD_opencv_ccalib=OFF -DBUILD_opencv_cnn_3dobj=OFF -DBUILD_opencv_contrib_world=OFF -DBUILD_opencv_cvv=OFF -DBUILD_opencv_datasets=OFF -DBUILD_openc v_dnn=OFF -DBUILD_opencv_dnns_easily_fooled=OFF -DBUILD_opencv_dpm=OFF -DBUILD_opencv_face=OFF -DBUILD_opencv_fuzzy=OFF -DBUILD_opencv_hdf=OFF -DBUILD_opencv_line_descriptor=OFF -DBUILD_opencv_matlab=OFF -DBUILD_o pencv_optflow=OFF -DBUILD_opencv_plot=OFF -DBUILD_opencv_README.md=OFF -DBUILD_opencv_reg=OFF -DBUILD_opencv_rgbd=OFF -DBUILD_opencv_saliency=OFF -DBUILD_opencv_sfm=OFF -DBUILD_opencv_stereo=OFF -DBUILD_opencv_str uctured_light=OFF -DBUILD_opencv_surface_matching=OFF -DBUILD_opencv_text=OFF -DBUILD_opencv_tracking=OFF -DBUILD_opencv_viz=OFF -DBUILD_opencv_xfeatures2d=OFF -DBUILD_opencv_ximgproc=OFF -DBUILD_opencv_xobjdetect =OFF -DBUILD_opencv_xphoto=OFF ..


O, ¿estas funciones aceleradas por CUDA ya se están utilizando si invoco cv2.threshold () en mi código de Python (en lugar de la implementación regular basada en CPU)

No, debe llamarlos explícitamente desde el módulo acelerado de GPU. Llamar a cv2.threshold () simplemente ejecutará la versión de la CPU.

Dado que la API de Python de OpenCV se ajusta a las funciones de C ++, la comprobación de la API de C ++ generalmente ofrece pistas útiles sobre dónde están las funciones / módulos.

Por ejemplo, en this guía de transición puede ver los cambios de API que se hicieron de OpenCV 2.X a 3.X. Aquí, se puede acceder al módulo GPU en OpenCV 3.X mediante cv2.cuda y cv2.gpu en versiones anteriores. Y el módulo cuda en 3.X se divide en varias piezas pequeñas:

  • cuda - Visión por computadora acelerada por CUDA
  • cudaaritmo - Operaciones en matrices
  • cudabgsegm - Segmentación de fondo
  • cudacodec - Codificación / Decodificación de video
  • cudafeatures2d - Detección y descripción de funciones
  • cudafilters - Filtrado de imágenes
  • cudaimgproc - Procesamiento de imágenes
  • cudalegacy - Soporte heredado
  • cudaoptflow - flujo óptico
  • cudastereo - Correspondencia estéreo
  • cudawarping - Image Warping
  • cudev - Capa del dispositivo

Debe buscar estos módulos en cv2.


Entonces, como se confirma en la respuesta y el hilo de comentarios con @NAmorim, no hay enlaces Python accesibles a los diversos módulos CUDA de OpenCV .

Pude sortear esta restricción usando Cython para obtener acceso a las funciones CUDA que necesitaba e implementando la lógica necesaria para convertir mis objetos Python (principalmente matrices NumPy) en objetos OpenCV C / C ++ y viceversa.

Código de trabajo

Primero escribí un archivo de definición de Cython, GpuWrapper.pxd . El propósito de este archivo es hacer referencia a clases y métodos externos de C / C ++, como los métodos CUDA que me interesan.

from libcpp cimport bool from cpython.ref cimport PyObject # References PyObject to OpenCV object conversion code borrowed from OpenCV''s own conversion file, cv2.cpp cdef extern from ''pyopencv_converter.cpp'': cdef PyObject* pyopencv_from(const Mat& m) cdef bool pyopencv_to(PyObject* o, Mat& m) cdef extern from ''opencv2/imgproc.hpp'' namespace ''cv'': cdef enum InterpolationFlags: INTER_NEAREST = 0 cdef enum ColorConversionCodes: COLOR_BGR2GRAY cdef extern from ''opencv2/core/core.hpp'': cdef int CV_8UC1 cdef int CV_32FC1 cdef extern from ''opencv2/core/core.hpp'' namespace ''cv'': cdef cppclass Size_[T]: Size_() except + Size_(T width, T height) except + T width T height ctypedef Size_[int] Size2i ctypedef Size2i Size cdef cppclass Scalar[T]: Scalar() except + Scalar(T v0) except + cdef extern from ''opencv2/core/core.hpp'' namespace ''cv'': cdef cppclass Mat: Mat() except + void create(int, int, int) except + void* data int rows int cols cdef extern from ''opencv2/core/cuda.hpp'' namespace ''cv::cuda'': cdef cppclass GpuMat: GpuMat() except + void upload(Mat arr) except + void download(Mat dst) const cdef cppclass Stream: Stream() except + cdef extern from ''opencv2/cudawarping.hpp'' namespace ''cv::cuda'': cdef void warpPerspective(GpuMat src, GpuMat dst, Mat M, Size dsize, int flags, int borderMode, Scalar borderValue, Stream& stream) # Function using default values cdef void warpPerspective(GpuMat src, GpuMat dst, Mat M, Size dsize, int flags)

También necesitamos la capacidad de convertir objetos Python en objetos OpenCV. Copié los primeros cientos de líneas de los modules/python/src2/cv2.cpp . Puede encontrar ese código a continuación en el apéndice.

¡Finalmente podemos escribir nuestros métodos de envoltura de Cython para llamar a las funciones CUDA de OpenCV! Esto es parte del archivo de implementación de Cython, GpuWrapper.pyx .

import numpy as np # Import Python functions, attributes, submodules of numpy cimport numpy as np # Import numpy C/C++ API def cudaWarpPerspectiveWrapper(np.ndarray[np.uint8_t, ndim=2] _src, np.ndarray[np.float32_t, ndim=2] _M, _size_tuple, int _flags=INTER_NEAREST): # Create GPU/device InputArray for src cdef Mat src_mat cdef GpuMat src_gpu pyopencv_to(<PyObject*> _src, src_mat) src_gpu.upload(src_mat) # Create CPU/host InputArray for M cdef Mat M_mat = Mat() pyopencv_to(<PyObject*> _M, M_mat) # Create Size object from size tuple # Note that size/shape in Python is handled in row-major-order -- therefore, width is [1] and height is [0] cdef Size size = Size(<int> _size_tuple[1], <int> _size_tuple[0]) # Create empty GPU/device OutputArray for dst cdef GpuMat dst_gpu = GpuMat() warpPerspective(src_gpu, dst_gpu, M_mat, size, INTER_NEAREST) # Get result of dst cdef Mat dst_host dst_gpu.download(dst_host) cdef np.ndarray out = <np.ndarray> pyopencv_from(dst_host) return out

Después de ejecutar un script de configuración para cythonize y compilar este código (consulte el apéndice), podemos importar GpuWrapper como un módulo de Python y ejecutar cudaWarpPerspectiveWrapper . Esto me permitió ejecutar el código a través de CUDA con solo una falta de coincidencia de 0.34722222222222854%, ¡bastante emocionante!

Referencias (solo puede publicar un máximo de 2)

Apéndice

pyopencv_converter.cpp

#include <Python.h> #include "numpy/ndarrayobject.h" #include "opencv2/core/core.hpp" static PyObject* opencv_error = 0; // === FAIL MESSAGE ==================================================================================================== 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; } struct ArgInfo { const char * name; bool outputarg; // more fields may be added if necessary ArgInfo(const char * name_, bool outputarg_) : name(name_) , outputarg(outputarg_) {} // to match with older pyopencv_to function signature operator const char *() const { return name; } }; // === THREADING ======================================================================================================= 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; }; // === ERROR HANDLING ================================================================================================== #define ERRWRAP2(expr) / try / { / PyAllowThreads allowThreads; / expr; / } / catch (const cv::Exception &e) / { / PyErr_SetString(opencv_error, e.what()); / return 0; / } // === USING NAMESPACE CV ============================================================================================== using namespace cv; // === NUMPY ALLOCATOR ================================================================================================= class NumpyAllocator : public MatAllocator { public: NumpyAllocator() { stdAllocator = Mat::getStdAllocator(); } ~NumpyAllocator() {} UMatData* allocate(PyObject* o, int dims, const int* sizes, int type, size_t* step) const { UMatData* u = new UMatData(this); u->data = u->origdata = (uchar*)PyArray_DATA((PyArrayObject*) o); npy_intp* _strides = PyArray_STRIDES((PyArrayObject*) o); for( int i = 0; i < dims - 1; i++ ) step[i] = (size_t)_strides[i]; step[dims-1] = CV_ELEM_SIZE(type); u->size = sizes[0]*step[0]; u->userdata = o; return u; } UMatData* allocate(int dims0, const int* sizes, int type, void* data, size_t* step, int flags, UMatUsageFlags usageFlags) const { if( data != 0 ) { CV_Error(Error::StsAssert, "The data should normally be NULL!"); // probably this is safe to do in such extreme case return stdAllocator->allocate(dims0, sizes, type, data, step, flags, usageFlags); } 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, dims = dims0; cv::AutoBuffer<npy_intp> _sizes(dims + 1); for( i = 0; i < dims; i++ ) _sizes[i] = sizes[i]; if( cn > 1 ) _sizes[dims++] = cn; PyObject* o = PyArray_SimpleNew(dims, _sizes, typenum); if(!o) CV_Error_(Error::StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims)); return allocate(o, dims0, sizes, type, step); } bool allocate(UMatData* u, int accessFlags, UMatUsageFlags usageFlags) const { return stdAllocator->allocate(u, accessFlags, usageFlags); } void deallocate(UMatData* u) const { if(!u) return; PyEnsureGIL gil; CV_Assert(u->urefcount >= 0); CV_Assert(u->refcount >= 0); if(u->refcount == 0) { PyObject* o = (PyObject*)u->userdata; Py_XDECREF(o); delete u; } } const MatAllocator* stdAllocator; }; // === ALLOCATOR INITIALIZATION ======================================================================================== NumpyAllocator g_numpyAllocator; // === CONVERTOR FUNCTIONS ============================================================================================= template<typename T> static bool pyopencv_to(PyObject* obj, T& p, const char* name = "<unknown>"); template<typename T> static PyObject* pyopencv_from(const T& src); enum { ARG_NONE = 0, ARG_MAT = 1, ARG_SCALAR = 2 }; // special case, when the convertor needs full ArgInfo structure static bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo info) { bool allowND = true; if(!o || o == Py_None) { if( !m.data ) m.allocator = &g_numpyAllocator; return true; } if( PyInt_Check(o) ) { double v[] = {static_cast<double>(PyInt_AsLong((PyObject*)o)), 0., 0., 0.}; m = Mat(4, 1, CV_64F, v).clone(); return true; } if( PyFloat_Check(o) ) { double v[] = {PyFloat_AsDouble((PyObject*)o), 0., 0., 0.}; m = Mat(4, 1, CV_64F, v).clone(); return true; } if( PyTuple_Check(o) ) { int i, sz = (int)PyTuple_Size((PyObject*)o); m = Mat(sz, 1, CV_64F); for( i = 0; i < sz; i++ ) { PyObject* oi = PyTuple_GET_ITEM(o, i); if( PyInt_Check(oi) ) m.at<double>(i) = (double)PyInt_AsLong(oi); else if( PyFloat_Check(oi) ) m.at<double>(i) = (double)PyFloat_AsDouble(oi); else { failmsg("%s is not a numerical tuple", info.name); m.release(); return false; } } return true; } if( !PyArray_Check(o) ) { failmsg("%s is not a numpy array, neither a scalar", info.name); return false; } PyArrayObject* oarr = (PyArrayObject*) o; bool needcopy = false, needcast = false; int typenum = PyArray_TYPE(oarr), new_typenum = typenum; 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 ? CV_32S : typenum == NPY_INT32 ? CV_32S : typenum == NPY_FLOAT ? CV_32F : typenum == NPY_DOUBLE ? CV_64F : -1; if( type < 0 ) { if( typenum == NPY_INT64 || typenum == NPY_UINT64 || typenum == NPY_LONG ) { needcopy = needcast = true; new_typenum = NPY_INT; type = CV_32S; } else { failmsg("%s data type = %d is not supported", info.name, typenum); return false; } } #ifndef CV_MAX_DIM const int CV_MAX_DIM = 32; #endif int ndims = PyArray_NDIM(oarr); if(ndims >= CV_MAX_DIM) { failmsg("%s dimensionality (=%d) is too high", info.name, ndims); return false; } int size[CV_MAX_DIM+1]; size_t step[CV_MAX_DIM+1]; size_t elemsize = CV_ELEM_SIZE1(type); const npy_intp* _sizes = PyArray_DIMS(oarr); const npy_intp* _strides = PyArray_STRIDES(oarr); bool ismultichannel = ndims == 3 && _sizes[2] <= CV_CN_MAX; for( int i = ndims-1; i >= 0 && !needcopy; i-- ) { // these checks handle cases of // a) multi-dimensional (ndims > 2) arrays, as well as simpler 1- and 2-dimensional cases // b) transposed arrays, where _strides[] elements go in non-descending order // c) flipped arrays, where some of _strides[] elements are negative // the _sizes[i] > 1 is needed to avoid spurious copies when NPY_RELAXED_STRIDES is set if( (i == ndims-1 && _sizes[i] > 1 && (size_t)_strides[i] != elemsize) || (i < ndims-1 && _sizes[i] > 1 && _strides[i] < _strides[i+1]) ) needcopy = true; } if( ismultichannel && _strides[1] != (npy_intp)elemsize*_sizes[2] ) needcopy = true; if (needcopy) { if (info.outputarg) { failmsg("Layout of the output array %s is incompatible with cv::Mat (step[ndims-1] != elemsize or step[1] != elemsize*nchannels)", info.name); return false; } if( needcast ) { o = PyArray_Cast(oarr, new_typenum); oarr = (PyArrayObject*) o; } else { oarr = PyArray_GETCONTIGUOUS(oarr); o = (PyObject*) oarr; } _strides = PyArray_STRIDES(oarr); } // Normalize strides in case NPY_RELAXED_STRIDES is set size_t default_step = elemsize; for ( int i = ndims - 1; i >= 0; --i ) { size[i] = (int)_sizes[i]; if ( size[i] > 1 ) { step[i] = (size_t)_strides[i]; default_step = step[i] * size[i]; } else { step[i] = default_step; default_step *= size[i]; } } // handle degenerate case if( ndims == 0) { size[ndims] = 1; step[ndims] = elemsize; ndims++; } if( ismultichannel ) { ndims--; type |= CV_MAKETYPE(0, size[2]); } if( ndims > 2 && !allowND ) { failmsg("%s has more than 2 dimensions", info.name); return false; } m = Mat(ndims, size, type, PyArray_DATA(oarr), step); m.u = g_numpyAllocator.allocate(o, ndims, size, type, step); m.addref(); if( !needcopy ) { Py_INCREF(o); } m.allocator = &g_numpyAllocator; return true; } template<> bool pyopencv_to(PyObject* o, Mat& m, const char* name) { return pyopencv_to(o, m, ArgInfo(name, 0)); } template<> PyObject* pyopencv_from(const Mat& m) { if( !m.data ) Py_RETURN_NONE; Mat temp, *p = (Mat*)&m; if(!p->u || p->allocator != &g_numpyAllocator) { temp.allocator = &g_numpyAllocator; ERRWRAP2(m.copyTo(temp)); p = &temp; } PyObject* o = (PyObject*)p->u->userdata; Py_INCREF(o); return o; }

setupGpuWrapper.py

import subprocess import os import numpy as np from distutils.core import setup, Extension from Cython.Build import cythonize from Cython.Distutils import build_ext """ Run setup with the following command: ``` python setupGpuWrapper.py build_ext --inplace ``` """ # Determine current directory of this setup file to find our module CUR_DIR = os.path.dirname(__file__) # Use pkg-config to determine library locations and include locations opencv_libs_str = subprocess.check_output("pkg-config --libs opencv".split()).decode() opencv_incs_str = subprocess.check_output("pkg-config --cflags opencv".split()).decode() # Parse into usable format for Extension call opencv_libs = [str(lib) for lib in opencv_libs_str.strip().split()] opencv_incs = [str(inc) for inc in opencv_incs_str.strip().split()] extensions = [ Extension(''GpuWrapper'', sources=[os.path.join(CUR_DIR, ''GpuWrapper.pyx'')], language=''c++'', include_dirs=[np.get_include()] + opencv_incs, extra_link_args=opencv_libs) ] setup( cmdclass={''build_ext'': build_ext}, name="GpuWrapper", ext_modules=cythonize(extensions) )


Hice algunas pruebas sobre esto con OpenCV 4.0.0. @nchaumont mencionó que a partir de OpenCV 4, había enlaces de Python para CUDA incluidos.

Al menos desde Open CV 4.1.0, posiblemente antes, los enlaces de Python predeterminados incluyen CUDA, siempre que Open CV se haya creado con soporte CUDA.

La mayor parte de la funcionalidad parece estar expuesta como cv2.cuda.thing (por ejemplo, cv2.cuda.cvtColor() .

Actualmente, carecen de documentación en línea; por ejemplo, el detector de borde Canny de GPU no menciona a Python. Sin embargo, puede usar la función de help en REPL de Python para ver los documentos de C ++, que deberían ser en su mayoría equivalentes.


Utilicé la siguiente forma de acceder a los métodos C ++ CUDA de OpenCV en Python:

  1. Crear un módulo personalizado opencv_contrib
  2. Escriba código C ++ para envolver el método OpenCV CUDA
  3. Usando enlaces de Python de OpenCV, exponga su método personalizado
  4. Construir opencv con opencv_contrib
  5. Ejecute el código de Python para probar

Creé un pequeño repositorio de Github para demostrar lo mismo