mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
feat: G-API: Custom stream sources in Python (#27276)
Implemented: - A C++ proxy class PythonCustomStreamSource that implements the IStreamSource interface. This class acts as a bridge between G-API’s internal streaming engine and user-defined Python objects. Internally, it stores a reference to a Python object (PyObject*) and is responsible for: calling the Python object’s pull() method to retrieve the next frame, calling the descr_of() method to obtain the frame format description, acquiring and releasing the Python GIL as needed, converting the returned numpy.ndarray into cv::Mat, and handling any exceptions or conversion errors with proper diagnostics. - A Python-facing factory function, cv.gapi.wip.make_py_src(), which takes a Python object as an argument and wraps it into a cv::Ptr<IStreamSource>. Internally, this function constructs a PythonCustomStreamSource instance and passes the Python object to it. This design allows Python users to define any class that implements two methods: pull() and descr_of(). No subclassing or special decorators are required on the Python side. The user simply needs to implement the expected interface. Co-authored-by: Leonor Francisco <leonor.francisco@tecnico.ulisboa.pt>
This commit is contained in:
@@ -66,6 +66,7 @@ file(GLOB gapi_ext_hdrs
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/streaming/onevpl/*.hpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/plaidml/*.hpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/util/*.hpp"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/include/opencv2/${name}/pysrc/*.hpp"
|
||||
)
|
||||
|
||||
set(gapi_srcs
|
||||
@@ -242,6 +243,9 @@ set(gapi_srcs
|
||||
src/streaming/gstreamer/gstreamer_media_adapter.cpp
|
||||
src/streaming/gstreamer/gstreamerenv.cpp
|
||||
|
||||
# Python Custom Stream source
|
||||
src/pysrc/python_stream_source.cpp
|
||||
|
||||
# Utils (ITT tracing)
|
||||
src/utils/itt.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP
|
||||
#define OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP
|
||||
#include <opencv2/gapi/streaming/source.hpp>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
namespace cv {
|
||||
namespace gapi {
|
||||
namespace wip {
|
||||
|
||||
/**
|
||||
* @brief Creates a G-API IStreamSource that delegates to a Python-defined source.
|
||||
*
|
||||
* This factory function wraps a Python object (for example, an instance of a class
|
||||
* implementing a `pull()` and a `descr_of()` method) into a `cv::gapi::wip::IStreamSource`,
|
||||
* enabling it to be used within a G-API computation graph. The OpenCV Python bindings
|
||||
* automatically convert the PyObject into a `cv::Ptr<IStreamSource>`.
|
||||
*
|
||||
* @param src
|
||||
* A `cv::Ptr<IStreamSource>` that internally holds the original Python object.
|
||||
*
|
||||
* @return
|
||||
* A `cv::Ptr<IStreamSource>` that wraps the provided Python object. On each frame pull,
|
||||
* G-API will:
|
||||
* - Acquire the Python GIL
|
||||
* - Call the Python object’s `pull()` method
|
||||
* - Convert the resulting NumPy array to a `cv::Mat`
|
||||
* - Pass the `cv::Mat` into the G-API pipeline
|
||||
*
|
||||
* @note
|
||||
* In Python, you can use the returned `make_py_src` as follows:
|
||||
*
|
||||
* @code{.py}
|
||||
* class MyClass:
|
||||
* def __init__(self):
|
||||
* # Initialize your source
|
||||
* def pull(self):
|
||||
* # Return the next frame as a numpy.ndarray or None for end-of-stream
|
||||
* def descr_of(self):
|
||||
* # Return a numpy.ndarray that describes the format of the frames
|
||||
*
|
||||
* # Create a G-API source from a Python class
|
||||
* py_src = cv.gapi.wip.make_py_src(MyClass())
|
||||
*
|
||||
* # Define a simple graph: input → copy → output
|
||||
* g_in = cv.GMat()
|
||||
* g_out = cv.gapi.copy(g_in)
|
||||
* graph = cv.GComputation(g_in, g_out)
|
||||
*
|
||||
* # Compile the pipeline for streaming and assign the source
|
||||
* pipeline = graph.compileStreaming()
|
||||
* pipeline.setSource([py_src])
|
||||
* pipeline.start()
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
CV_EXPORTS_W cv::Ptr<cv::gapi::wip::IStreamSource>
|
||||
make_py_src(const cv::Ptr<cv::gapi::wip::IStreamSource>& src);
|
||||
|
||||
|
||||
} // namespace wip
|
||||
} // namespace gapi
|
||||
} // namespace cv
|
||||
|
||||
|
||||
#endif // OPENCV_GAPI_PYSRC_PYTHONSTREAMSOURCE_HPP
|
||||
@@ -1162,6 +1162,161 @@ bool pyopencv_to(PyObject* obj, cv::GProtoOutputArgs& value, const ArgInfo& info
|
||||
}
|
||||
}
|
||||
|
||||
namespace cv {
|
||||
namespace gapi {
|
||||
namespace wip {
|
||||
|
||||
/**
|
||||
* @class PythonCustomStreamSource
|
||||
* @brief Wraps a Python-defined frame source as an IStreamSource for G-API.
|
||||
*
|
||||
* This class allows a G-API pipeline to pull frames from a Python object. The Python object
|
||||
* must implement at a `pull()` method which must return a `numpy.ndarray` containing
|
||||
* the next frame, or `None` to signal end-of-stream. It can also implement a `descr_of()`
|
||||
* method which must return a `numpy.ndarray` that describes the format
|
||||
* (data type, number of channels, height, width) of the frames produced.
|
||||
*/
|
||||
|
||||
class PythonCustomStreamSource : public IStreamSource
|
||||
{
|
||||
public:
|
||||
PythonCustomStreamSource(PyObject* _obj = nullptr) : obj(_obj)
|
||||
{
|
||||
if (obj)
|
||||
Py_INCREF(obj);
|
||||
}
|
||||
|
||||
~PythonCustomStreamSource()
|
||||
{
|
||||
if (obj)
|
||||
Py_DECREF(obj);
|
||||
}
|
||||
|
||||
bool pull(cv::gapi::wip::Data& data) CV_OVERRIDE
|
||||
{
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
PyObject* src = reinterpret_cast<PyObject*>(obj);
|
||||
|
||||
PyGILState_STATE gstate;
|
||||
gstate = PyGILState_Ensure();
|
||||
|
||||
PyObject* result = PyObject_CallMethodObjArgs(src, PyUnicode_FromString("pull"), NULL);
|
||||
bool hasPyPullError = PyErr_Occurred() != nullptr;
|
||||
|
||||
if (!result)
|
||||
{
|
||||
PyErr_Print();
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): call to .pull() failed");
|
||||
}
|
||||
|
||||
if (result == Py_None)
|
||||
{
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PyArray_Check(result))
|
||||
{
|
||||
PyErr_Format(PyExc_TypeError, "Expected numpy.ndarray from .pull()");
|
||||
PyErr_Print();
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): .pull() did not return a numpy.ndarray");
|
||||
}
|
||||
|
||||
cv::Mat mat;
|
||||
ArgInfo info("pull return", 0);
|
||||
if (!pyopencv_to(result, mat, info) || PyErr_Occurred())
|
||||
{
|
||||
PyErr_Print();
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::pull(): failed to convert numpy to cv::Mat");
|
||||
}
|
||||
|
||||
if (mat.empty())
|
||||
{
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
return false;
|
||||
}
|
||||
|
||||
data = mat;
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
|
||||
if (hasPyPullError)
|
||||
CV_Error(cv::Error::StsError, "Python .pull() call error");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
GMetaArg descr_of() const CV_OVERRIDE
|
||||
{
|
||||
if (!obj)
|
||||
return cv::GMetaArg(cv::GFrameDesc{cv::MediaFormat::BGR, cv::Size(640, 480)});
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
PyObject* result = PyObject_CallMethodObjArgs(obj, PyUnicode_FromString("descr_of"), NULL);
|
||||
if (!result)
|
||||
{
|
||||
PyErr_Print();
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): conversion error");
|
||||
}
|
||||
|
||||
if (!PyArray_Check(result)) {
|
||||
PyErr_Format(PyExc_TypeError, "Expected numpy.ndarray from .descr_of()");
|
||||
PyErr_Print();
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): did not return a numpy.ndarray");
|
||||
}
|
||||
|
||||
cv::Mat mat;
|
||||
ArgInfo info("descr_of return", 0);
|
||||
if (!pyopencv_to(result, mat, info))
|
||||
{
|
||||
PyErr_Print();
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
CV_Error(cv::Error::StsError, "PythonCustomStreamSource::descr_of(): conversion error");
|
||||
}
|
||||
|
||||
Py_DECREF(result);
|
||||
PyGILState_Release(gstate);
|
||||
cv::GMatDesc mdesc = cv::descr_of(mat);
|
||||
|
||||
return cv::GMetaArg(mdesc);
|
||||
}
|
||||
|
||||
private:
|
||||
PyObject* obj;
|
||||
};
|
||||
|
||||
inline cv::Ptr<IStreamSource> make_pysrc_from_pyobject(PyObject* obj)
|
||||
{
|
||||
return cv::makePtr<PythonCustomStreamSource>(obj);
|
||||
}
|
||||
|
||||
} // namespace wip
|
||||
} // namespace gapi
|
||||
} // namespace cv
|
||||
|
||||
template<>
|
||||
bool pyopencv_to(PyObject* obj, cv::Ptr<cv::gapi::wip::IStreamSource>& p, const ArgInfo&)
|
||||
{
|
||||
if (!obj)
|
||||
return false;
|
||||
|
||||
p = cv::makePtr<cv::gapi::wip::PythonCustomStreamSource>(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
// extend cv.gapi methods
|
||||
#define PYOPENCV_EXTRA_METHODS_GAPI \
|
||||
{"kernels", CV_PY_FN_WITH_KW(pyopencv_cv_gapi_kernels), "kernels(...) -> GKernelPackage"}, \
|
||||
|
||||
@@ -539,7 +539,38 @@ try:
|
||||
self.assertEqual(0.0, cv.norm(convertNV12p2BGR(expected1), actual1, cv.NORM_INF))
|
||||
self.assertEqual(0.0, cv.norm(convertNV12p2BGR(expected2), actual2, cv.NORM_INF))
|
||||
|
||||
def test_python_custom_stream_source(self):
|
||||
class MySource:
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
def pull(self):
|
||||
if self.count >= 3:
|
||||
return None
|
||||
self.count += 1
|
||||
return np.ones((10, 10, 3), np.uint8) * self.count
|
||||
|
||||
def descr_of(self):
|
||||
return np.zeros((10, 10, 3), np.uint8)
|
||||
|
||||
g_in = cv.GMat()
|
||||
g_out = cv.gapi.copy(g_in)
|
||||
c = cv.GComputation(g_in, g_out)
|
||||
|
||||
comp = c.compileStreaming()
|
||||
|
||||
src = cv.gapi.wip.make_py_src(MySource())
|
||||
comp.setSource([src])
|
||||
comp.start()
|
||||
|
||||
frames = []
|
||||
while True:
|
||||
has_frame, frame = comp.pull()
|
||||
if not has_frame:
|
||||
break
|
||||
frames.append(frame)
|
||||
|
||||
self.assertEqual(len(frames), 3)
|
||||
|
||||
except unittest.SkipTest as e:
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#include <opencv2/gapi/pysrc/python_stream_source.hpp>
|
||||
#include <opencv2/gapi/streaming/source.hpp>
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
#include <opencv2/core.hpp>
|
||||
|
||||
namespace cv {
|
||||
namespace gapi {
|
||||
namespace wip {
|
||||
|
||||
cv::Ptr<cv::gapi::wip::IStreamSource> make_py_src(const cv::Ptr<cv::gapi::wip::IStreamSource>& src)
|
||||
{
|
||||
return src;
|
||||
}
|
||||
|
||||
} // namespace wip
|
||||
} // namespace gapi
|
||||
} // namespace cv
|
||||
Reference in New Issue
Block a user