mirror of
https://github.com/opencv/opencv.git
synced 2026-07-29 15:23:05 +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:
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user