Implement cv.gin and multiple output for python
authorAnatoliy Talamanov <anatoliy.talamanov@intel.com>
Tue, 29 Sep 2020 10:45:40 +0000 (13:45 +0300)
committerAnatoliy Talamanov <anatoliy.talamanov@intel.com>
Tue, 29 Sep 2020 10:45:40 +0000 (13:45 +0300)
modules/gapi/include/opencv2/gapi/core.hpp
modules/gapi/include/opencv2/gapi/gcomputation.hpp
modules/gapi/misc/python/pyopencv_gapi.hpp
modules/gapi/misc/python/shadow_gapi.hpp
modules/gapi/misc/python/test/test_gapi_core.py
modules/gapi/misc/python/test/test_gapi_sample_pipelines.py
modules/gapi/src/api/gcomputation.cpp
modules/gapi/test/gapi_gcomputation_tests.cpp
modules/python/src2/cv2.cpp

index 8ecba2b9d6bc7d5581e055aa801313b68322275f..0313aad0936dacd99728b6bdc73ee5278c40eff0 100644 (file)
@@ -1331,7 +1331,7 @@ GAPI_EXPORTS GMat threshold(const GMat& src, const GScalar& thresh, const GScala
 This function applicable for all threshold types except CV_THRESH_OTSU and CV_THRESH_TRIANGLE
 @note Function textual ID is "org.opencv.core.matrixop.thresholdOT"
 */
-GAPI_EXPORTS std::tuple<GMat, GScalar> threshold(const GMat& src, const GScalar& maxval, int type);
+GAPI_EXPORTS_W std::tuple<GMat, GScalar> threshold(const GMat& src, const GScalar& maxval, int type);
 
 /** @brief Applies a range-level threshold to each matrix element.
 
index b24766a23261e2fd5950f7f7999139fc63dff8d4..c03bb9d0a43bc9ea107875f6f24229e5e4afc032 100644 (file)
@@ -259,6 +259,9 @@ public:
      */
     void apply(GRunArgs &&ins, GRunArgsP &&outs, GCompileArgs &&args = {});       // Arg-to-arg overload
 
+    /// @private -- Exclude this function from OpenCV documentation
+    GAPI_WRAP GRunArgs apply(GRunArgs &&ins, GCompileArgs &&args = {});
+
     /// @private -- Exclude this function from OpenCV documentation
     void apply(const std::vector<cv::Mat>& ins,                                   // Compatibility overload
                const std::vector<cv::Mat>& outs,
@@ -286,7 +289,7 @@ public:
      * @param args compilation arguments for underlying compilation
      * process.
      */
-    GAPI_WRAP void apply(cv::Mat in, CV_OUT cv::Scalar &out, GCompileArgs &&args = {}); // Unary overload (scalar)
+    void apply(cv::Mat in, cv::Scalar &out, GCompileArgs &&args = {}); // Unary overload (scalar)
 
     /**
      * @brief Execute a binary computation (with compilation on the fly)
@@ -298,7 +301,7 @@ public:
      * @param args compilation arguments for underlying compilation
      * process.
      */
-    GAPI_WRAP void apply(cv::Mat in1, cv::Mat in2, CV_OUT cv::Mat &out, GCompileArgs &&args = {}); // Binary overload
+    void apply(cv::Mat in1, cv::Mat in2, cv::Mat &out, GCompileArgs &&args = {}); // Binary overload
 
     /**
      * @brief Execute an binary computation (with compilation on the fly)
@@ -528,6 +531,7 @@ protected:
         GCompileArgs comp_args = std::get<sizeof...(Ts)-1>(meta_and_compile_args);
         return compileStreaming(std::move(meta_args), std::move(comp_args));
     }
+    void recompile(GMetaArgs&& in_metas, GCompileArgs &&args);
     /// @private
     std::shared_ptr<Priv> m_priv;
 };
index 7ef4cac57fc44d893dcdae36774c8c03131aec0d..b4a16cd6c7a8db742926c32408f80be205801797 100644 (file)
@@ -1,3 +1,8 @@
+#ifndef OPENCV_GAPI_PYOPENCV_GAPI_HPP
+#define OPENCV_GAPI_PYOPENCV_GAPI_HPP
+
+#ifdef HAVE_OPENCV_GAPI
+
 using gapi_GKernelPackage = cv::gapi::GKernelPackage;
 
 template<>
@@ -12,6 +17,67 @@ PyObject* pyopencv_from(const std::vector<GCompileArg>& value)
     return pyopencv_from_generic_vec(value);
 }
 
+template<>
+bool pyopencv_to(PyObject* obj, GRunArgs& value, const ArgInfo& info)
+{
+    return pyopencv_to_generic_vec(obj, value, info);
+}
+
+static PyObject* from_grunarg(const GRunArg& v)
+{
+    switch (v.index())
+    {
+        case GRunArg::index_of<cv::Mat>():
+        {
+            const auto& m = util::get<cv::Mat>(v);
+            return pyopencv_from(m);
+        }
+
+        case GRunArg::index_of<cv::Scalar>():
+        {
+            const auto& s = util::get<cv::Scalar>(v);
+            return pyopencv_from(s);
+        }
+
+        default:
+            return NULL;
+    }
+    GAPI_Assert(false);
+}
+
+template<>
+PyObject* pyopencv_from(const GRunArgs& value)
+{
+    size_t i, n = value.size();
+
+    // NB: It doesn't make sense to return list with a single element
+    if (n == 1)
+    {
+        PyObject* item = from_grunarg(value[0]);
+        if(!item)
+        {
+            PyErr_SetString(PyExc_TypeError, "Failed to unpack GRunArgs");
+            return NULL;
+        }
+        return item;
+    }
+
+    PyObject* list = PyList_New(n);
+    for(i = 0; i < n; ++i)
+    {
+        PyObject* item = from_grunarg(value[i]);
+        if(!item)
+        {
+            Py_DECREF(list);
+            PyErr_SetString(PyExc_TypeError, "Failed to unpack GRunArgs");
+            return NULL;
+        }
+        PyList_SetItem(list, i, item);
+    }
+
+    return list;
+}
+
 template <typename T>
 static PyObject* extract_proto_args(PyObject* py_args, PyObject* kw)
 {
@@ -19,13 +85,19 @@ static PyObject* extract_proto_args(PyObject* py_args, PyObject* kw)
 
     GProtoArgs args;
     Py_ssize_t size = PyTuple_Size(py_args);
-    for (int i = 0; i < size; ++i) {
+    for (int i = 0; i < size; ++i)
+    {
         PyObject* item = PyTuple_GetItem(py_args, i);
-        if (PyObject_TypeCheck(item, reinterpret_cast<PyTypeObject*>(pyopencv_GScalar_TypePtr))) {
+        if (PyObject_TypeCheck(item, reinterpret_cast<PyTypeObject*>(pyopencv_GScalar_TypePtr)))
+        {
             args.emplace_back(reinterpret_cast<pyopencv_GScalar_t*>(item)->v);
-        } else if (PyObject_TypeCheck(item, reinterpret_cast<PyTypeObject*>(pyopencv_GMat_TypePtr))) {
+        }
+        else if (PyObject_TypeCheck(item, reinterpret_cast<PyTypeObject*>(pyopencv_GMat_TypePtr)))
+        {
             args.emplace_back(reinterpret_cast<pyopencv_GMat_t*>(item)->v);
-        } else {
+        }
+        else
+        {
             PyErr_SetString(PyExc_TypeError, "cv.GIn() supports only cv.GMat and cv.GScalar");
             return NULL;
         }
@@ -43,3 +115,51 @@ static PyObject* pyopencv_cv_GOut(PyObject* , PyObject* py_args, PyObject* kw)
 {
     return extract_proto_args<GProtoOutputArgs>(py_args, kw);
 }
+
+static PyObject* pyopencv_cv_gin(PyObject* , PyObject* py_args, PyObject* kw)
+{
+    using namespace cv;
+
+    GRunArgs args;
+    Py_ssize_t size = PyTuple_Size(py_args);
+    for (int i = 0; i < size; ++i)
+    {
+        PyObject* item = PyTuple_GetItem(py_args, i);
+        if (PyTuple_Check(item))
+        {
+            cv::Scalar s;
+            if (pyopencv_to(item, s, ArgInfo("scalar", true)))
+            {
+                args.emplace_back(s);
+            }
+            else
+            {
+                PyErr_SetString(PyExc_TypeError, "Failed convert tuple to cv::Scalar");
+                return NULL;
+            }
+        }
+        else if (PyArray_Check(item))
+        {
+            cv::Mat m;
+            if (pyopencv_to(item, m, ArgInfo("mat", true)))
+            {
+                args.emplace_back(m);
+            }
+            else
+            {
+                PyErr_SetString(PyExc_TypeError, "Failed convert array to cv::Mat");
+                return NULL;
+            }
+        }
+    }
+
+    return pyopencv_from_generic_vec(args);
+}
+
+static PyObject* pyopencv_cv_gout(PyObject* o, PyObject* py_args, PyObject* kw)
+{
+    return pyopencv_cv_gin(o, py_args, kw);
+}
+
+#endif  // HAVE_OPENCV_GAPI
+#endif  // OPENCV_GAPI_PYOPENCV_GAPI_HPP
index 2150b86cff442a232b8bf8bc574486e82481cb20..dab083def7017b08de86d8ea7cf4461be9ec380f 100644 (file)
@@ -4,9 +4,11 @@
 namespace cv
 {
    GAPI_EXPORTS_W GCompileArgs compile_args(gapi::GKernelPackage pkg);
+
    class GAPI_EXPORTS_W_SIMPLE GProtoArg { };
    class GAPI_EXPORTS_W_SIMPLE GProtoInputArgs { };
    class GAPI_EXPORTS_W_SIMPLE GProtoOutputArgs { };
+   class GAPI_EXPORTS_W_SIMPLE GRunArg {  };
 
    using GProtoInputArgs  = GIOProtoArgs<In_Tag>;
    using GProtoOutputArgs = GIOProtoArgs<Out_Tag>;
index 7720dbc39876d69d112f9004c0828f34578721e2..b219ce15438249246493654f0873bf10c7550a10 100644 (file)
@@ -33,7 +33,7 @@ class gapi_core_test(NewOpenCVTests):
         comp = cv.GComputation(cv.GIn(g_in1, g_in2), cv.GOut(g_out))
 
         for pkg in pkgs:
-            actual = comp.apply(in1, in2, args=cv.compile_args(pkg))
+            actual = comp.apply(cv.gin(in1, in2), args=cv.compile_args(pkg))
             # Comparison
             self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF))
 
@@ -51,10 +51,51 @@ class gapi_core_test(NewOpenCVTests):
         comp = cv.GComputation(g_in, g_out)
 
         for pkg in pkgs:
-            actual = comp.apply(in_mat, args=cv.compile_args(pkg))
+            actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg))
             # Comparison
             self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF))
 
 
+    def test_split3(self):
+        sz = (1280, 720, 3)
+        in_mat = np.random.randint(0, 100, sz).astype(np.uint8)
+
+        # OpenCV
+        expected = cv.split(in_mat)
+
+        # G-API
+        g_in = cv.GMat()
+        b, g, r = cv.gapi.split3(g_in)
+        comp = cv.GComputation(cv.GIn(g_in), cv.GOut(b, g, r))
+
+        for pkg in pkgs:
+            actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg))
+            # Comparison
+            for e, a in zip(expected, actual):
+                self.assertEqual(0.0, cv.norm(e, a, cv.NORM_INF))
+
+
+    def test_threshold(self):
+        sz = (1280, 720)
+        in_mat = np.random.randint(0, 100, sz).astype(np.uint8)
+        rand_int = np.random.randint(0, 50)
+        maxv = (rand_int, rand_int)
+
+        # OpenCV
+        expected_thresh, expected_mat = cv.threshold(in_mat, maxv[0], maxv[0], cv.THRESH_TRIANGLE)
+
+        # G-API
+        g_in = cv.GMat()
+        g_sc = cv.GScalar()
+        mat, threshold = cv.gapi.threshold(g_in, g_sc, cv.THRESH_TRIANGLE)
+        comp = cv.GComputation(cv.GIn(g_in, g_sc), cv.GOut(mat, threshold))
+
+        for pkg in pkgs:
+            actual_mat, actual_thresh = comp.apply(cv.gin(in_mat, maxv), args=cv.compile_args(pkg))
+            # Comparison
+            self.assertEqual(0.0, cv.norm(expected_mat, actual_mat, cv.NORM_INF))
+            self.assertEqual(expected_thresh, actual_thresh[0])
+
+
 if __name__ == '__main__':
     NewOpenCVTests.bootstrap()
index 1f672f4bc2ecdb10410e764f5082b17a8e4a337c..8000496f7925c3e47017d9cdb0f3d472c4bc290c 100644 (file)
@@ -33,7 +33,7 @@ class gapi_sample_pipelines(NewOpenCVTests):
         comp = cv.GComputation(g_in, g_out)
 
         for pkg in pkgs:
-            actual = comp.apply(in_mat, args=cv.compile_args(pkg))
+            actual = comp.apply(cv.gin(in_mat), args=cv.compile_args(pkg))
             # Comparison
             self.assertEqual(0.0, cv.norm(expected, actual, cv.NORM_INF))
 
index 60119f717ac6f11156154884ae0eff60032d8d12..b8d2fafca77168ac3d1c4bc671083200ef528032 100644 (file)
@@ -129,15 +129,14 @@ static bool formats_are_same(const cv::GMetaArgs& metas1, const cv::GMetaArgs& m
                      });
 }
 
-void cv::GComputation::apply(GRunArgs &&ins, GRunArgsP &&outs, GCompileArgs &&args)
+void cv::GComputation::recompile(GMetaArgs&& in_metas, GCompileArgs &&args)
 {
-    const auto in_metas = descr_of(ins);
     // FIXME Graph should be recompiled when GCompileArgs have changed
     if (m_priv->m_lastMetas != in_metas)
     {
         if (m_priv->m_lastCompiled &&
-            m_priv->m_lastCompiled.canReshape() &&
-            formats_are_same(m_priv->m_lastMetas, in_metas))
+                m_priv->m_lastCompiled.canReshape() &&
+                formats_are_same(m_priv->m_lastMetas, in_metas))
         {
             m_priv->m_lastCompiled.reshape(in_metas, args);
         }
@@ -148,6 +147,11 @@ void cv::GComputation::apply(GRunArgs &&ins, GRunArgsP &&outs, GCompileArgs &&ar
         }
         m_priv->m_lastMetas = in_metas;
     }
+}
+
+void cv::GComputation::apply(GRunArgs &&ins, GRunArgsP &&outs, GCompileArgs &&args)
+{
+    recompile(descr_of(ins), std::move(args));
     m_priv->m_lastCompiled(std::move(ins), std::move(outs));
 }
 
@@ -165,6 +169,41 @@ void cv::GComputation::apply(const std::vector<cv::Mat> &ins,
     apply(std::move(call_ins), std::move(call_outs), std::move(args));
 }
 
+// NB: This overload is called from python code
+cv::GRunArgs cv::GComputation::apply(GRunArgs &&ins, GCompileArgs &&args)
+{
+    recompile(descr_of(ins), std::move(args));
+
+    const auto& out_metas = m_priv->m_lastCompiled.outMetas();
+    GRunArgs run_args;
+    GRunArgsP outs;
+    run_args.reserve(out_metas.size());
+    outs.reserve(out_metas.size());
+
+    for (auto&& meta : out_metas)
+    {
+        switch (meta.index())
+        {
+            case cv::GMetaArg::index_of<cv::GMatDesc>():
+            {
+                run_args.emplace_back(cv::Mat{});
+                outs.emplace_back(&cv::util::get<cv::Mat>(run_args.back()));
+                break;
+            }
+            case cv::GMetaArg::index_of<cv::GScalarDesc>():
+            {
+                run_args.emplace_back(cv::Scalar{});
+                outs.emplace_back(&cv::util::get<cv::Scalar>(run_args.back()));
+                break;
+            }
+            default:
+                util::throw_error(std::logic_error("Only cv::GMat and cv::GScalar are supported for python output"));
+        }
+    }
+    m_priv->m_lastCompiled(std::move(ins), std::move(outs));
+    return run_args;
+}
+
 #if !defined(GAPI_STANDALONE)
 void cv::GComputation::apply(cv::Mat in, cv::Mat &out, GCompileArgs &&args)
 {
index a7ec2afaa57a9ea5288de67ae201071961b6e360..47c0257d1e99f0789e79f089966ab2e7c0c46b04 100644 (file)
@@ -6,6 +6,9 @@
 
 
 #include "test_precomp.hpp"
+
+#include <opencv2/gapi/s11n.hpp>
+
 #include <opencv2/gapi/cpu/gcpukernel.hpp>
 #include <ade/util/zip_range.hpp>
 
@@ -87,6 +90,51 @@ namespace opencv_test
               }
           }
       };
+
+      // NB: Check an apply specifically designed to be called from Python,
+      // but can also be used from C++
+      struct GComputationPythonApplyTest: public ::testing::Test
+      {
+          cv::Size sz;
+          MatType type;
+          cv::Mat in_mat1, in_mat2, out_mat_ocv;
+          cv::GComputation m_c;
+
+          GComputationPythonApplyTest() : sz(cv::Size(300,300)), type(CV_8UC1),
+          in_mat1(sz, type), in_mat2(sz, type), out_mat_ocv(sz, type),
+          m_c([&](){
+                       cv::GMat in1, in2;
+                       cv::GMat out = in1 + in2;
+                       return cv::GComputation(cv::GIn(in1, in2), cv::GOut(out));
+                   })
+          {
+              cv::randu(in_mat1, cv::Scalar::all(0), cv::Scalar::all(255));
+              cv::randu(in_mat2, cv::Scalar::all(0), cv::Scalar::all(255));
+              out_mat_ocv = in_mat1 + in_mat2;
+          }
+
+      };
+  }
+
+  TEST_F(GComputationPythonApplyTest, WithoutSerialization)
+  {
+      auto output = m_c.apply(cv::gin(in_mat1, in_mat2));
+      EXPECT_EQ(1u, output.size());
+
+      const auto& out_mat_gapi = cv::util::get<cv::Mat>(output[0]);
+      EXPECT_EQ(0, cvtest::norm(out_mat_ocv, out_mat_gapi, NORM_INF));
+  }
+
+  TEST_F(GComputationPythonApplyTest, WithSerialization)
+  {
+      auto p = cv::gapi::serialize(m_c);
+      auto c = cv::gapi::deserialize<cv::GComputation>(p);
+
+      auto output = c.apply(cv::gin(in_mat1, in_mat2));
+      EXPECT_EQ(1u, output.size());
+
+      const auto& out_mat_gapi = cv::util::get<cv::Mat>(output[0]);
+      EXPECT_EQ(0, cvtest::norm(out_mat_ocv, out_mat_gapi, NORM_INF));
   }
 
   TEST_F(GComputationApplyTest, ThrowDontPassCustomKernel)
index f949a340e48c36e9d0ca4d3d25441ecd7bb948c5..feea5e76f2057037202ac8755cd4732bb3f5bb57 100644 (file)
@@ -1954,6 +1954,7 @@ static PyMethodDef special_methods[] = {
 #ifdef HAVE_OPENCV_GAPI
   {"GIn", CV_PY_FN_WITH_KW(pyopencv_cv_GIn), "GIn(...) -> GInputProtoArgs"},
   {"GOut", CV_PY_FN_WITH_KW(pyopencv_cv_GOut), "GOut(...) -> GOutputProtoArgs"},
+  {"gin", CV_PY_FN_WITH_KW(pyopencv_cv_gin), "gin(...) -> GRunArgs"},
 #endif
   {NULL, NULL},
 };