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

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

@ -63,20 +63,39 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
if( PyInt_Check(o) )
{
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();
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<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
// 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))
{

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

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

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

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

Loading…
Cancel
Save