From e998d89e88811c88f68f903aa7bae4cc5047e85a Mon Sep 17 00:00:00 2001 From: Anatoliy Talamanov Date: Tue, 29 Sep 2020 13:45:40 +0300 Subject: [PATCH] Implement cv.gin and multiple output for python --- modules/gapi/include/opencv2/gapi/core.hpp | 2 +- .../include/opencv2/gapi/gcomputation.hpp | 8 +- modules/gapi/misc/python/pyopencv_gapi.hpp | 128 +++++++++++++++++- modules/gapi/misc/python/shadow_gapi.hpp | 2 + .../gapi/misc/python/test/test_gapi_core.py | 45 +++++- .../python/test/test_gapi_sample_pipelines.py | 2 +- modules/gapi/src/api/gcomputation.cpp | 47 ++++++- modules/gapi/test/gapi_gcomputation_tests.cpp | 48 +++++++ modules/python/src2/cv2.cpp | 1 + 9 files changed, 269 insertions(+), 14 deletions(-) diff --git a/modules/gapi/include/opencv2/gapi/core.hpp b/modules/gapi/include/opencv2/gapi/core.hpp index 8ecba2b9d6..0313aad093 100644 --- a/modules/gapi/include/opencv2/gapi/core.hpp +++ b/modules/gapi/include/opencv2/gapi/core.hpp @@ -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 threshold(const GMat& src, const GScalar& maxval, int type); +GAPI_EXPORTS_W std::tuple threshold(const GMat& src, const GScalar& maxval, int type); /** @brief Applies a range-level threshold to each matrix element. diff --git a/modules/gapi/include/opencv2/gapi/gcomputation.hpp b/modules/gapi/include/opencv2/gapi/gcomputation.hpp index b24766a232..c03bb9d0a4 100644 --- a/modules/gapi/include/opencv2/gapi/gcomputation.hpp +++ b/modules/gapi/include/opencv2/gapi/gcomputation.hpp @@ -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& ins, // Compatibility overload const std::vector& 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(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 m_priv; }; diff --git a/modules/gapi/misc/python/pyopencv_gapi.hpp b/modules/gapi/misc/python/pyopencv_gapi.hpp index 7ef4cac57f..b4a16cd6c7 100644 --- a/modules/gapi/misc/python/pyopencv_gapi.hpp +++ b/modules/gapi/misc/python/pyopencv_gapi.hpp @@ -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& 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(): + { + const auto& m = util::get(v); + return pyopencv_from(m); + } + + case GRunArg::index_of(): + { + const auto& s = util::get(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 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(pyopencv_GScalar_TypePtr))) { + if (PyObject_TypeCheck(item, reinterpret_cast(pyopencv_GScalar_TypePtr))) + { args.emplace_back(reinterpret_cast(item)->v); - } else if (PyObject_TypeCheck(item, reinterpret_cast(pyopencv_GMat_TypePtr))) { + } + else if (PyObject_TypeCheck(item, reinterpret_cast(pyopencv_GMat_TypePtr))) + { args.emplace_back(reinterpret_cast(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(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 diff --git a/modules/gapi/misc/python/shadow_gapi.hpp b/modules/gapi/misc/python/shadow_gapi.hpp index 2150b86cff..dab083def7 100644 --- a/modules/gapi/misc/python/shadow_gapi.hpp +++ b/modules/gapi/misc/python/shadow_gapi.hpp @@ -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; using GProtoOutputArgs = GIOProtoArgs; diff --git a/modules/gapi/misc/python/test/test_gapi_core.py b/modules/gapi/misc/python/test/test_gapi_core.py index 7720dbc398..b219ce1543 100644 --- a/modules/gapi/misc/python/test/test_gapi_core.py +++ b/modules/gapi/misc/python/test/test_gapi_core.py @@ -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() diff --git a/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py b/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py index 1f672f4bc2..8000496f79 100644 --- a/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py +++ b/modules/gapi/misc/python/test/test_gapi_sample_pipelines.py @@ -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)) diff --git a/modules/gapi/src/api/gcomputation.cpp b/modules/gapi/src/api/gcomputation.cpp index 60119f717a..b8d2fafca7 100644 --- a/modules/gapi/src/api/gcomputation.cpp +++ b/modules/gapi/src/api/gcomputation.cpp @@ -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 &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(): + { + run_args.emplace_back(cv::Mat{}); + outs.emplace_back(&cv::util::get(run_args.back())); + break; + } + case cv::GMetaArg::index_of(): + { + run_args.emplace_back(cv::Scalar{}); + outs.emplace_back(&cv::util::get(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) { diff --git a/modules/gapi/test/gapi_gcomputation_tests.cpp b/modules/gapi/test/gapi_gcomputation_tests.cpp index a7ec2afaa5..47c0257d1e 100644 --- a/modules/gapi/test/gapi_gcomputation_tests.cpp +++ b/modules/gapi/test/gapi_gcomputation_tests.cpp @@ -6,6 +6,9 @@ #include "test_precomp.hpp" + +#include + #include #include @@ -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(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(p); + + auto output = c.apply(cv::gin(in_mat1, in_mat2)); + EXPECT_EQ(1u, output.size()); + + const auto& out_mat_gapi = cv::util::get(output[0]); + EXPECT_EQ(0, cvtest::norm(out_mat_ocv, out_mat_gapi, NORM_INF)); } TEST_F(GComputationApplyTest, ThrowDontPassCustomKernel) diff --git a/modules/python/src2/cv2.cpp b/modules/python/src2/cv2.cpp index f949a340e4..feea5e76f2 100644 --- a/modules/python/src2/cv2.cpp +++ b/modules/python/src2/cv2.cpp @@ -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}, };