Merge pull request #19982 from TolyaTalamanov:at/new-python-operation-api
authorAnatoliy Talamanov <anatoliy.talamanov@intel.com>
Thu, 20 May 2021 18:59:53 +0000 (21:59 +0300)
committerGitHub <noreply@github.com>
Thu, 20 May 2021 18:59:53 +0000 (18:59 +0000)
G-API: New python operations API

* Reimplement test using decorators

* Custom python operation API

* Remove wip status

* python: support Python code in bindings (through loader only)

* cleanup, skip tests for Python 2.x (not supported)

* python 2.x can't skip unittest modules

* Clean up

* Clean up

* Fix segfault python3.9

Co-authored-by: Alexander Alekhin <alexander.a.alekhin@gmail.com>
modules/gapi/include/opencv2/gapi/core.hpp
modules/gapi/include/opencv2/gapi/gmat.hpp
modules/gapi/include/opencv2/gapi/imgproc.hpp
modules/gapi/misc/python/package/gapi/__init__.py [new file with mode: 0644]
modules/gapi/misc/python/pyopencv_gapi.hpp
modules/gapi/misc/python/python_bridge.hpp
modules/gapi/misc/python/test/test_gapi_sample_pipelines.py
modules/python/common.cmake
modules/python/package/cv2/__init__.py
modules/python/package/cv2/_extra_py_code/__init__.py [new file with mode: 0644]
modules/python/python_loader.cmake

index cb5d55d..5973151 100644 (file)
@@ -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.
 
index 5e567fb..11c3071 100644 (file)
@@ -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);
index 25a64a5..2dbe626 100644 (file)
@@ -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 (file)
index 0000000..733c980
--- /dev/null
@@ -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
index 56a7e70..3ade7aa 100644 (file)
@@ -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 <opencv2/gapi/cpu/gcpukernel.hpp>
@@ -49,6 +48,121 @@ using GArray_GMat    = cv::GArray<cv::GMat>;
 // 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<Impl> 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<cv::detail::PyObjectHolder>(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<O>());                   \
+    }
+
+#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<GCompileArg>& 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<cv::Point2f>());
         case cv::detail::OpaqueKind::CV_SIZE      : return pyopencv_from(o.rref<cv::Size>());
         case cv::detail::OpaqueKind::CV_RECT      : return pyopencv_from(o.rref<cv::Rect>());
-        case cv::detail::OpaqueKind::CV_UNKNOWN   : break;
+        case cv::detail::OpaqueKind::CV_UNKNOWN   : return pyopencv_from(o.rref<cv::GArg>());
         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<cv::Rect>());
         case cv::detail::OpaqueKind::CV_SCALAR    : return pyopencv_from_generic_vec(v.rref<cv::Scalar>());
         case cv::detail::OpaqueKind::CV_MAT       : return pyopencv_from_generic_vec(v.rref<cv::Mat>());
-        case cv::detail::OpaqueKind::CV_UNKNOWN   : break;
+        case cv::detail::OpaqueKind::CV_UNKNOWN   : return pyopencv_from_generic_vec(v.rref<cv::GArg>());
         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<O>());                   \
-    }
-
-#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<cv::util::monostate>(in_metas[i]))
             {
-                PyTuple_SetItem(args, i,
-                        ins[i].opaque_kind != cv::detail::OpaqueKind::CV_UNKNOWN ? extract_opaque_value(ins[i])
-                                                                                 : ins[i].get<PyObject*>());
+                PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i]));
                 continue;
             }
 
             switch (in_metas[i].index())
             {
                 case cv::GMetaArg::index_of<cv::GMatDesc>():
-                    PyTuple_SetItem(args, i, pyopencv_from(ins[i].get<cv::Mat>()));
+                    PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get<cv::Mat>()));
                     break;
                 case cv::GMetaArg::index_of<cv::GScalarDesc>():
-                    PyTuple_SetItem(args, i, pyopencv_from(ins[i].get<cv::Scalar>()));
+                    PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get<cv::Scalar>()));
                     break;
                 case cv::GMetaArg::index_of<cv::GOpaqueDesc>():
-                    PyTuple_SetItem(args, i, pyopencv_from(ins[i].get<cv::detail::OpaqueRef>()));
+                    PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get<cv::detail::OpaqueRef>()));
                     break;
                 case cv::GMetaArg::index_of<cv::GArrayDesc>():
-                    PyTuple_SetItem(args, i, pyopencv_from(ins[i].get<cv::detail::VectorRef>()));
+                    PyTuple_SetItem(args.get(), i, pyopencv_from(ins[i].get<cv::detail::VectorRef>()));
                     break;
                 case cv::GMetaArg::index_of<cv::GFrameDesc>():
                     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<cv::GMatDesc>():
-                    PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get<cv::GMatDesc>(m)));
+                    PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get<cv::GMatDesc>(m)));
                     break;
                 case cv::GMetaArg::index_of<cv::GScalarDesc>():
-                    PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get<cv::GScalarDesc>(m)));
+                    PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get<cv::GScalarDesc>(m)));
                     break;
                 case cv::GMetaArg::index_of<cv::GArrayDesc>():
-                    PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get<cv::GArrayDesc>(m)));
+                    PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get<cv::GArrayDesc>(m)));
                     break;
                 case cv::GMetaArg::index_of<cv::GOpaqueDesc>():
-                    PyTuple_SetItem(args, idx, pyopencv_from(cv::util::get<cv::GOpaqueDesc>(m)));
+                    PyTuple_SetItem(args.get(), idx, pyopencv_from(cv::util::get<cv::GOpaqueDesc>(m)));
                     break;
                 case cv::GMetaArg::index_of<cv::util::monostate>():
-                    PyTuple_SetItem(args, idx, gargs[idx].get<PyObject*>());
+                    PyTuple_SetItem(args.get(), idx, pyopencv_from(gargs[idx]));
                     break;
                 case cv::GMetaArg::index_of<cv::GFrameDesc>():
                     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<cv::GOpaque<T>>
 };
 
 
-// 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
index 51f0ca8..0d1c6d5 100644 (file)
     }
 
 #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)
index b4440e4..2f92190 100644 (file)
 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<cv::Pointf>, 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<cv::Pointf>, 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__':
index 6a438fd..1a6cc97 100644 (file)
@@ -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)
index de70872..27db65e 100644 (file)
@@ -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 (file)
index 0000000..be84566
--- /dev/null
@@ -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__]
index 31cd335..677111b 100644 (file)
@@ -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)