diff --git a/modules/core/include/opencv2/core.hpp b/modules/core/include/opencv2/core.hpp index 7b5108fcc4..3cd5901af4 100644 --- a/modules/core/include/opencv2/core.hpp +++ b/modules/core/include/opencv2/core.hpp @@ -349,6 +349,9 @@ be set to the default -1. In this case, the output array will have the same dept array, be it src1, src2 or both. @note Saturation is not applied when the output array has the depth CV_32S. You may even get result of an incorrect sign in the case of overflow. +@note (Python) Be careful to difference behaviour between src1/src2 are single number and they are tuple/array. +`add(src,X)` means `add(src,(X,X,X,X))`. +`add(src,(X,))` means `add(src,(X,0,0,0))`. @param src1 first input array or a scalar. @param src2 second input array or a scalar. @param dst output array that has the same size and number of channels as the input array(s); the @@ -390,6 +393,9 @@ in the first case, when src1.depth() == src2.depth(), dtype can be set to the de case the output array will have the same depth as the input array, be it src1, src2 or both. @note Saturation is not applied when the output array has the depth CV_32S. You may even get result of an incorrect sign in the case of overflow. +@note (Python) Be careful to difference behaviour between src1/src2 are single number and they are tuple/array. +`subtract(src,X)` means `subtract(src,(X,X,X,X))`. +`subtract(src,(X,))` means `subtract(src,(X,0,0,0))`. @param src1 first input array or a scalar. @param src2 second input array or a scalar. @param dst output array of the same size and the same number of channels as the input array. @@ -415,6 +421,9 @@ For a not-per-element matrix product, see gemm . @note Saturation is not applied when the output array has the depth CV_32S. You may even get result of an incorrect sign in the case of overflow. +@note (Python) Be careful to difference behaviour between src1/src2 are single number and they are tuple/array. +`multiply(src,X)` means `multiply(src,(X,X,X,X))`. +`multiply(src,(X,))` means `multiply(src,(X,0,0,0))`. @param src1 first input array. @param src2 second input array of the same size and the same type as src1. @param dst output array of the same size and type as src1. @@ -443,6 +452,9 @@ Expect correct IEEE-754 behaviour for floating-point data (with NaN, Inf result @note Saturation is not applied when the output array has the depth CV_32S. You may even get result of an incorrect sign in the case of overflow. +@note (Python) Be careful to difference behaviour between src1/src2 are single number and they are tuple/array. +`divide(src,X)` means `divide(src,(X,X,X,X))`. +`divide(src,(X,))` means `divide(src,(X,0,0,0))`. @param src1 first input array. @param src2 second input array of the same size and type as src1. @param scale scalar factor. @@ -1412,6 +1424,9 @@ The function cv::absdiff calculates: multi-channel arrays, each channel is processed independently. @note Saturation is not applied when the arrays have the depth CV_32S. You may even get a negative value in the case of overflow. +@note (Python) Be careful to difference behaviour between src1/src2 are single number and they are tuple/array. +`absdiff(src,X)` means `absdiff(src,(X,X,X,X))`. +`absdiff(src,(X,))` means `absdiff(src,(X,0,0,0))`. @param src1 first input array or a scalar. @param src2 second input array or a scalar. @param dst output array that has the same size and type as input arrays. diff --git a/modules/python/src2/cv2.hpp b/modules/python/src2/cv2.hpp index 9293a593f2..b7992582ad 100644 --- a/modules/python/src2/cv2.hpp +++ b/modules/python/src2/cv2.hpp @@ -39,12 +39,20 @@ class ArgInfo { +private: + static const uint32_t arg_outputarg_flag = 0x1; + static const uint32_t arg_arithm_op_src_flag = 0x2; + public: const char* name; bool outputarg; + bool arithm_op_src; // more fields may be added if necessary - ArgInfo(const char* name_, bool outputarg_) : name(name_), outputarg(outputarg_) {} + ArgInfo(const char* name_, uint32_t arg_) : + name(name_), + outputarg((arg_ & arg_outputarg_flag) != 0), + arithm_op_src((arg_ & arg_arithm_op_src_flag) != 0) {} private: ArgInfo(const ArgInfo&) = delete; diff --git a/modules/python/src2/cv2_convert.cpp b/modules/python/src2/cv2_convert.cpp index e9e1fed4fd..40e1608fae 100644 --- a/modules/python/src2/cv2_convert.cpp +++ b/modules/python/src2/cv2_convert.cpp @@ -63,20 +63,39 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) if( PyInt_Check(o) ) { double v[] = {static_cast(PyInt_AsLong((PyObject*)o)), 0., 0., 0.}; + if ( info.arithm_op_src ) + { + // Normally cv.XXX(x) means cv.XXX( (x, 0., 0., 0.) ); + // However cv.add(mat,x) means cv::add(mat, (x,x,x,x) ). + v[1] = v[0]; + v[2] = v[0]; + v[3] = v[0]; + } m = Mat(4, 1, CV_64F, v).clone(); return true; } if( PyFloat_Check(o) ) { double v[] = {PyFloat_AsDouble((PyObject*)o), 0., 0., 0.}; + + if ( info.arithm_op_src ) + { + // Normally cv.XXX(x) means cv.XXX( (x, 0., 0., 0.) ); + // However cv.add(mat,x) means cv::add(mat, (x,x,x,x) ). + v[1] = v[0]; + v[2] = v[0]; + v[3] = v[0]; + } m = Mat(4, 1, CV_64F, v).clone(); return true; } if( PyTuple_Check(o) ) { - int i, sz = (int)PyTuple_Size((PyObject*)o); - m = Mat(sz, 1, CV_64F); - for( i = 0; i < sz; i++ ) + // see https://github.com/opencv/opencv/issues/24057 + const int sz = (int)PyTuple_Size((PyObject*)o); + const int sz2 = info.arithm_op_src ? std::max(4, sz) : sz; // Scalar has 4 elements. + m = Mat::zeros(sz2, 1, CV_64F); + for( int i = 0; i < sz; i++ ) { PyObject* oi = PyTuple_GetItem(o, i); if( PyInt_Check(oi) ) @@ -241,6 +260,31 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) } } + // see https://github.com/opencv/opencv/issues/24057 + if ( ( info.arithm_op_src ) && ( ndims == 1 ) && ( size[0] <= 4 ) ) + { + const int sz = size[0]; // Real Data Length(1, 2, 3 or 4) + const int sz2 = 4; // Scalar has 4 elements. + m = Mat::zeros(sz2, 1, CV_64F); + + const char *base_ptr = PyArray_BYTES(oarr); + for(int i = 0; i < sz; i++ ) + { + PyObject* oi = PyArray_GETITEM(oarr, base_ptr + step[0] * i); + if( PyInt_Check(oi) ) + m.at(i) = (double)PyInt_AsLong(oi); + else if( PyFloat_Check(oi) ) + m.at(i) = (double)PyFloat_AsDouble(oi); + else + { + failmsg("%s has some non-numerical elements", info.name); + m.release(); + return false; + } + } + return true; + } + // handle degenerate case // FIXIT: Don't force 1D for Scalars if( ndims == 0) { @@ -807,7 +851,7 @@ bool pyopencv_to(PyObject* obj, RotatedRect& dst, const ArgInfo& info) } { const String centerItemName = format("'%s' center point", info.name); - const ArgInfo centerItemInfo(centerItemName.c_str(), false); + const ArgInfo centerItemInfo(centerItemName.c_str(), 0); SafeSeqItem centerItem(obj, 0); if (!pyopencv_to(centerItem.item, dst.center, centerItemInfo)) { @@ -816,7 +860,7 @@ bool pyopencv_to(PyObject* obj, RotatedRect& dst, const ArgInfo& info) } { const String sizeItemName = format("'%s' size", info.name); - const ArgInfo sizeItemInfo(sizeItemName.c_str(), false); + const ArgInfo sizeItemInfo(sizeItemName.c_str(), 0); SafeSeqItem sizeItem(obj, 1); if (!pyopencv_to(sizeItem.item, dst.size, sizeItemInfo)) { @@ -825,7 +869,7 @@ bool pyopencv_to(PyObject* obj, RotatedRect& dst, const ArgInfo& info) } { const String angleItemName = format("'%s' angle", info.name); - const ArgInfo angleItemInfo(angleItemName.c_str(), false); + const ArgInfo angleItemInfo(angleItemName.c_str(), 0); SafeSeqItem angleItem(obj, 2); if (!pyopencv_to(angleItem.item, dst.angle, angleItemInfo)) { @@ -1075,7 +1119,7 @@ bool pyopencv_to(PyObject* obj, TermCriteria& dst, const ArgInfo& info) } { const String typeItemName = format("'%s' criteria type", info.name); - const ArgInfo typeItemInfo(typeItemName.c_str(), false); + const ArgInfo typeItemInfo(typeItemName.c_str(), 0); SafeSeqItem typeItem(obj, 0); if (!pyopencv_to(typeItem.item, dst.type, typeItemInfo)) { @@ -1084,7 +1128,7 @@ bool pyopencv_to(PyObject* obj, TermCriteria& dst, const ArgInfo& info) } { const String maxCountItemName = format("'%s' max count", info.name); - const ArgInfo maxCountItemInfo(maxCountItemName.c_str(), false); + const ArgInfo maxCountItemInfo(maxCountItemName.c_str(), 0); SafeSeqItem maxCountItem(obj, 1); if (!pyopencv_to(maxCountItem.item, dst.maxCount, maxCountItemInfo)) { @@ -1093,7 +1137,7 @@ bool pyopencv_to(PyObject* obj, TermCriteria& dst, const ArgInfo& info) } { const String epsilonItemName = format("'%s' epsilon", info.name); - const ArgInfo epsilonItemInfo(epsilonItemName.c_str(), false); + const ArgInfo epsilonItemInfo(epsilonItemName.c_str(), 0); SafeSeqItem epsilonItem(obj, 2); if (!pyopencv_to(epsilonItem.item, dst.epsilon, epsilonItemInfo)) { diff --git a/modules/python/src2/cv2_convert.hpp b/modules/python/src2/cv2_convert.hpp index 43ef7b2302..96a30e521f 100644 --- a/modules/python/src2/cv2_convert.hpp +++ b/modules/python/src2/cv2_convert.hpp @@ -286,13 +286,13 @@ bool pyopencv_to(PyObject *obj, std::map &map, const ArgInfo& info) while(PyDict_Next(obj, &pos, &py_key, &py_value)) { K cpp_key; - if (!pyopencv_to(py_key, cpp_key, ArgInfo("key", false))) { + if (!pyopencv_to(py_key, cpp_key, ArgInfo("key", 0))) { failmsg("Can't parse dict key. Key on position %lu has a wrong type", pos); return false; } V cpp_value; - if (!pyopencv_to(py_value, cpp_value, ArgInfo("value", false))) { + if (!pyopencv_to(py_value, cpp_value, ArgInfo("value", 0))) { failmsg("Can't parse dict value. Value on position %lu has a wrong type", pos); return false; } diff --git a/modules/python/src2/gen2.py b/modules/python/src2/gen2.py index 95bd0495cf..78f9c79b06 100755 --- a/modules/python/src2/gen2.py +++ b/modules/python/src2/gen2.py @@ -109,7 +109,7 @@ gen_template_set_prop_from_map = Template(""" if( PyMapping_HasKeyString(src, (char*)"$propname") ) { tmp = PyMapping_GetItemString(src, (char*)"$propname"); - ok = tmp && pyopencv_to_safe(tmp, dst.$propname, ArgInfo("$propname", false)); + ok = tmp && pyopencv_to_safe(tmp, dst.$propname, ArgInfo("$propname", 0)); Py_DECREF(tmp); if(!ok) return false; }""") @@ -163,7 +163,7 @@ static int pyopencv_${name}_set_${member}(pyopencv_${name}_t* p, PyObject *value PyErr_SetString(PyExc_TypeError, "Cannot delete the ${member} attribute"); return -1; } - return pyopencv_to_safe(value, p->v${access}${member}, ArgInfo("value", false)) ? 0 : -1; + return pyopencv_to_safe(value, p->v${access}${member}, ArgInfo("value", 0)) ? 0 : -1; } """) @@ -181,7 +181,7 @@ static int pyopencv_${name}_set_${member}(pyopencv_${name}_t* p, PyObject *value failmsgp("Incorrect type of object (must be '${name}' or its derivative)"); return -1; } - return pyopencv_to_safe(value, _self_${access}${member}, ArgInfo("value", false)) ? 0 : -1; + return pyopencv_to_safe(value, _self_${access}${member}, ArgInfo("value", 0)) ? 0 : -1; } """) @@ -492,6 +492,10 @@ class ArgInfo(object): def inputarg(self): return '/O' not in self._modifiers + @property + def arithm_op_src_arg(self): + return '/AOS' in self._modifiers + @property def outputarg(self): return '/O' in self._modifiers or '/IO' in self._modifiers @@ -517,7 +521,9 @@ class ArgInfo(object): "UMat", "vector_UMat"] # or self.tp.startswith("vector") def crepr(self): - return "ArgInfo(\"%s\", %d)" % (self.name, self.outputarg) + arg = 0x01 if self.outputarg else 0x0 + arg += 0x02 if self.arithm_op_src_arg else 0x0 + return "ArgInfo(\"%s\", %d)" % (self.name, arg) def find_argument_class_info(argument_type, function_namespace, diff --git a/modules/python/src2/hdr_parser.py b/modules/python/src2/hdr_parser.py index 710c792179..0dc5dd1488 100755 --- a/modules/python/src2/hdr_parser.py +++ b/modules/python/src2/hdr_parser.py @@ -535,6 +535,13 @@ class CppHeaderParser(object): funcname = self.get_dotted_name(funcname) + # see https://github.com/opencv/opencv/issues/24057 + is_arithm_op_func = funcname in {"cv.add", + "cv.subtract", + "cv.absdiff", + "cv.multiply", + "cv.divide"} + if not self.wrap_mode: decl = self.parse_func_decl_no_wrap(decl_str, static_method, docstring) decl[0] = funcname @@ -595,6 +602,8 @@ class CppHeaderParser(object): if arg_type == "InputArray": arg_type = mat + if is_arithm_op_func: + modlist.append("/AOS") # Arithm Ope Source elif arg_type == "InputOutputArray": arg_type = mat modlist.append("/IO") diff --git a/modules/python/test/test_misc.py b/modules/python/test/test_misc.py index 9f7406587c..51ece30dc6 100644 --- a/modules/python/test/test_misc.py +++ b/modules/python/test/test_misc.py @@ -42,6 +42,93 @@ def get_conversion_error_msg(value, expected, actual): def get_no_exception_msg(value): return 'Exception is not risen for {} of type {}'.format(value, type(value).__name__) + +def rpad(src, dst_size, pad_value=0): + """Extend `src` up to `dst_size` with given value. + + Args: + src (np.ndarray | tuple | list): 1d array like object to pad. + dst_size (_type_): Desired `src` size after padding. + pad_value (int, optional): Padding value. Defaults to 0. + + Returns: + np.ndarray: 1d array with len == `dst_size`. + """ + src = np.asarray(src) + if len(src.shape) != 1: + raise ValueError("Only 1d arrays are supported") + + # Considering the meaning, it is desirable to use np.pad(). + # However, the old numpy doesn't include the following fixes and cannot work as expected. + # So an alternative fix that combines np.append() and np.fill() is used. + # https://docs.scipy.org/doc/numpy-1.13.0/release.html#support-for-returning-arrays-of-arbitrary-dimensions-in-apply-along-axis + + return np.append(src, np.full( dst_size - len(src), pad_value, dtype=src.dtype) ) + +def get_ocv_arithm_op_table(apply_saturation=False): + def saturate(func): + def wrapped_func(x, y): + dst_dtype = x.dtype + if apply_saturation: + if np.issubdtype(x.dtype, np.integer): + x = x.astype(np.int64) + # Apply padding or truncation for array-like `y` inputs + if not isinstance(y, (float, int)): + if len(y) > x.shape[-1]: + y = y[:x.shape[-1]] + else: + y = rpad(y, x.shape[-1], pad_value=0) + + dst = func(x, y) + if apply_saturation: + min_val, max_val = get_limits(dst_dtype) + dst = np.clip(dst, min_val, max_val) + return dst.astype(dst_dtype) + return wrapped_func + + @saturate + def subtract(x, y): + return x - y + + @saturate + def add(x, y): + return x + y + + @saturate + def divide(x, y): + if not isinstance(y, (int, float)): + dst_dtype = np.result_type(x, y) + y = np.array(y).astype(dst_dtype) + _, max_value = get_limits(dst_dtype) + y[y == 0] = max_value + + # to compatible between python2 and python3, it calicurates with float. + # python2: int / int = int + # python3: int / int = float + dst = 1.0 * x / y + + if np.issubdtype(x.dtype, np.integer): + dst = np.rint(dst) + return dst + + @saturate + def multiply(x, y): + return x * y + + @saturate + def absdiff(x, y): + res = np.abs(x - y) + return res + + return { + cv.subtract: subtract, + cv.add: add, + cv.multiply: multiply, + cv.divide: divide, + cv.absdiff: absdiff + } + + class Bindings(NewOpenCVTests): def test_inheritance(self): @@ -816,6 +903,32 @@ class Arguments(NewOpenCVTests): np.testing.assert_equal(dst, src_copy) self.assertEqual(arguments_dump, 'lambda=25, sigma=5.5') + def test_arithm_op_without_saturation(self): + np.random.seed(4231568) + src = np.random.randint(20, 40, 8 * 4 * 3).astype(np.uint8).reshape(8, 4, 3) + operations = get_ocv_arithm_op_table(apply_saturation=False) + for ocv_op, numpy_op in operations.items(): + for val in (2, 4, (5, ), (6, 4), (2., 4., 1.), + np.uint8([1, 2, 2]), np.float64([5, 2, 6, 3]),): + dst = ocv_op(src, val) + expected = numpy_op(src, val) + # Temporarily allows a difference of 1 for arm64 workaround. + self.assertLess(np.max(np.abs(dst - expected)), 2, + msg="Operation '{}' is failed for {}".format(ocv_op.__name__, val ) ) + + def test_arithm_op_with_saturation(self): + np.random.seed(4231568) + src = np.random.randint(20, 40, 4 * 8 * 4).astype(np.uint8).reshape(4, 8, 4) + operations = get_ocv_arithm_op_table(apply_saturation=True) + + for ocv_op, numpy_op in operations.items(): + for val in (10, 4, (40, ), (15, 12), (25., 41., 15.), + np.uint8([1, 2, 20]), np.float64([50, 21, 64, 30]),): + dst = ocv_op(src, val) + expected = numpy_op(src, val) + # Temporarily allows a difference of 1 for arm64 workaround. + self.assertLess(np.max(np.abs(dst - expected)), 2, + msg="Saturated Operation '{}' is failed for {}".format(ocv_op.__name__, val ) ) class CanUsePurePythonModuleFunction(NewOpenCVTests): def test_can_get_ocv_version(self):