mirror of
https://github.com/opencv/opencv.git
synced 2025-06-07 17:44:04 +08:00
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
This commit is contained in:
parent
f617fbe166
commit
b870ad46bf
@ -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…
Reference in New Issue
Block a user