From 67ba045e3b0c5b06510817e7153ce50b3f1b5151 Mon Sep 17 00:00:00 2001 From: Dmitry Kurtaev Date: Wed, 14 May 2025 17:01:45 +0300 Subject: [PATCH] Merge pull request #27284 from dkurt:java_video_capture_read Java VideoCapture buffered stream constructor #27284 ### Pull Request Readiness Checklist resolves #26809 See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- modules/java/generator/gen_java.py | 22 +++-- modules/python/src2/cv2.cpp | 2 + modules/python/src2/cv2_convert.hpp | 23 +++++ modules/python/src2/hdr_parser.py | 2 + .../predefined_types.py | 1 + modules/videoio/include/opencv2/videoio.hpp | 12 ++- modules/videoio/misc/java/filelist_common | 1 + modules/videoio/misc/java/gen_dict.json | 40 ++++++++ .../misc/java/src/cpp/videoio_converters.cpp | 97 +++++++++++++++++++ .../misc/java/src/cpp/videoio_converters.hpp | 25 +++++ .../misc/java/test/VideoCaptureTest.java | 82 +++++++++++++++- 11 files changed, 296 insertions(+), 11 deletions(-) create mode 100644 modules/videoio/misc/java/filelist_common create mode 100644 modules/videoio/misc/java/gen_dict.json create mode 100644 modules/videoio/misc/java/src/cpp/videoio_converters.cpp create mode 100644 modules/videoio/misc/java/src/cpp/videoio_converters.hpp diff --git a/modules/java/generator/gen_java.py b/modules/java/generator/gen_java.py index 670d36c957..a77ca199f3 100755 --- a/modules/java/generator/gen_java.py +++ b/modules/java/generator/gen_java.py @@ -65,6 +65,7 @@ type_dict = { "char" : { "j_type" : "char", "jn_type" : "char", "jni_type" : "jchar", "suffix" : "C" }, "int" : { "j_type" : "int", "jn_type" : "int", "jni_type" : "jint", "suffix" : "I" }, "long" : { "j_type" : "int", "jn_type" : "int", "jni_type" : "jint", "suffix" : "I" }, + "long long" : { "j_type" : "long", "jn_type" : "long", "jni_type" : "jlong", "suffix" : "J" }, "float" : { "j_type" : "float", "jn_type" : "float", "jni_type" : "jfloat", "suffix" : "F" }, "double" : { "j_type" : "double", "jn_type" : "double", "jni_type" : "jdouble", "suffix" : "D" }, "size_t" : { "j_type" : "long", "jn_type" : "long", "jni_type" : "jlong", "suffix" : "J" }, @@ -89,6 +90,13 @@ type_dict = { 'v_type': 'string', 'j_import': 'java.lang.String' }, + "byte[]": { + "j_type" : "byte[]", + "jn_type": "byte[]", + "jni_type": "jbyteArray", + "jni_name": "n_%(n)s", + "jni_var": "char* n_%(n)s = reinterpret_cast(env->GetByteArrayElements(%(n)s, NULL))", + }, } # Defines a rule to add extra prefixes for names from specific namespaces. @@ -511,14 +519,14 @@ class JavaWrapperGenerator(object): if classinfo.base: classinfo.addImports(classinfo.base) - type_dict.setdefault("Ptr_"+name, {}).update( - { "j_type" : classinfo.jname, - "jn_type" : "long", "jn_args" : (("__int64", ".getNativeObjAddr()"),), - "jni_name" : "*((Ptr<"+classinfo.fullNameCPP()+">*)%(n)s_nativeObj)", "jni_type" : "jlong", - "suffix" : "J", - "j_import" : "org.opencv.%s.%s" % (self.module, classinfo.jname) + if ("Ptr_"+name) not in type_dict: + type_dict["Ptr_"+name] = { + "j_type" : classinfo.jname, + "jn_type" : "long", "jn_args" : (("__int64", ".getNativeObjAddr()"),), + "jni_name" : "*((Ptr<"+classinfo.fullNameCPP()+">*)%(n)s_nativeObj)", "jni_type" : "jlong", + "suffix" : "J", + "j_import" : "org.opencv.%s.%s" % (self.module, classinfo.jname) } - ) logging.info('ok: class %s, name: %s, base: %s', classinfo, name, classinfo.base) def add_const(self, decl, enumType=None): # [ "const cname", val, [], [] ] diff --git a/modules/python/src2/cv2.cpp b/modules/python/src2/cv2.cpp index 4a3181bdde..b7df25e510 100644 --- a/modules/python/src2/cv2.cpp +++ b/modules/python/src2/cv2.cpp @@ -45,7 +45,9 @@ typedef std::vector vector_DMatch; typedef std::vector vector_String; typedef std::vector vector_string; typedef std::vector vector_Scalar; +#ifdef HAVE_OPENCV_OBJDETECT typedef std::vector vector_Dictionary; +#endif // HAVE_OPENCV_OBJDETECT typedef std::vector > vector_vector_char; typedef std::vector > vector_vector_Point; diff --git a/modules/python/src2/cv2_convert.hpp b/modules/python/src2/cv2_convert.hpp index 979425c3f9..5ef81855ea 100644 --- a/modules/python/src2/cv2_convert.hpp +++ b/modules/python/src2/cv2_convert.hpp @@ -182,6 +182,29 @@ struct PyOpenCV_Converter } }; +// There is conflict between "long long" and "int64". +// They are the same type on some 32-bit platforms. +template +struct PyOpenCV_Converter + < T, typename std::enable_if< std::is_same::value && !std::is_same::value >::type > +{ + static inline PyObject* from(const long long& value) + { + return PyLong_FromLongLong(value); + } + + static inline bool to(PyObject* obj, long long& value, const ArgInfo& info) + { + CV_UNUSED(info); + if(!obj || obj == Py_None) + return true; + else if(PyLong_Check(obj)) + value = PyLong_AsLongLong(obj); + else + return false; + return value != (long long)-1 || !PyErr_Occurred(); + } +}; // --- uchar template<> bool pyopencv_to(PyObject* obj, uchar& value, const ArgInfo& info); diff --git a/modules/python/src2/hdr_parser.py b/modules/python/src2/hdr_parser.py index 27f0fe0963..afcc390dee 100755 --- a/modules/python/src2/hdr_parser.py +++ b/modules/python/src2/hdr_parser.py @@ -190,6 +190,8 @@ class CppHeaderParser(object): angle_stack[-1] += 1 elif arg_type == "struct": arg_type += " " + w + elif prev_w in ["signed", "unsigned", "short", "long"] and w in ["char", "short", "int", "long"]: + arg_type += " " + w elif arg_type and arg_type != "~": arg_name = " ".join(word_list[wi:]) break diff --git a/modules/python/src2/typing_stubs_generation/predefined_types.py b/modules/python/src2/typing_stubs_generation/predefined_types.py index df2fd3cc58..6879d1a18d 100644 --- a/modules/python/src2/typing_stubs_generation/predefined_types.py +++ b/modules/python/src2/typing_stubs_generation/predefined_types.py @@ -28,6 +28,7 @@ _PREDEFINED_TYPES = ( PrimitiveTypeNode.int_("uint32_t"), PrimitiveTypeNode.int_("size_t"), PrimitiveTypeNode.int_("int64_t"), + PrimitiveTypeNode.int_("long long"), PrimitiveTypeNode.float_("float"), PrimitiveTypeNode.float_("double"), PrimitiveTypeNode.bool_("bool"), diff --git a/modules/videoio/include/opencv2/videoio.hpp b/modules/videoio/include/opencv2/videoio.hpp index 6b5c1b6cc4..bdda38e5e2 100644 --- a/modules/videoio/include/opencv2/videoio.hpp +++ b/modules/videoio/include/opencv2/videoio.hpp @@ -726,8 +726,14 @@ class CV_EXPORTS_W IStreamReader public: virtual ~IStreamReader(); - /** @brief Read bytes from stream */ - virtual long long read(char* buffer, long long size) = 0; + /** @brief Read bytes from stream + * + * @param buffer already allocated buffer of at least @p size bytes + * @param size maximum number of bytes to read + * + * @return actual number of read bytes + */ + CV_WRAP virtual long long read(char* buffer, long long size) = 0; /** @brief Sets the stream position * @@ -736,7 +742,7 @@ public: * * @see fseek */ - virtual long long seek(long long offset, int origin) = 0; + CV_WRAP virtual long long seek(long long offset, int origin) = 0; }; class IVideoCapture; diff --git a/modules/videoio/misc/java/filelist_common b/modules/videoio/misc/java/filelist_common new file mode 100644 index 0000000000..c19b6f83ef --- /dev/null +++ b/modules/videoio/misc/java/filelist_common @@ -0,0 +1 @@ +misc/java/src/cpp/videoio_converters.hpp diff --git a/modules/videoio/misc/java/gen_dict.json b/modules/videoio/misc/java/gen_dict.json new file mode 100644 index 0000000000..0c18f4284a --- /dev/null +++ b/modules/videoio/misc/java/gen_dict.json @@ -0,0 +1,40 @@ +{ + "ManualFuncs" : { + "IStreamReader" : { + "IStreamReader" : { + "j_code" : [ + "\n", + "/**", + " * Constructor of streaming callback object with abstract 'read' and 'seek' methods that should be implemented in Java code.
", + " * NOTE: Implemented callbacks should be called from the creation thread to avoid JNI performance degradation", + "*/", + "protected IStreamReader() { nativeObj = 0; }", + "\n" + ], + "jn_code": [], + "cpp_code": [] + } + } + }, + "func_arg_fix" : { + "read": { "buffer": {"ctype" : "byte[]"} } + }, + "type_dict": { + "Ptr_IStreamReader": { + "j_type": "IStreamReader", + "jn_type": "IStreamReader", + "jni_name": "n_%(n)s", + "jni_type": "jobject", + "jni_var": "auto n_%(n)s = makePtr(env, source)", + "j_import": "org.opencv.videoio.IStreamReader" + }, + "vector_VideoCaptureAPIs": { + "j_type": "List", + "jn_type": "List", + "jni_type": "jobject", + "jni_var": "std::vector< cv::VideoCaptureAPIs > %(n)s", + "suffix": "Ljava_util_List", + "v_type": "vector_VideoCaptureAPIs" + } + } +} diff --git a/modules/videoio/misc/java/src/cpp/videoio_converters.cpp b/modules/videoio/misc/java/src/cpp/videoio_converters.cpp new file mode 100644 index 0000000000..9f7699a2ce --- /dev/null +++ b/modules/videoio/misc/java/src/cpp/videoio_converters.cpp @@ -0,0 +1,97 @@ +#include "videoio_converters.hpp" + +class JNIEnvHandler +{ +public: + JNIEnvHandler(JavaVM* _vm) : vm(_vm) + { + jint res = vm->GetEnv((void**)&env, JNI_VERSION_1_6); + if (res == JNI_EDETACHED) + { +#ifdef __ANDROID__ + res = vm->AttachCurrentThread(&env, NULL); +#else + res = vm->AttachCurrentThread((void**)&env, NULL); +#endif // __ANDROID__ + detach = true; + } + } + + ~JNIEnvHandler() + { + if (env && detach) + { + vm->DetachCurrentThread(); + } + } + + JavaVM* vm; + JNIEnv* env = nullptr; + bool detach = false; +}; + +JavaStreamReader::JavaStreamReader(JNIEnv* env, jobject _obj) +{ + obj = env->NewGlobalRef(_obj); + jclass cls = env->GetObjectClass(obj); + m_read = env->GetMethodID(cls, "read", "([BJ)J"); + m_seek = env->GetMethodID(cls, "seek", "(JI)J"); + env->GetJavaVM(&vm); +} + +JavaStreamReader::~JavaStreamReader() +{ + JNIEnvHandler handler(vm); + JNIEnv* env = handler.env; + if (!env) + return; + env->DeleteGlobalRef(obj); +} + +long long JavaStreamReader::read(char* buffer, long long size) +{ + if (!m_read) + return 0; + JNIEnvHandler handler(vm); + JNIEnv* env = handler.env; + if (!env) + return 0; + jbyteArray jBuffer = env->NewByteArray(static_cast(size)); + if (!jBuffer) + return 0; + jlong res = env->CallLongMethod(obj, m_read, jBuffer, size); + env->GetByteArrayRegion(jBuffer, 0, static_cast(size), reinterpret_cast(buffer)); + env->DeleteLocalRef(jBuffer); + return res; +} + +long long JavaStreamReader::seek(long long offset, int way) +{ + JNIEnvHandler handler(vm); + JNIEnv* env = handler.env; + if (!env) + return 0; + if (!m_seek) + return 0; + return env->CallLongMethod(obj, m_seek, offset, way); +} + +// Same as dnn::vector_Target_to_List +jobject vector_VideoCaptureAPIs_to_List(JNIEnv* env, std::vector& vs) +{ + static jclass juArrayList = ARRAYLIST(env); + static jmethodID m_create = CONSTRUCTOR(env, juArrayList); + jmethodID m_add = LIST_ADD(env, juArrayList); + + static jclass jInteger = env->FindClass("java/lang/Integer"); + static jmethodID m_create_Integer = env->GetMethodID(jInteger, "", "(I)V"); + + jobject result = env->NewObject(juArrayList, m_create, vs.size()); + for (size_t i = 0; i < vs.size(); ++i) + { + jobject element = env->NewObject(jInteger, m_create_Integer, vs[i]); + env->CallBooleanMethod(result, m_add, element); + env->DeleteLocalRef(element); + } + return result; +} diff --git a/modules/videoio/misc/java/src/cpp/videoio_converters.hpp b/modules/videoio/misc/java/src/cpp/videoio_converters.hpp new file mode 100644 index 0000000000..d1ec43e2be --- /dev/null +++ b/modules/videoio/misc/java/src/cpp/videoio_converters.hpp @@ -0,0 +1,25 @@ +#ifndef VIDEOIO_CONVERTERS_HPP +#define VIDEOIO_CONVERTERS_HPP + +#include +#include "opencv_java.hpp" +#include "opencv2/core.hpp" +#include "opencv2/videoio/videoio.hpp" + +class JavaStreamReader : public cv::IStreamReader +{ +public: + JavaStreamReader(JNIEnv* env, jobject obj); + ~JavaStreamReader(); + long long read(char* buffer, long long size) CV_OVERRIDE; + long long seek(long long offset, int way) CV_OVERRIDE; + +private: + JavaVM* vm; + jobject obj; + jmethodID m_read, m_seek; +}; + +jobject vector_VideoCaptureAPIs_to_List(JNIEnv* env, std::vector& vs); + +#endif diff --git a/modules/videoio/misc/java/test/VideoCaptureTest.java b/modules/videoio/misc/java/test/VideoCaptureTest.java index 9609a55620..db862a35d8 100644 --- a/modules/videoio/misc/java/test/VideoCaptureTest.java +++ b/modules/videoio/misc/java/test/VideoCaptureTest.java @@ -1,27 +1,41 @@ package org.opencv.test.videoio; import java.util.List; +import java.io.File; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.io.FileNotFoundException; +import org.opencv.core.Mat; import org.opencv.core.Size; +import org.opencv.core.MatOfInt; import org.opencv.videoio.Videoio; import org.opencv.videoio.VideoCapture; +import org.opencv.videoio.IStreamReader; import org.opencv.test.OpenCVTestCase; public class VideoCaptureTest extends OpenCVTestCase { + private final static String ENV_OPENCV_TEST_DATA_PATH = "OPENCV_TEST_DATA_PATH"; private VideoCapture capture; private boolean isOpened; private boolean isSucceed; + private File testDataPath; @Override protected void setUp() throws Exception { super.setUp(); capture = null; - isTestCaseEnabled = false; isSucceed = false; isOpened = false; + + String envTestDataPath = System.getenv(ENV_OPENCV_TEST_DATA_PATH); + + if(envTestDataPath == null) throw new Exception(ENV_OPENCV_TEST_DATA_PATH + " has to be defined!"); + + testDataPath = new File(envTestDataPath); } public void testGrab() { @@ -61,4 +75,70 @@ public class VideoCaptureTest extends OpenCVTestCase { assertNotNull(capture); } + public void testConstructorStream() throws FileNotFoundException { + // Check backend is available + Integer apiPref = Videoio.CAP_ANY; + for (Integer backend : Videoio.getStreamBufferedBackends()) + { + if (!Videoio.hasBackend(backend)) + continue; + if (!Videoio.isBackendBuiltIn(backend)) + { + int[] abi = new int[1], api = new int[1]; + Videoio.getStreamBufferedBackendPluginVersion(backend, abi, api); + if (abi[0] < 1 || (abi[0] == 1 && api[0] < 2)) + continue; + } + apiPref = backend; + break; + } + if (apiPref == Videoio.CAP_ANY) + { + throw new TestSkipException(); + } + + RandomAccessFile f = new RandomAccessFile(new File(testDataPath, "cv/video/768x576.avi"), "r"); + + IStreamReader stream = new IStreamReader() + { + @Override + public long read(byte[] buffer, long size) + { + assertEquals(buffer.length, size); + try + { + return Math.max(f.read(buffer), 0); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + return 0; + } + } + + @Override + public long seek(long offset, int origin) + { + try + { + if (origin == 0) + f.seek(offset); + return f.getFilePointer(); + } + catch (IOException e) + { + System.out.println(e.getMessage()); + return 0; + } + } + }; + capture = new VideoCapture(stream, apiPref, new MatOfInt()); + assertNotNull(capture); + assertTrue(capture.isOpened()); + + Mat frame = new Mat(); + assertTrue(capture.read(frame)); + assertEquals(frame.rows(), 576); + assertEquals(frame.cols(), 768); + } }