Merge pull request #24074 from Kumataro/fix24057

Python: support tuple src for cv::add()/subtract()/... #24074

fix https://github.com/opencv/opencv/issues/24057

### Pull Request Readiness Checklist

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
pull/24302/head
Kumataro 2 years ago committed by GitHub
parent f617fbe166
commit b870ad46bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      modules/core/include/opencv2/core.hpp
  2. 10
      modules/python/src2/cv2.hpp
  3. 62
      modules/python/src2/cv2_convert.cpp
  4. 4
      modules/python/src2/cv2_convert.hpp
  5. 14
      modules/python/src2/gen2.py
  6. 9
      modules/python/src2/hdr_parser.py
  7. 113
      modules/python/test/test_misc.py

@ -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. 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 @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. 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 src1 first input array or a scalar.
@param src2 second 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 @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. 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 @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. 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 src1 first input array or a scalar.
@param src2 second 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. @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 @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 CV_32S. You may even get result of an incorrect sign in the case of
overflow. 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 src1 first input array.
@param src2 second input array of the same size and the same type as src1. @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. @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 @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. 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 src1 first input array.
@param src2 second input array of the same size and type as src1. @param src2 second input array of the same size and type as src1.
@param scale scalar factor. @param scale scalar factor.
@ -1412,6 +1424,9 @@ The function cv::absdiff calculates:
multi-channel arrays, each channel is processed independently. multi-channel arrays, each channel is processed independently.
@note Saturation is not applied when the arrays have the depth CV_32S. @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. 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 src1 first input array or a scalar.
@param src2 second 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. @param dst output array that has the same size and type as input arrays.

@ -39,12 +39,20 @@
class ArgInfo class ArgInfo
{ {
private:
static const uint32_t arg_outputarg_flag = 0x1;
static const uint32_t arg_arithm_op_src_flag = 0x2;
public: public:
const char* name; const char* name;
bool outputarg; bool outputarg;
bool arithm_op_src;
// more fields may be added if necessary // 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: private:
ArgInfo(const ArgInfo&) = delete; ArgInfo(const ArgInfo&) = delete;

@ -63,20 +63,39 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
if( PyInt_Check(o) ) if( PyInt_Check(o) )
{ {
double v[] = {static_cast<double>(PyInt_AsLong((PyObject*)o)), 0., 0., 0.}; double v[] = {static_cast<double>(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(); m = Mat(4, 1, CV_64F, v).clone();
return true; return true;
} }
if( PyFloat_Check(o) ) if( PyFloat_Check(o) )
{ {
double v[] = {PyFloat_AsDouble((PyObject*)o), 0., 0., 0.}; 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(); m = Mat(4, 1, CV_64F, v).clone();
return true; return true;
} }
if( PyTuple_Check(o) ) if( PyTuple_Check(o) )
{ {
int i, sz = (int)PyTuple_Size((PyObject*)o); // see https://github.com/opencv/opencv/issues/24057
m = Mat(sz, 1, CV_64F); const int sz = (int)PyTuple_Size((PyObject*)o);
for( i = 0; i < sz; i++ ) 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); PyObject* oi = PyTuple_GetItem(o, i);
if( PyInt_Check(oi) ) 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<double>(i) = (double)PyInt_AsLong(oi);
else if( PyFloat_Check(oi) )
m.at<double>(i) = (double)PyFloat_AsDouble(oi);
else
{
failmsg("%s has some non-numerical elements", info.name);
m.release();
return false;
}
}
return true;
}
// handle degenerate case // handle degenerate case
// FIXIT: Don't force 1D for Scalars // FIXIT: Don't force 1D for Scalars
if( ndims == 0) { 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 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); SafeSeqItem centerItem(obj, 0);
if (!pyopencv_to(centerItem.item, dst.center, centerItemInfo)) 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 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); SafeSeqItem sizeItem(obj, 1);
if (!pyopencv_to(sizeItem.item, dst.size, sizeItemInfo)) 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 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); SafeSeqItem angleItem(obj, 2);
if (!pyopencv_to(angleItem.item, dst.angle, angleItemInfo)) 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 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); SafeSeqItem typeItem(obj, 0);
if (!pyopencv_to(typeItem.item, dst.type, typeItemInfo)) 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 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); SafeSeqItem maxCountItem(obj, 1);
if (!pyopencv_to(maxCountItem.item, dst.maxCount, maxCountItemInfo)) 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 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); SafeSeqItem epsilonItem(obj, 2);
if (!pyopencv_to(epsilonItem.item, dst.epsilon, epsilonItemInfo)) if (!pyopencv_to(epsilonItem.item, dst.epsilon, epsilonItemInfo))
{ {

@ -286,13 +286,13 @@ bool pyopencv_to(PyObject *obj, std::map<K,V> &map, const ArgInfo& info)
while(PyDict_Next(obj, &pos, &py_key, &py_value)) while(PyDict_Next(obj, &pos, &py_key, &py_value))
{ {
K cpp_key; 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); failmsg("Can't parse dict key. Key on position %lu has a wrong type", pos);
return false; return false;
} }
V cpp_value; 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); failmsg("Can't parse dict value. Value on position %lu has a wrong type", pos);
return false; return false;
} }

@ -109,7 +109,7 @@ gen_template_set_prop_from_map = Template("""
if( PyMapping_HasKeyString(src, (char*)"$propname") ) if( PyMapping_HasKeyString(src, (char*)"$propname") )
{ {
tmp = PyMapping_GetItemString(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); Py_DECREF(tmp);
if(!ok) return false; 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"); PyErr_SetString(PyExc_TypeError, "Cannot delete the ${member} attribute");
return -1; 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)"); failmsgp("Incorrect type of object (must be '${name}' or its derivative)");
return -1; 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): def inputarg(self):
return '/O' not in self._modifiers return '/O' not in self._modifiers
@property
def arithm_op_src_arg(self):
return '/AOS' in self._modifiers
@property @property
def outputarg(self): def outputarg(self):
return '/O' in self._modifiers or '/IO' in self._modifiers 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") "UMat", "vector_UMat"] # or self.tp.startswith("vector")
def crepr(self): 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, def find_argument_class_info(argument_type, function_namespace,

@ -535,6 +535,13 @@ class CppHeaderParser(object):
funcname = self.get_dotted_name(funcname) 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: if not self.wrap_mode:
decl = self.parse_func_decl_no_wrap(decl_str, static_method, docstring) decl = self.parse_func_decl_no_wrap(decl_str, static_method, docstring)
decl[0] = funcname decl[0] = funcname
@ -595,6 +602,8 @@ class CppHeaderParser(object):
if arg_type == "InputArray": if arg_type == "InputArray":
arg_type = mat arg_type = mat
if is_arithm_op_func:
modlist.append("/AOS") # Arithm Ope Source
elif arg_type == "InputOutputArray": elif arg_type == "InputOutputArray":
arg_type = mat arg_type = mat
modlist.append("/IO") modlist.append("/IO")

@ -42,6 +42,93 @@ def get_conversion_error_msg(value, expected, actual):
def get_no_exception_msg(value): def get_no_exception_msg(value):
return 'Exception is not risen for {} of type {}'.format(value, type(value).__name__) 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): class Bindings(NewOpenCVTests):
def test_inheritance(self): def test_inheritance(self):
@ -816,6 +903,32 @@ class Arguments(NewOpenCVTests):
np.testing.assert_equal(dst, src_copy) np.testing.assert_equal(dst, src_copy)
self.assertEqual(arguments_dump, 'lambda=25, sigma=5.5') 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): class CanUsePurePythonModuleFunction(NewOpenCVTests):
def test_can_get_ocv_version(self): def test_can_get_ocv_version(self):

Loading…
Cancel
Save