diff --git a/modules/gapi/include/opencv2/gapi/core.hpp b/modules/gapi/include/opencv2/gapi/core.hpp index cb5d55d13f..597315136f 100644 --- a/modules/gapi/include/opencv2/gapi/core.hpp +++ b/modules/gapi/include/opencv2/gapi/core.hpp @@ -1490,7 +1490,7 @@ enlarge an image, it will generally look best with cv::INTER_CUBIC (slow) or cv: @sa warpAffine, warpPerspective, remap, resizeP */ -GAPI_EXPORTS GMat resize(const GMat& src, const Size& dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR); +GAPI_EXPORTS_W GMat resize(const GMat& src, const Size& dsize, double fx = 0, double fy = 0, int interpolation = INTER_LINEAR); /** @brief Resizes a planar image. diff --git a/modules/gapi/include/opencv2/gapi/gmat.hpp b/modules/gapi/include/opencv2/gapi/gmat.hpp index 5e567fb107..11c3071f03 100644 --- a/modules/gapi/include/opencv2/gapi/gmat.hpp +++ b/modules/gapi/include/opencv2/gapi/gmat.hpp @@ -120,7 +120,7 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // Meta combinator: return a new GMatDesc which differs in size by delta // (all other fields are taken unchanged from this GMatDesc) // FIXME: a better name? - GMatDesc withSizeDelta(cv::Size delta) const + GAPI_WRAP GMatDesc withSizeDelta(cv::Size delta) const { GMatDesc desc(*this); desc.size += delta; @@ -130,12 +130,12 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // (all other fields are taken unchanged from this GMatDesc) // // This is an overload. - GMatDesc withSizeDelta(int dx, int dy) const + GAPI_WRAP GMatDesc withSizeDelta(int dx, int dy) const { return withSizeDelta(cv::Size{dx,dy}); } - GMatDesc withSize(cv::Size sz) const + GAPI_WRAP GMatDesc withSize(cv::Size sz) const { GMatDesc desc(*this); desc.size = sz; @@ -144,7 +144,7 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // Meta combinator: return a new GMatDesc with specified data depth. // (all other fields are taken unchanged from this GMatDesc) - GMatDesc withDepth(int ddepth) const + GAPI_WRAP GMatDesc withDepth(int ddepth) const { GAPI_Assert(CV_MAT_CN(ddepth) == 1 || ddepth == -1); GMatDesc desc(*this); @@ -166,7 +166,7 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // Meta combinator: return a new GMatDesc with planar flag set // (no size changes are performed, only channel interpretation is changed // (interleaved -> planar) - GMatDesc asPlanar() const + GAPI_WRAP GMatDesc asPlanar() const { GAPI_Assert(planar == false); GMatDesc desc(*this); @@ -177,7 +177,7 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // Meta combinator: return a new GMatDesc // reinterpreting 1-channel input as planar image // (size height is divided by plane number) - GMatDesc asPlanar(int planes) const + GAPI_WRAP GMatDesc asPlanar(int planes) const { GAPI_Assert(planar == false); GAPI_Assert(chan == 1); @@ -192,7 +192,7 @@ struct GAPI_EXPORTS_W_SIMPLE GMatDesc // Meta combinator: return a new GMatDesc with planar flag set to false // (no size changes are performed, only channel interpretation is changed // (planar -> interleaved) - GMatDesc asInterleaved() const + GAPI_WRAP GMatDesc asInterleaved() const { GAPI_Assert(planar == true); GMatDesc desc(*this); diff --git a/modules/gapi/include/opencv2/gapi/imgproc.hpp b/modules/gapi/include/opencv2/gapi/imgproc.hpp index 25a64a5067..2dbe626ff1 100644 --- a/modules/gapi/include/opencv2/gapi/imgproc.hpp +++ b/modules/gapi/include/opencv2/gapi/imgproc.hpp @@ -1341,7 +1341,7 @@ Output image is 8-bit unsigned 3-channel image @ref CV_8UC3. @param src input image: 8-bit unsigned 3-channel image @ref CV_8UC3. @sa RGB2BGR */ -GAPI_EXPORTS GMat BGR2RGB(const GMat& src); +GAPI_EXPORTS_W GMat BGR2RGB(const GMat& src); /** @brief Converts an image from RGB color space to gray-scaled. diff --git a/modules/gapi/misc/python/package/gapi/__init__.py b/modules/gapi/misc/python/package/gapi/__init__.py new file mode 100644 index 0000000000..733c980010 --- /dev/null +++ b/modules/gapi/misc/python/package/gapi/__init__.py @@ -0,0 +1,246 @@ +__all__ = ['op', 'kernel'] + +import sys +import cv2 as cv + +# NB: Register function in specific module +def register(mname): + def parameterized(func): + sys.modules[mname].__dict__[func.__name__] = func + return func + return parameterized + + +@register('cv2') +class GOpaque(): + # NB: Inheritance from c++ class cause segfault. + # So just aggregate cv.GOpaqueT instead of inheritance + def __new__(cls, argtype): + return cv.GOpaqueT(argtype) + + class Bool(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_BOOL) + + class Int(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_INT) + + class Double(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_DOUBLE) + + class Float(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_FLOAT) + + class String(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_STRING) + + class Point(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_POINT) + + class Point2f(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_POINT2F) + + class Size(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_SIZE) + + class Rect(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_RECT) + + class Any(): + def __new__(self): + return cv.GOpaqueT(cv.gapi.CV_ANY) + +@register('cv2') +class GArray(): + # NB: Inheritance from c++ class cause segfault. + # So just aggregate cv.GArrayT instead of inheritance + def __new__(cls, argtype): + return cv.GArrayT(argtype) + + class Bool(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_BOOL) + + class Int(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_INT) + + class Double(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_DOUBLE) + + class Float(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_FLOAT) + + class String(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_STRING) + + class Point(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_POINT) + + class Point2f(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_POINT2F) + + class Size(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_SIZE) + + class Rect(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_RECT) + + class Scalar(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_SCALAR) + + class Mat(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_MAT) + + class GMat(): + def __new__(self): + return cv.GArrayT(cv.gapi.CV_GMAT) + + class Any(): + def __new__(self): + return cv.GArray(cv.gapi.CV_ANY) + + +# NB: Top lvl decorator takes arguments +def op(op_id, in_types, out_types): + + garray_types= { + cv.GArray.Bool: cv.gapi.CV_BOOL, + cv.GArray.Int: cv.gapi.CV_INT, + cv.GArray.Double: cv.gapi.CV_DOUBLE, + cv.GArray.Float: cv.gapi.CV_FLOAT, + cv.GArray.String: cv.gapi.CV_STRING, + cv.GArray.Point: cv.gapi.CV_POINT, + cv.GArray.Point2f: cv.gapi.CV_POINT2F, + cv.GArray.Size: cv.gapi.CV_SIZE, + cv.GArray.Rect: cv.gapi.CV_RECT, + cv.GArray.Scalar: cv.gapi.CV_SCALAR, + cv.GArray.Mat: cv.gapi.CV_MAT, + cv.GArray.GMat: cv.gapi.CV_GMAT, + cv.GArray.Any: cv.gapi.CV_ANY + } + + gopaque_types= { + cv.GOpaque.Size: cv.gapi.CV_SIZE, + cv.GOpaque.Rect: cv.gapi.CV_RECT, + cv.GOpaque.Bool: cv.gapi.CV_BOOL, + cv.GOpaque.Int: cv.gapi.CV_INT, + cv.GOpaque.Double: cv.gapi.CV_DOUBLE, + cv.GOpaque.Float: cv.gapi.CV_FLOAT, + cv.GOpaque.String: cv.gapi.CV_STRING, + cv.GOpaque.Point: cv.gapi.CV_POINT, + cv.GOpaque.Point2f: cv.gapi.CV_POINT2F, + cv.GOpaque.Size: cv.gapi.CV_SIZE, + cv.GOpaque.Rect: cv.gapi.CV_RECT, + cv.GOpaque.Any: cv.gapi.CV_ANY + } + + type2str = { + cv.gapi.CV_BOOL: 'cv.gapi.CV_BOOL' , + cv.gapi.CV_INT: 'cv.gapi.CV_INT' , + cv.gapi.CV_DOUBLE: 'cv.gapi.CV_DOUBLE' , + cv.gapi.CV_FLOAT: 'cv.gapi.CV_FLOAT' , + cv.gapi.CV_STRING: 'cv.gapi.CV_STRING' , + cv.gapi.CV_POINT: 'cv.gapi.CV_POINT' , + cv.gapi.CV_POINT2F: 'cv.gapi.CV_POINT2F' , + cv.gapi.CV_SIZE: 'cv.gapi.CV_SIZE', + cv.gapi.CV_RECT: 'cv.gapi.CV_RECT', + cv.gapi.CV_SCALAR: 'cv.gapi.CV_SCALAR', + cv.gapi.CV_MAT: 'cv.gapi.CV_MAT', + cv.gapi.CV_GMAT: 'cv.gapi.CV_GMAT' + } + + # NB: Second lvl decorator takes class to decorate + def op_with_params(cls): + if not in_types: + raise Exception('{} operation should have at least one input!'.format(cls.__name__)) + + if not out_types: + raise Exception('{} operation should have at least one output!'.format(cls.__name__)) + + for i, t in enumerate(out_types): + if t not in [cv.GMat, cv.GScalar, *garray_types, *gopaque_types]: + raise Exception('{} unsupported output type: {} in possition: {}' + .format(cls.__name__, t.__name__, i)) + + def on(*args): + if len(in_types) != len(args): + raise Exception('Invalid number of input elements!\nExpected: {}, Actual: {}' + .format(len(in_types), len(args))) + + for i, (t, a) in enumerate(zip(in_types, args)): + if t in garray_types: + if not isinstance(a, cv.GArrayT): + raise Exception("{} invalid type for argument {}.\nExpected: {}, Actual: {}" + .format(cls.__name__, i, cv.GArrayT.__name__, type(a).__name__)) + + elif a.type() != garray_types[t]: + raise Exception("{} invalid GArrayT type for argument {}.\nExpected: {}, Actual: {}" + .format(cls.__name__, i, type2str[garray_types[t]], type2str[a.type()])) + + elif t in gopaque_types: + if not isinstance(a, cv.GOpaqueT): + raise Exception("{} invalid type for argument {}.\nExpected: {}, Actual: {}" + .format(cls.__name__, i, cv.GOpaqueT.__name__, type(a).__name__)) + + elif a.type() != gopaque_types[t]: + raise Exception("{} invalid GOpaque type for argument {}.\nExpected: {}, Actual: {}" + .format(cls.__name__, i, type2str[gopaque_types[t]], type2str[a.type()])) + + else: + if t != type(a): + raise Exception('{} invalid input type for argument {}.\nExpected: {}, Actual: {}' + .format(cls.__name__, i, t.__name__, type(a).__name__)) + + op = cv.gapi.__op(op_id, cls.outMeta, *args) + + out_protos = [] + for i, out_type in enumerate(out_types): + if out_type == cv.GMat: + out_protos.append(op.getGMat()) + elif out_type == cv.GScalar: + out_protos.append(op.getGScalar()) + elif out_type in gopaque_types: + out_protos.append(op.getGOpaque(gopaque_types[out_type])) + elif out_type in garray_types: + out_protos.append(op.getGArray(garray_types[out_type])) + else: + raise Exception("""In {}: G-API operation can't produce the output with type: {} in position: {}""" + .format(cls.__name__, out_type.__name__, i)) + + return tuple(out_protos) if len(out_protos) != 1 else out_protos[0] + + # NB: Extend operation class + cls.id = op_id + cls.on = staticmethod(on) + return cls + + return op_with_params + + +def kernel(op_cls): + # NB: Second lvl decorator takes class to decorate + def kernel_with_params(cls): + # NB: Add new members to kernel class + cls.id = op_cls.id + cls.outMeta = op_cls.outMeta + return cls + + return kernel_with_params diff --git a/modules/gapi/misc/python/pyopencv_gapi.hpp b/modules/gapi/misc/python/pyopencv_gapi.hpp index 56a7e70d88..3ade7aaa4e 100644 --- a/modules/gapi/misc/python/pyopencv_gapi.hpp +++ b/modules/gapi/misc/python/pyopencv_gapi.hpp @@ -5,7 +5,6 @@ #ifdef _MSC_VER #pragma warning(disable: 4503) // "decorated name length exceeded" - // on empty_meta(const cv::GMetaArgs&, const cv::GArgs&) #endif #include @@ -49,6 +48,121 @@ using GArray_GMat = cv::GArray; // WA: Create using using std::string; +namespace cv +{ +namespace detail +{ + +class PyObjectHolder +{ +public: + PyObjectHolder(PyObject* o, bool owner = true); + PyObject* get() const; + +private: + class Impl; + std::shared_ptr m_impl; +}; + +} // namespace detail +} // namespace cv + +class cv::detail::PyObjectHolder::Impl +{ +public: + Impl(PyObject* object, bool owner); + PyObject* get() const; + ~Impl(); + +private: + PyObject* m_object; +}; + +cv::detail::PyObjectHolder::Impl::Impl(PyObject* object, bool owner) + : m_object(object) +{ + // NB: Become an owner of that PyObject. + // Need to store this and get access + // after the caller which provide the object is out of range. + if (owner) + { + // NB: Impossible take ownership if object is NULL. + GAPI_Assert(object); + Py_INCREF(m_object); + } +} + +cv::detail::PyObjectHolder::Impl::~Impl() +{ + // NB: If NULL was set, don't decrease counter. + if (m_object) + { + Py_DECREF(m_object); + } +} + +PyObject* cv::detail::PyObjectHolder::Impl::get() const +{ + return m_object; +} + +cv::detail::PyObjectHolder::PyObjectHolder(PyObject* object, bool owner) + : m_impl(new cv::detail::PyObjectHolder::Impl{object, owner}) +{ +} + +PyObject* cv::detail::PyObjectHolder::get() const +{ + return m_impl->get(); +} + +template<> +PyObject* pyopencv_from(const cv::detail::PyObjectHolder& v) +{ + PyObject* o = cv::util::any_cast(v).get(); + Py_INCREF(o); + return o; +} + +template<> +PyObject* pyopencv_from(const cv::GArg& value) +{ + GAPI_Assert(value.kind != cv::detail::ArgKind::GOBJREF); +#define HANDLE_CASE(T, O) case cv::detail::OpaqueKind::CV_##T: \ + { \ + return pyopencv_from(value.get()); \ + } + +#define UNSUPPORTED(T) case cv::detail::OpaqueKind::CV_##T: break + switch (value.opaque_kind) + { + HANDLE_CASE(BOOL, bool); + HANDLE_CASE(INT, int); + HANDLE_CASE(DOUBLE, double); + HANDLE_CASE(FLOAT, float); + HANDLE_CASE(STRING, std::string); + HANDLE_CASE(POINT, cv::Point); + HANDLE_CASE(POINT2F, cv::Point2f); + HANDLE_CASE(SIZE, cv::Size); + HANDLE_CASE(RECT, cv::Rect); + HANDLE_CASE(SCALAR, cv::Scalar); + HANDLE_CASE(MAT, cv::Mat); + HANDLE_CASE(UNKNOWN, cv::detail::PyObjectHolder); + UNSUPPORTED(UINT64); + UNSUPPORTED(DRAW_PRIM); +#undef HANDLE_CASE +#undef UNSUPPORTED + } + util::throw_error(std::logic_error("Unsupported kernel input type")); +} + +template<> +bool pyopencv_to(PyObject* obj, cv::GArg& value, const ArgInfo& info) +{ + value = cv::GArg(cv::detail::PyObjectHolder(obj)); + return true; +} + template <> bool pyopencv_to(PyObject* obj, std::vector& value, const ArgInfo& info) { @@ -81,7 +195,7 @@ PyObject* pyopencv_from(const cv::detail::OpaqueRef& o) case cv::detail::OpaqueKind::CV_POINT2F : return pyopencv_from(o.rref()); case cv::detail::OpaqueKind::CV_SIZE : return pyopencv_from(o.rref()); case cv::detail::OpaqueKind::CV_RECT : return pyopencv_from(o.rref()); - case cv::detail::OpaqueKind::CV_UNKNOWN : break; + case cv::detail::OpaqueKind::CV_UNKNOWN : return pyopencv_from(o.rref()); case cv::detail::OpaqueKind::CV_UINT64 : break; case cv::detail::OpaqueKind::CV_SCALAR : break; case cv::detail::OpaqueKind::CV_MAT : break; @@ -108,7 +222,7 @@ PyObject* pyopencv_from(const cv::detail::VectorRef& v) case cv::detail::OpaqueKind::CV_RECT : return pyopencv_from_generic_vec(v.rref()); case cv::detail::OpaqueKind::CV_SCALAR : return pyopencv_from_generic_vec(v.rref()); case cv::detail::OpaqueKind::CV_MAT : return pyopencv_from_generic_vec(v.rref()); - case cv::detail::OpaqueKind::CV_UNKNOWN : break; + case cv::detail::OpaqueKind::CV_UNKNOWN : return pyopencv_from_generic_vec(v.rref()); case cv::detail::OpaqueKind::CV_UINT64 : break; case cv::detail::OpaqueKind::CV_DRAW_PRIM : break; } @@ -270,7 +384,7 @@ static cv::detail::OpaqueRef extract_opaque_ref(PyObject* from, cv::detail::Opaq HANDLE_CASE(POINT2F, cv::Point2f); HANDLE_CASE(SIZE, cv::Size); HANDLE_CASE(RECT, cv::Rect); - UNSUPPORTED(UNKNOWN); + HANDLE_CASE(UNKNOWN, cv::GArg); UNSUPPORTED(UINT64); UNSUPPORTED(SCALAR); UNSUPPORTED(MAT); @@ -303,7 +417,7 @@ static cv::detail::VectorRef extract_vector_ref(PyObject* from, cv::detail::Opaq HANDLE_CASE(RECT, cv::Rect); HANDLE_CASE(SCALAR, cv::Scalar); HANDLE_CASE(MAT, cv::Mat); - UNSUPPORTED(UNKNOWN); + HANDLE_CASE(UNKNOWN, cv::GArg); UNSUPPORTED(UINT64); UNSUPPORTED(DRAW_PRIM); #undef HANDLE_CASE @@ -415,38 +529,7 @@ static cv::GMetaArgs extract_meta_args(const cv::GTypesInfo& info, PyObject* py_ return metas; } -inline PyObject* extract_opaque_value(const cv::GArg& value) -{ - GAPI_Assert(value.kind != cv::detail::ArgKind::GOBJREF); -#define HANDLE_CASE(T, O) case cv::detail::OpaqueKind::CV_##T: \ - { \ - return pyopencv_from(value.get()); \ - } - -#define UNSUPPORTED(T) case cv::detail::OpaqueKind::CV_##T: break - switch (value.opaque_kind) - { - HANDLE_CASE(BOOL, bool); - HANDLE_CASE(INT, int); - HANDLE_CASE(DOUBLE, double); - HANDLE_CASE(FLOAT, float); - HANDLE_CASE(STRING, std::string); - HANDLE_CASE(POINT, cv::Point); - HANDLE_CASE(POINT2F, cv::Point2f); - HANDLE_CASE(SIZE, cv::Size); - HANDLE_CASE(RECT, cv::Rect); - HANDLE_CASE(SCALAR, cv::Scalar); - HANDLE_CASE(MAT, cv::Mat); - UNSUPPORTED(UNKNOWN); - UNSUPPORTED(UINT64); - UNSUPPORTED(DRAW_PRIM); -#undef HANDLE_CASE -#undef UNSUPPORTED - } - util::throw_error(std::logic_error("Unsupported kernel input type")); -} - -static cv::GRunArgs run_py_kernel(PyObject* kernel, +static cv::GRunArgs run_py_kernel(cv::detail::PyObjectHolder kernel, const cv::gapi::python::GPythonContext &ctx) { const auto& ins = ctx.ins; @@ -460,33 +543,32 @@ static cv::GRunArgs run_py_kernel(PyObject* kernel, try { int in_idx = 0; - PyObject* args = PyTuple_New(ins.size()); + // NB: Doesn't increase reference counter (false), + // because PyObject already have ownership. + // In case exception decrement reference counter. + cv::detail::PyObjectHolder args(PyTuple_New(ins.size()), false); for (size_t i = 0; i < ins.size(); ++i) { - // NB: If meta is monostate then object isn't associated with G-TYPE, so in case it - // kind matches with supported types do conversion from c++ to python, if not (CV_UNKNOWN) - // obtain PyObject* and pass as-is. + // NB: If meta is monostate then object isn't associated with G-TYPE. if (cv::util::holds_alternative(in_metas[i])) { - PyTuple_SetItem(args, i, - ins[i].opaque_kind != cv::detail::OpaqueKind::CV_UNKNOWN ? extract_opaque_value(ins[i]) - : ins[i].get()); + PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i])); continue; } switch (in_metas[i].index()) { case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, i, pyopencv_from(ins[i].get())); + PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get())); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, i, pyopencv_from(ins[i].get())); + PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get())); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, i, pyopencv_from(ins[i].get())); + PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get())); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, i, pyopencv_from(ins[i].get())); + PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get())); break; case cv::GMetaArg::index_of(): util::throw_error(std::logic_error("GFrame isn't supported for custom operation")); @@ -494,11 +576,21 @@ static cv::GRunArgs run_py_kernel(PyObject* kernel, } ++in_idx; } + // NB: Doesn't increase reference counter (false). + // In case PyObject_CallObject return NULL, do nothing in destructor. + cv::detail::PyObjectHolder result( + PyObject_CallObject(kernel.get(), args.get()), false); + + if (PyErr_Occurred()) { + PyErr_PrintEx(0); + PyErr_Clear(); + throw std::logic_error("Python kernel failed with error!"); + } + // NB: In fact it's impossible situation, becase errors were handled above. + GAPI_Assert(result.get() && "Python kernel returned NULL!"); - PyObject* result = PyObject_CallObject(kernel, args); - - outs = out_info.size() == 1 ? cv::GRunArgs{extract_run_arg(out_info[0], result)} - : extract_run_args(out_info, result); + outs = out_info.size() == 1 ? cv::GRunArgs{extract_run_arg(out_info[0], result.get())} + : extract_run_args(out_info, result.get()); } catch (...) { @@ -510,12 +602,6 @@ static cv::GRunArgs run_py_kernel(PyObject* kernel, return outs; } -// FIXME: Now it's impossible to obtain meta function from operation, -// because kernel connects to operation only by id (string). -static cv::GMetaArgs empty_meta(const cv::GMetaArgs &, const cv::GArgs &) { - return {}; -} - static GMetaArg get_meta_arg(PyObject* obj) { if (PyObject_TypeCheck(obj, @@ -558,33 +644,38 @@ static cv::GMetaArgs get_meta_args(PyObject* tuple) return metas; } -static GMetaArgs python_meta(PyObject* outMeta, const cv::GMetaArgs &meta, const cv::GArgs &gargs) { +static GMetaArgs run_py_meta(cv::detail::PyObjectHolder out_meta, + const cv::GMetaArgs &meta, + const cv::GArgs &gargs) { PyGILState_STATE gstate; gstate = PyGILState_Ensure(); cv::GMetaArgs out_metas; try { - PyObject* args = PyTuple_New(meta.size()); + // NB: Doesn't increase reference counter (false), + // because PyObject already have ownership. + // In case exception decrement reference counter. + cv::detail::PyObjectHolder args(PyTuple_New(meta.size()), false); size_t idx = 0; for (auto&& m : meta) { switch (m.index()) { case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get(m))); + PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get(m))); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get(m))); + PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get(m))); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get(m))); + PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get(m))); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get(m))); + PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get(m))); break; case cv::GMetaArg::index_of(): - PyTuple_SetItem(args, idx, gargs[idx].get()); + PyTuple_SetItem(args.get(), idx, pyopencv_from(gargs[idx])); break; case cv::GMetaArg::index_of(): util::throw_error(std::logic_error("GFrame isn't supported for custom operation")); @@ -592,9 +683,21 @@ static GMetaArgs python_meta(PyObject* outMeta, const cv::GMetaArgs &meta, const } ++idx; } - PyObject* result = PyObject_CallObject(outMeta, args); - out_metas = PyTuple_Check(result) ? get_meta_args(result) - : cv::GMetaArgs{get_meta_arg(result)}; + // NB: Doesn't increase reference counter (false). + // In case PyObject_CallObject return NULL, do nothing in destructor. + cv::detail::PyObjectHolder result( + PyObject_CallObject(out_meta.get(), args.get()), false); + + if (PyErr_Occurred()) { + PyErr_PrintEx(0); + PyErr_Clear(); + throw std::logic_error("Python outMeta failed with error!"); + } + // NB: In fact it's impossible situation, becase errors were handled above. + GAPI_Assert(result.get() && "Python outMeta returned NULL!"); + + out_metas = PyTuple_Check(result.get()) ? get_meta_args(result.get()) + : cv::GMetaArgs{get_meta_arg(result.get())}; } catch (...) { @@ -611,23 +714,43 @@ static PyObject* pyopencv_cv_gapi_kernels(PyObject* , PyObject* py_args, PyObjec using namespace cv; gapi::GKernelPackage pkg; Py_ssize_t size = PyTuple_Size(py_args); + for (int i = 0; i < size; ++i) { - PyObject* pair = PyTuple_GetItem(py_args, i); - PyObject* kernel = PyTuple_GetItem(pair, 0); + PyObject* user_kernel = PyTuple_GetItem(py_args, i); + + PyObject* id_obj = PyObject_GetAttrString(user_kernel, "id"); + if (!id_obj) { + PyErr_SetString(PyExc_TypeError, + "Python kernel should contain id, please use cv.gapi.kernel to define kernel"); + return NULL; + } + + PyObject* out_meta = PyObject_GetAttrString(user_kernel, "outMeta"); + if (!out_meta) { + PyErr_SetString(PyExc_TypeError, + "Python kernel should contain outMeta, please use cv.gapi.kernel to define kernel"); + return NULL; + } + + PyObject* run = PyObject_GetAttrString(user_kernel, "run"); + if (!run) { + PyErr_SetString(PyExc_TypeError, + "Python kernel should contain run, please use cv.gapi.kernel to define kernel"); + return NULL; + } std::string id; - if (!pyopencv_to(PyTuple_GetItem(pair, 1), id, ArgInfo("id", false))) + if (!pyopencv_to(id_obj, id, ArgInfo("id", false))) { - PyErr_SetString(PyExc_TypeError, "Failed to obtain: kernel id must be a string"); + PyErr_SetString(PyExc_TypeError, "Failed to obtain string"); return NULL; } - Py_INCREF(kernel); + + using namespace std::placeholders; gapi::python::GPythonFunctor f(id.c_str(), - empty_meta, - std::bind(run_py_kernel, - kernel, - std::placeholders::_1)); + std::bind(run_py_meta , cv::detail::PyObjectHolder{out_meta}, _1, _2), + std::bind(run_py_kernel, cv::detail::PyObjectHolder{run} , _1)); pkg.include(f); } return pyopencv_from(pkg); @@ -644,7 +767,6 @@ static PyObject* pyopencv_cv_gapi_op(PyObject* , PyObject* py_args, PyObject*) return NULL; } PyObject* outMeta = PyTuple_GetItem(py_args, 1); - Py_INCREF(outMeta); cv::GArgs args; for (int i = 2; i < size; i++) @@ -684,13 +806,12 @@ static PyObject* pyopencv_cv_gapi_op(PyObject* , PyObject* py_args, PyObject*) } else { - Py_INCREF(item); - args.emplace_back(cv::GArg(item)); + args.emplace_back(cv::GArg(cv::detail::PyObjectHolder{item})); } } - cv::GKernel::M outMetaWrapper = std::bind(python_meta, - outMeta, + cv::GKernel::M outMetaWrapper = std::bind(run_py_meta, + cv::detail::PyObjectHolder{outMeta}, std::placeholders::_1, std::placeholders::_2); return pyopencv_from(cv::gapi::wip::op(id, outMetaWrapper, std::move(args))); @@ -698,7 +819,7 @@ static PyObject* pyopencv_cv_gapi_op(PyObject* , PyObject* py_args, PyObject*) static PyObject* pyopencv_cv_gin(PyObject*, PyObject* py_args, PyObject*) { - Py_INCREF(py_args); + cv::detail::PyObjectHolder holder{py_args}; auto callback = cv::detail::ExtractArgsCallback{[=](const cv::GTypesInfo& info) { PyGILState_STATE gstate; @@ -707,7 +828,7 @@ static PyObject* pyopencv_cv_gin(PyObject*, PyObject* py_args, PyObject*) cv::GRunArgs args; try { - args = extract_run_args(info, py_args); + args = extract_run_args(info, holder.get()); } catch (...) { @@ -792,10 +913,10 @@ struct PyOpenCV_Converter> }; -// extend cv.gapi.wip. methods -#define PYOPENCV_EXTRA_METHODS_GAPI_WIP \ +// extend cv.gapi methods +#define PYOPENCV_EXTRA_METHODS_GAPI \ {"kernels", CV_PY_FN_WITH_KW(pyopencv_cv_gapi_kernels), "kernels(...) -> GKernelPackage"}, \ - {"op", CV_PY_FN_WITH_KW_(pyopencv_cv_gapi_op, 0), "kernels(...) -> retval\n"}, \ + {"__op", CV_PY_FN_WITH_KW(pyopencv_cv_gapi_op), "__op(...) -> retval\n"}, #endif // HAVE_OPENCV_GAPI diff --git a/modules/gapi/misc/python/python_bridge.hpp b/modules/gapi/misc/python/python_bridge.hpp index 51f0ca8ab0..0d1c6d51c5 100644 --- a/modules/gapi/misc/python/python_bridge.hpp +++ b/modules/gapi/misc/python/python_bridge.hpp @@ -25,29 +25,31 @@ } #define GARRAY_TYPE_LIST_G(G, G2) \ -WRAP_ARGS(bool , cv::gapi::ArgType::CV_BOOL, G) \ -WRAP_ARGS(int , cv::gapi::ArgType::CV_INT, G) \ -WRAP_ARGS(double , cv::gapi::ArgType::CV_DOUBLE, G) \ -WRAP_ARGS(float , cv::gapi::ArgType::CV_FLOAT, G) \ -WRAP_ARGS(std::string , cv::gapi::ArgType::CV_STRING, G) \ -WRAP_ARGS(cv::Point , cv::gapi::ArgType::CV_POINT, G) \ -WRAP_ARGS(cv::Point2f , cv::gapi::ArgType::CV_POINT2F, G) \ -WRAP_ARGS(cv::Size , cv::gapi::ArgType::CV_SIZE, G) \ -WRAP_ARGS(cv::Rect , cv::gapi::ArgType::CV_RECT, G) \ -WRAP_ARGS(cv::Scalar , cv::gapi::ArgType::CV_SCALAR, G) \ -WRAP_ARGS(cv::Mat , cv::gapi::ArgType::CV_MAT, G) \ -WRAP_ARGS(cv::GMat , cv::gapi::ArgType::CV_GMAT, G2) +WRAP_ARGS(bool , cv::gapi::ArgType::CV_BOOL, G) \ +WRAP_ARGS(int , cv::gapi::ArgType::CV_INT, G) \ +WRAP_ARGS(double , cv::gapi::ArgType::CV_DOUBLE, G) \ +WRAP_ARGS(float , cv::gapi::ArgType::CV_FLOAT, G) \ +WRAP_ARGS(std::string , cv::gapi::ArgType::CV_STRING, G) \ +WRAP_ARGS(cv::Point , cv::gapi::ArgType::CV_POINT, G) \ +WRAP_ARGS(cv::Point2f , cv::gapi::ArgType::CV_POINT2F, G) \ +WRAP_ARGS(cv::Size , cv::gapi::ArgType::CV_SIZE, G) \ +WRAP_ARGS(cv::Rect , cv::gapi::ArgType::CV_RECT, G) \ +WRAP_ARGS(cv::Scalar , cv::gapi::ArgType::CV_SCALAR, G) \ +WRAP_ARGS(cv::Mat , cv::gapi::ArgType::CV_MAT, G) \ +WRAP_ARGS(cv::GArg , cv::gapi::ArgType::CV_ANY, G) \ +WRAP_ARGS(cv::GMat , cv::gapi::ArgType::CV_GMAT, G2) \ #define GOPAQUE_TYPE_LIST_G(G, G2) \ -WRAP_ARGS(bool , cv::gapi::ArgType::CV_BOOL, G) \ -WRAP_ARGS(int , cv::gapi::ArgType::CV_INT, G) \ -WRAP_ARGS(double , cv::gapi::ArgType::CV_DOUBLE, G) \ -WRAP_ARGS(float , cv::gapi::ArgType::CV_FLOAT, G) \ -WRAP_ARGS(std::string , cv::gapi::ArgType::CV_STRING, G) \ -WRAP_ARGS(cv::Point , cv::gapi::ArgType::CV_POINT, G) \ -WRAP_ARGS(cv::Point2f , cv::gapi::ArgType::CV_POINT2F, G) \ -WRAP_ARGS(cv::Size , cv::gapi::ArgType::CV_SIZE, G) \ -WRAP_ARGS(cv::Rect , cv::gapi::ArgType::CV_RECT, G2) \ +WRAP_ARGS(bool , cv::gapi::ArgType::CV_BOOL, G) \ +WRAP_ARGS(int , cv::gapi::ArgType::CV_INT, G) \ +WRAP_ARGS(double , cv::gapi::ArgType::CV_DOUBLE, G) \ +WRAP_ARGS(float , cv::gapi::ArgType::CV_FLOAT, G) \ +WRAP_ARGS(std::string , cv::gapi::ArgType::CV_STRING, G) \ +WRAP_ARGS(cv::Point , cv::gapi::ArgType::CV_POINT, G) \ +WRAP_ARGS(cv::Point2f , cv::gapi::ArgType::CV_POINT2F, G) \ +WRAP_ARGS(cv::Size , cv::gapi::ArgType::CV_SIZE, G) \ +WRAP_ARGS(cv::GArg , cv::gapi::ArgType::CV_ANY, G) \ +WRAP_ARGS(cv::Rect , cv::gapi::ArgType::CV_RECT, G2) \ namespace cv { namespace gapi { @@ -66,6 +68,7 @@ enum ArgType { CV_SCALAR, CV_MAT, CV_GMAT, + CV_ANY, }; GAPI_EXPORTS_W inline cv::GInferOutputs infer(const String& name, const cv::GInferInputs& inputs) diff --git a/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py b/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py index b4440e48c5..2f921901db 100644 --- a/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py +++ b/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py @@ -3,523 +3,678 @@ import numpy as np import cv2 as cv import os +import sys +import unittest from tests_common import NewOpenCVTests -# Plaidml is an optional backend -pkgs = [ - ('ocl' , cv.gapi.core.ocl.kernels()), - ('cpu' , cv.gapi.core.cpu.kernels()), - ('fluid' , cv.gapi.core.fluid.kernels()) - # ('plaidml', cv.gapi.core.plaidml.kernels()) - ] - -# Test output GMat. -def custom_add(img1, img2, dtype): - return cv.add(img1, img2) +try: -# Test output GScalar. -def custom_mean(img): - return cv.mean(img) + if sys.version_info[:2] < (3, 0): + raise unittest.SkipTest('Python 2.x is not supported') -# Test output tuple of GMat's. -def custom_split3(img): - # NB: cv.split return list but g-api requires tuple in multiple output case - return tuple(cv.split(img)) + # Plaidml is an optional backend + pkgs = [ + ('ocl' , cv.gapi.core.ocl.kernels()), + ('cpu' , cv.gapi.core.cpu.kernels()), + ('fluid' , cv.gapi.core.fluid.kernels()) + # ('plaidml', cv.gapi.core.plaidml.kernels()) + ] -# Test output GOpaque. -def custom_size(img): - # NB: Take only H, W, because the operation should return cv::Size which is 2D. - return img.shape[:2] -# Test output GArray. -def custom_goodFeaturesToTrack(img, max_corners, quality_lvl, - min_distance, mask, block_sz, - use_harris_detector, k): - features = cv.goodFeaturesToTrack(img, max_corners, quality_lvl, - min_distance, mask=mask, - blockSize=block_sz, - useHarrisDetector=use_harris_detector, k=k) - # NB: The operation output is cv::GArray, so it should be mapped - # to python paramaters like this: [(1.2, 3.4), (5.2, 3.2)], because the cv::Point2f - # according to opencv rules mapped to the tuple and cv::GArray<> mapped to the list. - # OpenCV returns np.array with shape (n_features, 1, 2), so let's to convert it to list - # tuples with size - n_features. - features = list(map(tuple, features.reshape(features.shape[0], -1))) - return features + @cv.gapi.op('custom.add', in_types=[cv.GMat, cv.GMat, int], out_types=[cv.GMat]) + class GAdd: + """Calculates sum of two matrices.""" -# Test input scalar. -def custom_addC(img, sc, dtype): - # NB: dtype is just ignored in this implementation. - # More over from G-API kernel got scalar as tuples with 4 elements - # where the last element is equal to zero, just cut him for broadcasting. - return img + np.array(sc, dtype=np.uint8)[:-1] + @staticmethod + def outMeta(desc1, desc2, depth): + return desc1 -# Test input opaque. -def custom_sizeR(rect): - # NB: rect - is tuple (x, y, h, w) - return (rect[2], rect[3]) + @cv.gapi.kernel(GAdd) + class GAddImpl: + """Implementation for GAdd operation.""" -# Test input array. -def custom_boundingRect(array): - # NB: OpenCV - numpy array (n_points x 2). - # G-API - array of tuples (n_points). - return cv.boundingRect(np.array(array)) + @staticmethod + def run(img1, img2, dtype): + return cv.add(img1, img2) -# Test input mat -def add(g_in1, g_in2, dtype): - def custom_add_meta(img_desc1, img_desc2, dtype): - return img_desc1 - return cv.gapi.wip.op('custom.add', custom_add_meta, g_in1, g_in2, dtype).getGMat() + @cv.gapi.op('custom.split3', in_types=[cv.GMat], out_types=[cv.GMat, cv.GMat, cv.GMat]) + class GSplit3: + """Divides a 3-channel matrix into 3 single-channel matrices.""" + @staticmethod + def outMeta(desc): + out_desc = desc.withType(desc.depth, 1) + return out_desc, out_desc, out_desc -# Test multiple output mat -def split3(g_in): - def custom_split3_meta(img_desc): - out_desc = img_desc.withType(img_desc.depth, 1) - return out_desc, out_desc, out_desc - op = cv.gapi.wip.op('custom.split3', custom_split3_meta, g_in) + @cv.gapi.kernel(GSplit3) + class GSplit3Impl: + """Implementation for GSplit3 operation.""" - ch1 = op.getGMat() - ch2 = op.getGMat() - ch3 = op.getGMat() + @staticmethod + def run(img): + # NB: cv.split return list but g-api requires tuple in multiple output case + return tuple(cv.split(img)) - return ch1, ch2, ch3 -# Test output scalar -def mean(g_in): - def custom_mean_meta(img_desc): - return cv.empty_scalar_desc() + @cv.gapi.op('custom.mean', in_types=[cv.GMat], out_types=[cv.GScalar]) + class GMean: + """Calculates the mean value M of matrix elements.""" - op = cv.gapi.wip.op('custom.mean', custom_mean_meta, g_in) - return op.getGScalar() + @staticmethod + def outMeta(desc): + return cv.empty_scalar_desc() -# Test input scalar -def addC(g_in, g_sc, dtype): - def custom_addC_meta(img_desc, sc_desc, dtype): - return img_desc + @cv.gapi.kernel(GMean) + class GMeanImpl: + """Implementation for GMean operation.""" - op = cv.gapi.wip.op('custom.addC', custom_addC_meta, g_in, g_sc, dtype) - return op.getGMat() + @staticmethod + def run(img): + # NB: cv.split return list but g-api requires tuple in multiple output case + return cv.mean(img) -# Test output opaque. -def size(g_in): - def custom_size_meta(img_desc): - return cv.empty_gopaque_desc() + @cv.gapi.op('custom.addC', in_types=[cv.GMat, cv.GScalar, int], out_types=[cv.GMat]) + class GAddC: + """Adds a given scalar value to each element of given matrix.""" - op = cv.gapi.wip.op('custom.size', custom_size_meta, g_in) - return op.getGOpaque(cv.gapi.CV_SIZE) + @staticmethod + def outMeta(mat_desc, scalar_desc, dtype): + return mat_desc -# Test input opaque. -def sizeR(g_rect): - def custom_sizeR_meta(opaque_desc): - return cv.empty_gopaque_desc() + @cv.gapi.kernel(GAddC) + class GAddCImpl: + """Implementation for GAddC operation.""" - op = cv.gapi.wip.op('custom.sizeR', custom_sizeR_meta, g_rect) - return op.getGOpaque(cv.gapi.CV_SIZE) + @staticmethod + def run(img, sc, dtype): + # NB: dtype is just ignored in this implementation. + # Moreover from G-API kernel got scalar as tuples with 4 elements + # where the last element is equal to zero, just cut him for broadcasting. + return img + np.array(sc, dtype=np.uint8)[:-1] -# Test input array. -def boundingRect(g_array): - def custom_boundingRect_meta(array_desc): - return cv.empty_gopaque_desc() + @cv.gapi.op('custom.size', in_types=[cv.GMat], out_types=[cv.GOpaque.Size]) + class GSize: + """Gets dimensions from input matrix.""" - op = cv.gapi.wip.op('custom.boundingRect', custom_boundingRect_meta, g_array) - return op.getGOpaque(cv.gapi.CV_RECT) + @staticmethod + def outMeta(mat_desc): + return cv.empty_gopaque_desc() -# Test output GArray. -def goodFeaturesToTrack(g_in, max_corners, quality_lvl, - min_distance, mask, block_sz, - use_harris_detector, k): - def custom_goodFeaturesToTrack_meta(img_desc, max_corners, quality_lvl, - min_distance, mask, block_sz, use_harris_detector, k): - return cv.empty_array_desc() + @cv.gapi.kernel(GSize) + class GSizeImpl: + """Implementation for GSize operation.""" - op = cv.gapi.wip.op('custom.goodFeaturesToTrack', custom_goodFeaturesToTrack_meta, g_in, - max_corners, quality_lvl, min_distance, mask, block_sz, use_harris_detector, k) - return op.getGArray(cv.gapi.CV_POINT2F) + @staticmethod + def run(img): + # NB: Take only H, W, because the operation should return cv::Size which is 2D. + return img.shape[:2] -class gapi_sample_pipelines(NewOpenCVTests): + @cv.gapi.op('custom.sizeR', in_types=[cv.GOpaque.Rect], out_types=[cv.GOpaque.Size]) + class GSizeR: + """Gets dimensions from rectangle.""" - # NB: This test check multiple outputs for operation - def test_mean_over_r(self): - img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) - in_mat = cv.imread(img_path) + @staticmethod + def outMeta(opaq_desc): + return cv.empty_gopaque_desc() - # # OpenCV - _, _, r_ch = cv.split(in_mat) - expected = cv.mean(r_ch) - # G-API - g_in = cv.GMat() - b, g, r = cv.gapi.split3(g_in) - g_out = cv.gapi.mean(r) - comp = cv.GComputation(g_in, g_out) + @cv.gapi.kernel(GSizeR) + class GSizeRImpl: + """Implementation for GSizeR operation.""" + + @staticmethod + def run(rect): + # NB: rect - is tuple (x, y, h, w) + return (rect[2], rect[3]) + + + @cv.gapi.op('custom.boundingRect', in_types=[cv.GArray.Point], out_types=[cv.GOpaque.Rect]) + class GBoundingRect: + """Calculates minimal up-right bounding rectangle for the specified + 9 point set or non-zero pixels of gray-scale image.""" + + @staticmethod + def outMeta(arr_desc): + return cv.empty_gopaque_desc() + + + @cv.gapi.kernel(GBoundingRect) + class GBoundingRectImpl: + """Implementation for GBoundingRect operation.""" + + @staticmethod + def run(array): + # NB: OpenCV - numpy array (n_points x 2). + # G-API - array of tuples (n_points). + return cv.boundingRect(np.array(array)) - for pkg_name, pkg in pkgs: - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) - # Comparison - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF), - 'Failed on ' + pkg_name + ' backend') + @cv.gapi.op('custom.goodFeaturesToTrack', + in_types=[cv.GMat, int, float, float, int, bool, float], + out_types=[cv.GArray.Point2f]) + class GGoodFeatures: + """Finds the most prominent corners in the image + or in the specified image region.""" - def test_custom_mean(self): - img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) - in_mat = cv.imread(img_path) + @staticmethod + def outMeta(desc, max_corners, quality_lvl, + min_distance, block_sz, + use_harris_detector, k): + return cv.empty_array_desc() - # OpenCV - expected = cv.mean(in_mat) - # G-API - g_in = cv.GMat() - g_out = cv.gapi.mean(g_in) + @cv.gapi.kernel(GGoodFeatures) + class GGoodFeaturesImpl: + """Implementation for GGoodFeatures operation.""" - comp = cv.GComputation(g_in, g_out) + @staticmethod + def run(img, max_corners, quality_lvl, + min_distance, block_sz, + use_harris_detector, k): + features = cv.goodFeaturesToTrack(img, max_corners, quality_lvl, + min_distance, mask=None, + blockSize=block_sz, + useHarrisDetector=use_harris_detector, k=k) + # NB: The operation output is cv::GArray, so it should be mapped + # to python paramaters like this: [(1.2, 3.4), (5.2, 3.2)], because the cv::Point2f + # according to opencv rules mapped to the tuple and cv::GArray<> mapped to the list. + # OpenCV returns np.array with shape (n_features, 1, 2), so let's to convert it to list + # tuples with size == n_features. + features = list(map(tuple, features.reshape(features.shape[0], -1))) + return features - pkg = cv.gapi.wip.kernels((custom_mean, 'org.opencv.core.math.mean')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) - # Comparison - self.assertEqual(expected, actual) + # To validate invalid cases + def create_op(in_types, out_types): + @cv.gapi.op('custom.op', in_types=in_types, out_types=out_types) + class Op: + """Custom operation for testing.""" + @staticmethod + def outMeta(desc): + raise NotImplementedError("outMeta isn't imlemented") + return Op - def test_custom_add(self): - sz = (3, 3) - in_mat1 = np.full(sz, 45, dtype=np.uint8) - in_mat2 = np.full(sz, 50 , dtype=np.uint8) - # OpenCV - expected = cv.add(in_mat1, in_mat2) + class gapi_sample_pipelines(NewOpenCVTests): - # G-API - g_in1 = cv.GMat() - g_in2 = cv.GMat() - g_out = cv.gapi.add(g_in1, g_in2) - comp = cv.GComputation(cv.GIn(g_in1, g_in2), cv.GOut(g_out)) + def test_custom_op_add(self): + sz = (3, 3) + in_mat1 = np.full(sz, 45, dtype=np.uint8) + in_mat2 = np.full(sz, 50, dtype=np.uint8) - pkg = cv.gapi.wip.kernels((custom_add, 'org.opencv.core.math.add')) - actual = comp.apply(cv.gin(in_mat1, in_mat2), args=cv.compile_args(pkg)) + # OpenCV + expected = cv.add(in_mat1, in_mat2) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + # G-API + g_in1 = cv.GMat() + g_in2 = cv.GMat() + g_out = GAdd.on(g_in1, g_in2, cv.CV_8UC1) + comp = cv.GComputation(cv.GIn(g_in1, g_in2), cv.GOut(g_out)) - def test_custom_size(self): - sz = (100, 150, 3) - in_mat = np.full(sz, 45, dtype=np.uint8) + pkg = cv.gapi.kernels(GAddImpl) + actual = comp.apply(cv.gin(in_mat1, in_mat2), args=cv.compile_args(pkg)) - # OpenCV - expected = (100, 150) + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) - # G-API - g_in = cv.GMat() - g_sz = cv.gapi.streaming.size(g_in) - comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_sz)) - pkg = cv.gapi.wip.kernels((custom_size, 'org.opencv.streaming.size')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + def test_custom_op_split3(self): + sz = (4, 4) + in_ch1 = np.full(sz, 1, dtype=np.uint8) + in_ch2 = np.full(sz, 2, dtype=np.uint8) + in_ch3 = np.full(sz, 3, dtype=np.uint8) + # H x W x C + in_mat = np.stack((in_ch1, in_ch2, in_ch3), axis=2) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + # G-API + g_in = cv.GMat() + g_ch1, g_ch2, g_ch3 = GSplit3.on(g_in) + comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_ch1, g_ch2, g_ch3)) - def test_custom_goodFeaturesToTrack(self): - # G-API - img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) - in_mat = cv.cvtColor(cv.imread(img_path), cv.COLOR_RGB2GRAY) + pkg = cv.gapi.kernels(GSplit3Impl) + ch1, ch2, ch3 = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) - # NB: goodFeaturesToTrack configuration - max_corners = 50 - quality_lvl = 0.01 - min_distance = 10 - block_sz = 3 - use_harris_detector = True - k = 0.04 - mask = None + self.assertEqual(0.0, cv.norm(in_ch1, ch1, cv.NORM_INF)) + self.assertEqual(0.0, cv.norm(in_ch2, ch2, cv.NORM_INF)) + self.assertEqual(0.0, cv.norm(in_ch3, ch3, cv.NORM_INF)) - # OpenCV - expected = cv.goodFeaturesToTrack(in_mat, max_corners, quality_lvl, - min_distance, mask=mask, - blockSize=block_sz, useHarrisDetector=use_harris_detector, k=k) - # G-API - g_in = cv.GMat() - g_out = cv.gapi.goodFeaturesToTrack(g_in, max_corners, quality_lvl, - min_distance, mask, block_sz, use_harris_detector, k) + def test_custom_op_mean(self): + img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) + in_mat = cv.imread(img_path) - comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_out)) - pkg = cv.gapi.wip.kernels((custom_goodFeaturesToTrack, 'org.opencv.imgproc.feature.goodFeaturesToTrack')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + # OpenCV + expected = cv.mean(in_mat) - # NB: OpenCV & G-API have different output types. - # OpenCV - numpy array with shape (num_points, 1, 2) - # G-API - list of tuples with size - num_points - # Comparison - self.assertEqual(0.0, cv.norm(expected.flatten(), - np.array(actual, dtype=np.float32).flatten(), cv.NORM_INF)) + # G-API + g_in = cv.GMat() + g_out = GMean.on(g_in) + comp = cv.GComputation(g_in, g_out) - def test_custom_addC(self): - sz = (3, 3, 3) - in_mat = np.full(sz, 45, dtype=np.uint8) - sc = (50, 10, 20) + pkg = cv.gapi.kernels(GMeanImpl) + actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + + # Comparison + self.assertEqual(expected, actual) + + + def test_custom_op_addC(self): + sz = (3, 3, 3) + in_mat = np.full(sz, 45, dtype=np.uint8) + sc = (50, 10, 20) - # Numpy reference, make array from sc to keep uint8 dtype. - expected = in_mat + np.array(sc, dtype=np.uint8) + # Numpy reference, make array from sc to keep uint8 dtype. + expected = in_mat + np.array(sc, dtype=np.uint8) - # G-API - g_in = cv.GMat() - g_sc = cv.GScalar() - g_out = cv.gapi.addC(g_in, g_sc) - comp = cv.GComputation(cv.GIn(g_in, g_sc), cv.GOut(g_out)) + # G-API + g_in = cv.GMat() + g_sc = cv.GScalar() + g_out = GAddC.on(g_in, g_sc, cv.CV_8UC1) + comp = cv.GComputation(cv.GIn(g_in, g_sc), cv.GOut(g_out)) - pkg = cv.gapi.wip.kernels((custom_addC, 'org.opencv.core.math.addC')) - actual = comp.apply(cv.gin(in_mat, sc), args=cv.compile_args(pkg)) + pkg = cv.gapi.kernels(GAddCImpl) + actual = comp.apply(cv.gin(in_mat, sc), args=cv.compile_args(pkg)) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) - def test_custom_sizeR(self): - # x, y, h, w - roi = (10, 15, 100, 150) + def test_custom_op_size(self): + sz = (100, 150, 3) + in_mat = np.full(sz, 45, dtype=np.uint8) + + # Open_cV + expected = (100, 150) + + # G-API + g_in = cv.GMat() + g_sz = GSize.on(g_in) + comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_sz)) + + pkg = cv.gapi.kernels(GSizeImpl) + actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) - expected = (100, 150) + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) - # G-API - g_r = cv.GOpaqueT(cv.gapi.CV_RECT) - g_sz = cv.gapi.streaming.size(g_r) - comp = cv.GComputation(cv.GIn(g_r), cv.GOut(g_sz)) - pkg = cv.gapi.wip.kernels((custom_sizeR, 'org.opencv.streaming.sizeR')) - actual = comp.apply(cv.gin(roi), args=cv.compile_args(pkg)) + def test_custom_op_sizeR(self): + # x, y, h, w + roi = (10, 15, 100, 150) - # cv.norm works with tuples ? - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + expected = (100, 150) + # G-API + g_r = cv.GOpaque.Rect() + g_sz = GSizeR.on(g_r) + comp = cv.GComputation(cv.GIn(g_r), cv.GOut(g_sz)) + + pkg = cv.gapi.kernels(GSizeRImpl) + actual = comp.apply(cv.gin(roi), args=cv.compile_args(pkg)) + + # cv.norm works with tuples ? + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + + + def test_custom_op_boundingRect(self): + points = [(0,0), (0,1), (1,0), (1,1)] + + # OpenCV + expected = cv.boundingRect(np.array(points)) + + # G-API + g_pts = cv.GArray.Point() + g_br = GBoundingRect.on(g_pts) + comp = cv.GComputation(cv.GIn(g_pts), cv.GOut(g_br)) + + pkg = cv.gapi.kernels(GBoundingRectImpl) + actual = comp.apply(cv.gin(points), args=cv.compile_args(pkg)) + + # cv.norm works with tuples ? + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + + + def test_custom_op_goodFeaturesToTrack(self): + # G-API + img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) + in_mat = cv.cvtColor(cv.imread(img_path), cv.COLOR_RGB2GRAY) + + # NB: goodFeaturesToTrack configuration + max_corners = 50 + quality_lvl = 0.01 + min_distance = 10.0 + block_sz = 3 + use_harris_detector = True + k = 0.04 + + # OpenCV + expected = cv.goodFeaturesToTrack(in_mat, max_corners, quality_lvl, + min_distance, mask=None, + blockSize=block_sz, useHarrisDetector=use_harris_detector, k=k) + + # G-API + g_in = cv.GMat() + g_out = GGoodFeatures.on(g_in, max_corners, quality_lvl, + min_distance, block_sz, use_harris_detector, k) + + comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_out)) + pkg = cv.gapi.kernels(GGoodFeaturesImpl) + actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + + # NB: OpenCV & G-API have different output types. + # OpenCV - numpy array with shape (num_points, 1, 2) + # G-API - list of tuples with size - num_points + # Comparison + self.assertEqual(0.0, cv.norm(expected.flatten(), + np.array(actual, dtype=np.float32).flatten(), cv.NORM_INF)) - def test_custom_boundingRect(self): - points = [(0,0), (0,1), (1,0), (1,1)] - # OpenCV - expected = cv.boundingRect(np.array(points)) + def test_invalid_op(self): + # NB: Empty input types list + with self.assertRaises(Exception): create_op(in_types=[], out_types=[cv.GMat]) + # NB: Empty output types list + with self.assertRaises(Exception): create_op(in_types=[cv.GMat], out_types=[]) - # G-API - g_pts = cv.GArrayT(cv.gapi.CV_POINT) - g_br = cv.gapi.boundingRect(g_pts) - comp = cv.GComputation(cv.GIn(g_pts), cv.GOut(g_br)) + # Invalid output types + with self.assertRaises(Exception): create_op(in_types=[cv.GMat], out_types=[int]) + with self.assertRaises(Exception): create_op(in_types=[cv.GMat], out_types=[cv.GMat, int]) + with self.assertRaises(Exception): create_op(in_types=[cv.GMat], out_types=[str, cv.GScalar]) - pkg = cv.gapi.wip.kernels((custom_boundingRect, 'org.opencv.imgproc.shape.boundingRectVector32S')) - actual = comp.apply(cv.gin(points), args=cv.compile_args(pkg)) - # cv.norm works with tuples ? - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + def test_invalid_op_input(self): + # NB: Check GMat/GScalar + with self.assertRaises(Exception): create_op([cv.GMat] , [cv.GScalar]).on(cv.GScalar()) + with self.assertRaises(Exception): create_op([cv.GScalar], [cv.GScalar]).on(cv.GMat()) + # NB: Check GOpaque + op = create_op([cv.GOpaque.Rect], [cv.GMat]) + with self.assertRaises(Exception): op.on(cv.GOpaque.Bool()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Int()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Double()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Float()) + with self.assertRaises(Exception): op.on(cv.GOpaque.String()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Point()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Point2f()) + with self.assertRaises(Exception): op.on(cv.GOpaque.Size()) - def test_multiple_custom_kernels(self): - sz = (3, 3, 3) - in_mat1 = np.full(sz, 45, dtype=np.uint8) - in_mat2 = np.full(sz, 50 , dtype=np.uint8) + # NB: Check GArray + op = create_op([cv.GArray.Rect], [cv.GMat]) + with self.assertRaises(Exception): op.on(cv.GArray.Bool()) + with self.assertRaises(Exception): op.on(cv.GArray.Int()) + with self.assertRaises(Exception): op.on(cv.GArray.Double()) + with self.assertRaises(Exception): op.on(cv.GArray.Float()) + with self.assertRaises(Exception): op.on(cv.GArray.String()) + with self.assertRaises(Exception): op.on(cv.GArray.Point()) + with self.assertRaises(Exception): op.on(cv.GArray.Point2f()) + with self.assertRaises(Exception): op.on(cv.GArray.Size()) - # OpenCV - expected = cv.mean(cv.split(cv.add(in_mat1, in_mat2))[1]) + # Check other possible invalid options + with self.assertRaises(Exception): op.on(cv.GMat()) + with self.assertRaises(Exception): op.on(cv.GScalar()) - # G-API - g_in1 = cv.GMat() - g_in2 = cv.GMat() - g_sum = cv.gapi.add(g_in1, g_in2) - g_b, g_r, g_g = cv.gapi.split3(g_sum) - g_mean = cv.gapi.mean(g_b) + with self.assertRaises(Exception): op.on(1) + with self.assertRaises(Exception): op.on('foo') + with self.assertRaises(Exception): op.on(False) - comp = cv.GComputation(cv.GIn(g_in1, g_in2), cv.GOut(g_mean)) + with self.assertRaises(Exception): create_op([cv.GMat, int], [cv.GMat]).on(cv.GMat(), 'foo') + with self.assertRaises(Exception): create_op([cv.GMat, int], [cv.GMat]).on(cv.GMat()) - pkg = cv.gapi.wip.kernels((custom_add , 'org.opencv.core.math.add'), - (custom_mean , 'org.opencv.core.math.mean'), - (custom_split3, 'org.opencv.core.transform.split3')) + def test_stateful_kernel(self): + @cv.gapi.op('custom.sum', in_types=[cv.GArray.Int], out_types=[cv.GOpaque.Int]) + class GSum: + @staticmethod + def outMeta(arr_desc): + return cv.empty_gopaque_desc() - actual = comp.apply(cv.gin(in_mat1, in_mat2), args=cv.compile_args(pkg)) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + @cv.gapi.kernel(GSum) + class GSumImpl: + last_result = 0 + @staticmethod + def run(arr): + GSumImpl.last_result = sum(arr) + return GSumImpl.last_result - def test_custom_op_add(self): - sz = (3, 3) - in_mat1 = np.full(sz, 45, dtype=np.uint8) - in_mat2 = np.full(sz, 50, dtype=np.uint8) - # OpenCV - expected = cv.add(in_mat1, in_mat2) + g_in = cv.GArray.Int() + comp = cv.GComputation(cv.GIn(g_in), cv.GOut(GSum.on(g_in))) - # G-API - g_in1 = cv.GMat() - g_in2 = cv.GMat() - g_out = add(g_in1, g_in2, cv.CV_8UC1) + s = comp.apply(cv.gin([1, 2, 3, 4]), args=cv.compile_args(cv.gapi.kernels(GSumImpl))) + self.assertEqual(10, s) - comp = cv.GComputation(cv.GIn(g_in1, g_in2), cv.GOut(g_out)) + s = comp.apply(cv.gin([1, 2, 8, 7]), args=cv.compile_args(cv.gapi.kernels(GSumImpl))) + self.assertEqual(18, s) - pkg = cv.gapi.wip.kernels((custom_add, 'custom.add')) - actual = comp.apply(cv.gin(in_mat1, in_mat2), args=cv.compile_args(pkg)) + self.assertEqual(18, GSumImpl.last_result) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + def test_opaq_with_custom_type(self): + @cv.gapi.op('custom.op', in_types=[cv.GOpaque.Any, cv.GOpaque.String], out_types=[cv.GOpaque.Any]) + class GLookUp: + @staticmethod + def outMeta(opaq_desc0, opaq_desc1): + return cv.empty_gopaque_desc() - def test_custom_op_split3(self): - sz = (4, 4) - in_ch1 = np.full(sz, 1, dtype=np.uint8) - in_ch2 = np.full(sz, 2, dtype=np.uint8) - in_ch3 = np.full(sz, 3, dtype=np.uint8) - # H x W x C - in_mat = np.stack((in_ch1, in_ch2, in_ch3), axis=2) + @cv.gapi.kernel(GLookUp) + class GLookUpImpl: + @staticmethod + def run(table, key): + return table[key] - # G-API - g_in = cv.GMat() - g_ch1, g_ch2, g_ch3 = split3(g_in) - comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_ch1, g_ch2, g_ch3)) + g_table = cv.GOpaque.Any() + g_key = cv.GOpaque.String() + g_out = GLookUp.on(g_table, g_key) - pkg = cv.gapi.wip.kernels((custom_split3, 'custom.split3')) - ch1, ch2, ch3 = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + comp = cv.GComputation(cv.GIn(g_table, g_key), cv.GOut(g_out)) - self.assertEqual(0.0, cv.norm(in_ch1, ch1, cv.NORM_INF)) - self.assertEqual(0.0, cv.norm(in_ch2, ch2, cv.NORM_INF)) - self.assertEqual(0.0, cv.norm(in_ch3, ch3, cv.NORM_INF)) + table = { + 'int': 42, + 'str': 'hello, world!', + 'tuple': (42, 42) + } + out = comp.apply(cv.gin(table, 'int'), args=cv.compile_args(cv.gapi.kernels(GLookUpImpl))) + self.assertEqual(42, out) - def test_custom_op_mean(self): - img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) - in_mat = cv.imread(img_path) + out = comp.apply(cv.gin(table, 'str'), args=cv.compile_args(cv.gapi.kernels(GLookUpImpl))) + self.assertEqual('hello, world!', out) - # OpenCV - expected = cv.mean(in_mat) + out = comp.apply(cv.gin(table, 'tuple'), args=cv.compile_args(cv.gapi.kernels(GLookUpImpl))) + self.assertEqual((42, 42), out) - # G-API - g_in = cv.GMat() - g_out = mean(g_in) - comp = cv.GComputation(g_in, g_out) + def test_array_with_custom_type(self): + @cv.gapi.op('custom.op', in_types=[cv.GArray.Any, cv.GArray.Any], out_types=[cv.GArray.Any]) + class GConcat: + @staticmethod + def outMeta(arr_desc0, arr_desc1): + return cv.empty_array_desc() - pkg = cv.gapi.wip.kernels((custom_mean, 'custom.mean')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + @cv.gapi.kernel(GConcat) + class GConcatImpl: + @staticmethod + def run(arr0, arr1): + return arr0 + arr1 - # Comparison - self.assertEqual(expected, actual) + g_arr0 = cv.GArray.Any() + g_arr1 = cv.GArray.Any() + g_out = GConcat.on(g_arr0, g_arr1) + comp = cv.GComputation(cv.GIn(g_arr0, g_arr1), cv.GOut(g_out)) - def test_custom_op_addC(self): - sz = (3, 3, 3) - in_mat = np.full(sz, 45, dtype=np.uint8) - sc = (50, 10, 20) + arr0 = [(2, 2), 2.0] + arr1 = [3, 'str'] - # Numpy reference, make array from sc to keep uint8 dtype. - expected = in_mat + np.array(sc, dtype=np.uint8) + out = comp.apply(cv.gin(arr0, arr1), + args=cv.compile_args(cv.gapi.kernels(GConcatImpl))) - # G-API - g_in = cv.GMat() - g_sc = cv.GScalar() - g_out = addC(g_in, g_sc, cv.CV_8UC1) - comp = cv.GComputation(cv.GIn(g_in, g_sc), cv.GOut(g_out)) + self.assertEqual(arr0 + arr1, out) - pkg = cv.gapi.wip.kernels((custom_addC, 'custom.addC')) - actual = comp.apply(cv.gin(in_mat, sc), args=cv.compile_args(pkg)) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + def test_raise_in_kernel(self): + @cv.gapi.op('custom.op', in_types=[cv.GMat, cv.GMat], out_types=[cv.GMat]) + class GAdd: + @staticmethod + def outMeta(desc0, desc1): + return desc0 + @cv.gapi.kernel(GAdd) + class GAddImpl: + @staticmethod + def run(img0, img1): + raise Exception('Error') + return img0 + img1 - def test_custom_op_size(self): - sz = (100, 150, 3) - in_mat = np.full(sz, 45, dtype=np.uint8) + g_in0 = cv.GMat() + g_in1 = cv.GMat() + g_out = GAdd.on(g_in0, g_in1) - # Open_cV - expected = (100, 150) + comp = cv.GComputation(cv.GIn(g_in0, g_in1), cv.GOut(g_out)) - # G-API - g_in = cv.GMat() - g_sz = size(g_in) - comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_sz)) + img0 = np.array([1, 2, 3]) + img1 = np.array([1, 2, 3]) - pkg = cv.gapi.wip.kernels((custom_size, 'custom.size')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + with self.assertRaises(Exception): comp.apply(cv.gin(img0, img1), + args=cv.compile_args( + cv.gapi.kernels(GAddImpl))) - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + def test_raise_in_outMeta(self): + @cv.gapi.op('custom.op', in_types=[cv.GMat, cv.GMat], out_types=[cv.GMat]) + class GAdd: + @staticmethod + def outMeta(desc0, desc1): + raise NotImplementedError("outMeta isn't implemented") + + @cv.gapi.kernel(GAdd) + class GAddImpl: + @staticmethod + def run(img0, img1): + return img0 + img1 + + g_in0 = cv.GMat() + g_in1 = cv.GMat() + g_out = GAdd.on(g_in0, g_in1) - def test_custom_op_sizeR(self): - # x, y, h, w - roi = (10, 15, 100, 150) + comp = cv.GComputation(cv.GIn(g_in0, g_in1), cv.GOut(g_out)) - expected = (100, 150) + img0 = np.array([1, 2, 3]) + img1 = np.array([1, 2, 3]) - # G-API - g_r = cv.GOpaqueT(cv.gapi.CV_RECT) - g_sz = sizeR(g_r) - comp = cv.GComputation(cv.GIn(g_r), cv.GOut(g_sz)) + with self.assertRaises(Exception): comp.apply(cv.gin(img0, img1), + args=cv.compile_args( + cv.gapi.kernels(GAddImpl))) - pkg = cv.gapi.wip.kernels((custom_sizeR, 'custom.sizeR')) - actual = comp.apply(cv.gin(roi), args=cv.compile_args(pkg)) - # cv.norm works with tuples ? - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + def test_invalid_outMeta(self): + @cv.gapi.op('custom.op', in_types=[cv.GMat, cv.GMat], out_types=[cv.GMat]) + class GAdd: + @staticmethod + def outMeta(desc0, desc1): + # Invalid outMeta + return cv.empty_gopaque_desc() + @cv.gapi.kernel(GAdd) + class GAddImpl: + @staticmethod + def run(img0, img1): + return img0 + img1 - def test_custom_op_boundingRect(self): - points = [(0,0), (0,1), (1,0), (1,1)] + g_in0 = cv.GMat() + g_in1 = cv.GMat() + g_out = GAdd.on(g_in0, g_in1) + + comp = cv.GComputation(cv.GIn(g_in0, g_in1), cv.GOut(g_out)) - # OpenCV - expected = cv.boundingRect(np.array(points)) + img0 = np.array([1, 2, 3]) + img1 = np.array([1, 2, 3]) - # G-API - g_pts = cv.GArrayT(cv.gapi.CV_POINT) - g_br = boundingRect(g_pts) - comp = cv.GComputation(cv.GIn(g_pts), cv.GOut(g_br)) + # FIXME: Cause Bad variant access. + # Need to provide more descriptive error messsage. + with self.assertRaises(Exception): comp.apply(cv.gin(img0, img1), + args=cv.compile_args( + cv.gapi.kernels(GAddImpl))) - pkg = cv.gapi.wip.kernels((custom_boundingRect, 'custom.boundingRect')) - actual = comp.apply(cv.gin(points), args=cv.compile_args(pkg)) + def test_pipeline_with_custom_kernels(self): + @cv.gapi.op('custom.resize', in_types=[cv.GMat, tuple], out_types=[cv.GMat]) + class GResize: + @staticmethod + def outMeta(desc, size): + return desc.withSize(size) - # cv.norm works with tuples ? - self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + @cv.gapi.kernel(GResize) + class GResizeImpl: + @staticmethod + def run(img, size): + return cv.resize(img, size) + @cv.gapi.op('custom.transpose', in_types=[cv.GMat, tuple], out_types=[cv.GMat]) + class GTranspose: + @staticmethod + def outMeta(desc, order): + return desc + + @cv.gapi.kernel(GTranspose) + class GTransposeImpl: + @staticmethod + def run(img, order): + return np.transpose(img, order) + + img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) + img = cv.imread(img_path) + size = (32, 32) + order = (1, 0, 2) - def test_custom_op_goodFeaturesToTrack(self): - # G-API - img_path = self.find_file('cv/face/david2.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')]) - in_mat = cv.cvtColor(cv.imread(img_path), cv.COLOR_RGB2GRAY) + # Dummy pipeline just to validate this case: + # gapi -> custom -> custom -> gapi - # NB: goodFeaturesToTrack configuration - max_corners = 50 - quality_lvl = 0.01 - min_distance = 10 - block_sz = 3 - use_harris_detector = True - k = 0.04 - mask = None + # OpenCV + expected = cv.cvtColor(img, cv.COLOR_BGR2RGB) + expected = cv.resize(expected, size) + expected = np.transpose(expected, order) + expected = cv.mean(expected) - # OpenCV - expected = cv.goodFeaturesToTrack(in_mat, max_corners, quality_lvl, - min_distance, mask=mask, - blockSize=block_sz, useHarrisDetector=use_harris_detector, k=k) + # G-API + g_bgr = cv.GMat() + g_rgb = cv.gapi.BGR2RGB(g_bgr) + g_resized = GResize.on(g_rgb, size) + g_transposed = GTranspose.on(g_resized, order) + g_mean = cv.gapi.mean(g_transposed) + + comp = cv.GComputation(cv.GIn(g_bgr), cv.GOut(g_mean)) + actual = comp.apply(cv.gin(img), args=cv.compile_args( + cv.gapi.kernels(GResizeImpl, GTransposeImpl))) + + self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF)) + + +except unittest.SkipTest as e: - # G-API - g_in = cv.GMat() - g_out = goodFeaturesToTrack(g_in, max_corners, quality_lvl, - min_distance, mask, block_sz, use_harris_detector, k) + message = str(e) - comp = cv.GComputation(cv.GIn(g_in), cv.GOut(g_out)) - pkg = cv.gapi.wip.kernels((custom_goodFeaturesToTrack, 'custom.goodFeaturesToTrack')) - actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg)) + class TestSkip(unittest.TestCase): + def setUp(self): + self.skipTest('Skip tests: ' + message) - # NB: OpenCV & G-API have different output types. - # OpenCV - numpy array with shape (num_points, 1, 2) - # G-API - list of tuples with size - num_points - # Comparison - self.assertEqual(0.0, cv.norm(expected.flatten(), - np.array(actual, dtype=np.float32).flatten(), cv.NORM_INF)) + def test_skip(): + pass + + pass if __name__ == '__main__': diff --git a/modules/python/common.cmake b/modules/python/common.cmake index 6a438fd1a2..1a6cc97429 100644 --- a/modules/python/common.cmake +++ b/modules/python/common.cmake @@ -218,6 +218,28 @@ if(NOT OPENCV_SKIP_PYTHON_LOADER) endif() configure_file("${PYTHON_SOURCE_DIR}/package/template/config-x.y.py.in" "${__python_loader_install_tmp_path}/cv2/${__target_config}" @ONLY) install(FILES "${__python_loader_install_tmp_path}/cv2/${__target_config}" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/cv2/" COMPONENT python) + + # handle Python extra code + foreach(m ${OPENCV_MODULES_BUILD}) + if (";${OPENCV_MODULE_${m}_WRAPPERS};" MATCHES ";python;" AND HAVE_${m} + AND EXISTS "${OPENCV_MODULE_${m}_LOCATION}/misc/python/package" + ) + set(__base "${OPENCV_MODULE_${m}_LOCATION}/misc/python/package") + file(GLOB_RECURSE extra_py_files + RELATIVE "${__base}" + "${__base}/**/*.py" + ) + if(extra_py_files) + list(SORT extra_py_files) + foreach(f ${extra_py_files}) + configure_file("${__base}/${f}" "${__loader_path}/cv2/_extra_py_code/${f}" COPYONLY) + install(FILES "${__base}/${f}" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/cv2/_extra_py_code/${f}" COMPONENT python) + endforeach() + else() + message(WARNING "Module ${m} has no .py files in misc/python/package") + endif() + endif() + endforeach(m) endif() # NOT OPENCV_SKIP_PYTHON_LOADER unset(PYTHON_SRC_DIR) diff --git a/modules/python/package/cv2/__init__.py b/modules/python/package/cv2/__init__.py index de70872839..27db65eef6 100644 --- a/modules/python/package/cv2/__init__.py +++ b/modules/python/package/cv2/__init__.py @@ -4,6 +4,8 @@ OpenCV Python binary extension loader import os import sys +__all__ = [] + try: import numpy import numpy.core.multiarray @@ -13,6 +15,14 @@ except ImportError: print(' pip install numpy') raise + +py_code_loader = None +if sys.version_info[:2] >= (3, 0): + try: + from . import _extra_py_code as py_code_loader + except: + pass + # TODO # is_x64 = sys.maxsize > 2**32 @@ -97,6 +107,11 @@ def bootstrap(): except: pass + if DEBUG: print('OpenCV loader: binary extension... OK') + + if py_code_loader: + py_code_loader.init('cv2') + if DEBUG: print('OpenCV loader: DONE') bootstrap() diff --git a/modules/python/package/cv2/_extra_py_code/__init__.py b/modules/python/package/cv2/_extra_py_code/__init__.py new file mode 100644 index 0000000000..be84566825 --- /dev/null +++ b/modules/python/package/cv2/_extra_py_code/__init__.py @@ -0,0 +1,53 @@ +import sys +import importlib + +__all__ = ['init'] + + +DEBUG = False +if hasattr(sys, 'OpenCV_LOADER_DEBUG'): + DEBUG = True + + +def _load_py_code(base, name): + try: + m = importlib.import_module(__name__ + name) + except ImportError: + return # extension doesn't exist? + + if DEBUG: print('OpenCV loader: added python code extension for: ' + name) + + if hasattr(m, '__all__'): + export_members = { k : getattr(m, k) for k in m.__all__ } + else: + export_members = m.__dict__ + + for k, v in export_members.items(): + if k.startswith('_'): # skip internals + continue + if isinstance(v, type(sys)): # don't bring modules + continue + if DEBUG: print(' symbol: {} = {}'.format(k, v)) + setattr(sys.modules[base + name ], k, v) + + del sys.modules[__name__ + name] + + +# TODO: listdir +def init(base): + _load_py_code(base, '.cv2') # special case + prefix = base + prefix_len = len(prefix) + + modules = [ m for m in sys.modules.keys() if m.startswith(prefix) ] + for m in modules: + m2 = m[prefix_len:] # strip prefix + if len(m2) == 0: + continue + if m2.startswith('._'): # skip internals + continue + if m2.startswith('.load_config_'): # skip helper files + continue + _load_py_code(base, m2) + + del sys.modules[__name__] diff --git a/modules/python/python_loader.cmake b/modules/python/python_loader.cmake index 31cd33505a..677111b061 100644 --- a/modules/python/python_loader.cmake +++ b/modules/python/python_loader.cmake @@ -25,10 +25,12 @@ endif() set(PYTHON_LOADER_FILES "setup.py" "cv2/__init__.py" "cv2/load_config_py2.py" "cv2/load_config_py3.py" + "cv2/_extra_py_code/__init__.py" ) foreach(fname ${PYTHON_LOADER_FILES}) get_filename_component(__dir "${fname}" DIRECTORY) - file(COPY "${PYTHON_SOURCE_DIR}/package/${fname}" DESTINATION "${__loader_path}/${__dir}") + # avoid using of file(COPY) to rerun CMake on changes + configure_file("${PYTHON_SOURCE_DIR}/package/${fname}" "${__loader_path}/${fname}" COPYONLY) if(fname STREQUAL "setup.py") if(OPENCV_PYTHON_SETUP_PY_INSTALL_PATH) install(FILES "${PYTHON_SOURCE_DIR}/package/${fname}" DESTINATION "${OPENCV_PYTHON_SETUP_PY_INSTALL_PATH}" COMPONENT python)