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
This commit is contained in:
Dmitry Kurtaev 2025-05-14 17:01:45 +03:00 committed by GitHub
parent 547cef4e88
commit 67ba045e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 296 additions and 11 deletions

View File

@ -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<char*>(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,
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, [], [] ]

View File

@ -45,7 +45,9 @@ typedef std::vector<DMatch> vector_DMatch;
typedef std::vector<String> vector_String;
typedef std::vector<std::string> vector_string;
typedef std::vector<Scalar> vector_Scalar;
#ifdef HAVE_OPENCV_OBJDETECT
typedef std::vector<aruco::Dictionary> vector_Dictionary;
#endif // HAVE_OPENCV_OBJDETECT
typedef std::vector<std::vector<char> > vector_vector_char;
typedef std::vector<std::vector<Point> > vector_vector_Point;

View File

@ -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<typename T>
struct PyOpenCV_Converter
< T, typename std::enable_if< std::is_same<long long, T>::value && !std::is_same<long long, int64>::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);

View File

@ -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

View File

@ -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"),

View File

@ -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;

View File

@ -0,0 +1 @@
misc/java/src/cpp/videoio_converters.hpp

View File

@ -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.<br>",
" * <b>NOTE</b>: 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<JavaStreamReader>(env, source)",
"j_import": "org.opencv.videoio.IStreamReader"
},
"vector_VideoCaptureAPIs": {
"j_type": "List<Integer>",
"jn_type": "List<Integer>",
"jni_type": "jobject",
"jni_var": "std::vector< cv::VideoCaptureAPIs > %(n)s",
"suffix": "Ljava_util_List",
"v_type": "vector_VideoCaptureAPIs"
}
}
}

View File

@ -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<jsize>(size));
if (!jBuffer)
return 0;
jlong res = env->CallLongMethod(obj, m_read, jBuffer, size);
env->GetByteArrayRegion(jBuffer, 0, static_cast<jsize>(size), reinterpret_cast<jbyte*>(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<cv::VideoCaptureAPIs>& 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, "<init>", "(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;
}

View File

@ -0,0 +1,25 @@
#ifndef VIDEOIO_CONVERTERS_HPP
#define VIDEOIO_CONVERTERS_HPP
#include <jni.h>
#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<cv::VideoCaptureAPIs>& vs);
#endif

View File

@ -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);
}
}