From 31289d2f32ab1941c0f2de0ecfb741e95637db89 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Mon, 13 Jan 2020 18:11:34 +0300 Subject: [PATCH] Merge pull request #15915 from VadimLevin:dev/norm_fix MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Fix implicit conversion from array to scalar in python bindings * Fix wrong conversion behavior for primitive types - Introduce ArgTypeInfo namedtuple instead of plain tuple. If strict conversion parameter for type is set to true, it is handled like object argument in PyArg_ParseTupleAndKeywords and converted to concrete type with the appropriate pyopencv_to function call. - Remove deadcode and unused variables. - Fix implicit conversion from numpy array with 1 element to scalar - Fix narrowing conversion to size_t type. * Fix wrong conversion behavior for primitive types - Introduce ArgTypeInfo namedtuple instead of plain tuple. If strict conversion parameter for type is set to true, it is handled like object argument in PyArg_ParseTupleAndKeywords and converted to concrete type with the appropriate pyopencv_to function call. - Remove deadcode and unused variables. - Fix implicit conversion from numpy array with 1 element to scalar - Fix narrowing conversion to size_t type.· - Enable tests with wrong conversion behavior - Restrict passing None as value - Restrict bool to integer/floating types conversion * Add PyIntType support for Python 2 * Remove possible narrowing conversion of size_t * Bindings conversion update - Remove unused macro - Add better conversion for types to numpy types descriptors - Add argument name to fail messages - NoneType treated as a valid argument. Better handling will be added as a standalone patch * Add descriptor specialization for size_t * Add check for signed to unsigned integer conversion safety - If signed integer is positive it can be safely converted to unsigned - Add check for plain python 2 objects - Add check for numpy scalars - Add simple type_traits implementation for better code style * Resolve type "overflow" false negative in safe casting check - Move type_traits to separate header * Add copyright message to type_traits.hpp * Limit conversion scope for integral numpy types - Made canBeSafelyCasted specialized only for size_t, so type_traits header became unused and was removed. - Added clarification about descriptor pointer --- modules/python/src2/cv2.cpp | 328 +++++++++++++++++++++++++++++++++++---- modules/python/src2/gen2.py | 77 ++++++--- modules/python/test/test_misc.py | 25 ++- modules/python/test/test_norm.py | 173 +++++++++++++++++++++ 4 files changed, 531 insertions(+), 72 deletions(-) create mode 100644 modules/python/test/test_norm.py diff --git a/modules/python/src2/cv2.cpp b/modules/python/src2/cv2.cpp index d493f1f..0b516a4 100644 --- a/modules/python/src2/cv2.cpp +++ b/modules/python/src2/cv2.cpp @@ -13,11 +13,14 @@ # define Py_LIMITED_API 0x03030000 #endif -#include +#include #include +#include #if PY_MAJOR_VERSION < 3 #undef CVPY_DYNAMIC_INIT +#else +#define CV_PYTHON_3 1 #endif #if defined(_MSC_VER) && (_MSC_VER > 1800) @@ -37,16 +40,17 @@ #include "pycompat.hpp" #include +#define CV_HAS_CONVERSION_ERROR(x) (((x) == -1) && PyErr_Occurred()) + + class ArgInfo { public: - const char * name; + const char* name; bool outputarg; // more fields may be added if necessary - ArgInfo(const char * name_, bool outputarg_) - : name(name_) - , outputarg(outputarg_) {} + ArgInfo(const char* name_, bool outputarg_) : name(name_), outputarg(outputarg_) {} private: ArgInfo(const ArgInfo&); // = delete @@ -159,6 +163,135 @@ catch (const cv::Exception &e) \ using namespace cv; + +namespace { +template +NPY_TYPES asNumpyType() +{ + return NPY_OBJECT; +} + +template<> +NPY_TYPES asNumpyType() +{ + return NPY_BOOL; +} + +#define CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(src, dst) \ + template<> \ + NPY_TYPES asNumpyType() \ + { \ + return NPY_##dst; \ + } \ + template<> \ + NPY_TYPES asNumpyType() \ + { \ + return NPY_U##dst; \ + } + +CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int8_t, INT8); + +CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int16_t, INT16); + +CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int32_t, INT32); + +CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int64_t, INT64); + +#undef CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION + +template<> +NPY_TYPES asNumpyType() +{ + return NPY_FLOAT; +} + +template<> +NPY_TYPES asNumpyType() +{ + return NPY_DOUBLE; +} + +template +PyArray_Descr* getNumpyTypeDescriptor() +{ + return PyArray_DescrFromType(asNumpyType()); +} + +template <> +PyArray_Descr* getNumpyTypeDescriptor() +{ +#if SIZE_MAX == ULONG_MAX + return PyArray_DescrFromType(NPY_ULONG); +#elif SIZE_MAX == ULLONG_MAX + return PyArray_DescrFromType(NPY_ULONGLONG); +#else + return PyArray_DescrFromType(NPY_UINT); +#endif +} + +template +bool isRepresentable(U value) { + return (std::numeric_limits::min() <= value) && (value <= std::numeric_limits::max()); +} + +template +bool canBeSafelyCasted(PyObject* obj, PyArray_Descr* to) +{ + return PyArray_CanCastTo(PyArray_DescrFromScalar(obj), to) != 0; +} + + +template<> +bool canBeSafelyCasted(PyObject* obj, PyArray_Descr* to) +{ + PyArray_Descr* from = PyArray_DescrFromScalar(obj); + if (PyArray_CanCastTo(from, to)) + { + return true; + } + else + { + // False negative scenarios: + // - Signed input is positive so it can be safely cast to unsigned output + // - Input has wider limits but value is representable within output limits + // - All the above + if (PyDataType_ISSIGNED(from)) + { + int64_t input = 0; + PyArray_CastScalarToCtype(obj, &input, getNumpyTypeDescriptor()); + return (input >= 0) && isRepresentable(static_cast(input)); + } + else + { + uint64_t input = 0; + PyArray_CastScalarToCtype(obj, &input, getNumpyTypeDescriptor()); + return isRepresentable(input); + } + return false; + } +} + + +template +bool parseNumpyScalar(PyObject* obj, T& value) +{ + if (PyArray_CheckScalar(obj)) + { + // According to the numpy documentation: + // There are 21 statically-defined PyArray_Descr objects for the built-in data-types + // So descriptor pointer is not owning. + PyArray_Descr* to = getNumpyTypeDescriptor(); + if (canBeSafelyCasted(obj, to)) + { + PyArray_CastScalarToCtype(obj, &value, to); + return true; + } + } + return false; +} + +} // namespace + typedef std::vector vector_uchar; typedef std::vector vector_char; typedef std::vector vector_int; @@ -268,6 +401,11 @@ NumpyAllocator g_numpyAllocator; enum { ARG_NONE = 0, ARG_MAT = 1, ARG_SCALAR = 2 }; +static bool isBool(PyObject* obj) CV_NOEXCEPT +{ + return PyArray_IsScalar(obj, Bool) || PyBool_Check(obj); +} + // special case, when the converter needs full ArgInfo structure static bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info) { @@ -578,14 +716,22 @@ PyObject* pyopencv_from(const bool& value) template<> bool pyopencv_to(PyObject* obj, bool& value, const ArgInfo& info) { - CV_UNUSED(info); - if(!obj || obj == Py_None) + if (!obj || obj == Py_None) + { return true; - int _val = PyObject_IsTrue(obj); - if(_val < 0) - return false; - value = _val > 0; - return true; + } + if (isBool(obj) || PyArray_IsIntegerScalar(obj)) + { + npy_bool npy_value = NPY_FALSE; + const int ret_code = PyArray_BoolConverter(obj, &npy_value); + if (ret_code >= 0) + { + value = (npy_value == NPY_TRUE); + return true; + } + } + failmsg("Argument '%s' is not convertable to bool", info.name); + return false; } template<> @@ -597,11 +743,62 @@ PyObject* pyopencv_from(const size_t& value) template<> bool pyopencv_to(PyObject* obj, size_t& value, const ArgInfo& info) { - CV_UNUSED(info); - if(!obj || obj == Py_None) + if (!obj || obj == Py_None) + { return true; - value = (int)PyLong_AsUnsignedLong(obj); - return value != (size_t)-1 || !PyErr_Occurred(); + } + if (isBool(obj)) + { + failmsg("Argument '%s' must be integer type, not bool", info.name); + return false; + } + if (PyArray_IsIntegerScalar(obj)) + { + if (PyLong_Check(obj)) + { +#if defined(CV_PYTHON_3) + value = PyLong_AsSize_t(obj); +#else + #if ULONG_MAX == SIZE_MAX + value = PyLong_AsUnsignedLong(obj); + #else + value = PyLong_AsUnsignedLongLong(obj); + #endif +#endif + } +#if !defined(CV_PYTHON_3) + // Python 2.x has PyIntObject which is not a subtype of PyLongObject + // Overflow check here is unnecessary because object will be converted to long on the + // interpreter side + else if (PyInt_Check(obj)) + { + const long res = PyInt_AsLong(obj); + if (res < 0) { + failmsg("Argument '%s' can not be safely parsed to 'size_t'", info.name); + return false; + } + #if ULONG_MAX == SIZE_MAX + value = PyInt_AsUnsignedLongMask(obj); + #else + value = PyInt_AsUnsignedLongLongMask(obj); + #endif + } +#endif + else + { + const bool isParsed = parseNumpyScalar(obj, value); + if (!isParsed) { + failmsg("Argument '%s' can not be safely parsed to 'size_t'", info.name); + return false; + } + } + } + else + { + failmsg("Argument '%s' is required to be an integer", info.name); + return false; + } + return !PyErr_Occurred(); } template<> @@ -613,16 +810,25 @@ PyObject* pyopencv_from(const int& value) template<> bool pyopencv_to(PyObject* obj, int& value, const ArgInfo& info) { - CV_UNUSED(info); - if(!obj || obj == Py_None) + if (!obj || obj == Py_None) + { return true; - if(PyInt_Check(obj)) - value = (int)PyInt_AsLong(obj); - else if(PyLong_Check(obj)) - value = (int)PyLong_AsLong(obj); + } + if (isBool(obj)) + { + failmsg("Argument '%s' must be integer, not bool", info.name); + return false; + } + if (PyArray_IsIntegerScalar(obj)) + { + value = PyArray_PyIntAsInt(obj); + } else + { + failmsg("Argument '%s' is required to be an integer", info.name); return false; - return value != -1 || !PyErr_Occurred(); + } + return !CV_HAS_CONVERSION_ERROR(value); } template<> @@ -651,13 +857,39 @@ PyObject* pyopencv_from(const double& value) template<> bool pyopencv_to(PyObject* obj, double& value, const ArgInfo& info) { - CV_UNUSED(info); - if(!obj || obj == Py_None) + if (!obj || obj == Py_None) + { return true; - if(!!PyInt_CheckExact(obj)) - value = (double)PyInt_AS_LONG(obj); + } + if (isBool(obj)) + { + failmsg("Argument '%s' must be double, not bool", info.name); + return false; + } + if (PyArray_IsPythonNumber(obj)) + { + if (PyLong_Check(obj)) + { + value = PyLong_AsDouble(obj); + } + else + { + value = PyFloat_AsDouble(obj); + } + } + else if (PyArray_CheckScalar(obj)) + { + const bool isParsed = parseNumpyScalar(obj, value); + if (!isParsed) { + failmsg("Argument '%s' can not be safely parsed to 'double'", info.name); + return false; + } + } else - value = PyFloat_AsDouble(obj); + { + failmsg("Argument '%s' can not be treated as a double", info.name); + return false; + } return !PyErr_Occurred(); } @@ -670,13 +902,41 @@ PyObject* pyopencv_from(const float& value) template<> bool pyopencv_to(PyObject* obj, float& value, const ArgInfo& info) { - CV_UNUSED(info); - if(!obj || obj == Py_None) + if (!obj || obj == Py_None) + { return true; - if(!!PyInt_CheckExact(obj)) - value = (float)PyInt_AS_LONG(obj); + } + if (isBool(obj)) + { + failmsg("Argument '%s' must be float, not bool", info.name); + return false; + } + if (PyArray_IsPythonNumber(obj)) + { + if (PyLong_Check(obj)) + { + double res = PyLong_AsDouble(obj); + value = static_cast(res); + } + else + { + double res = PyFloat_AsDouble(obj); + value = static_cast(res); + } + } + else if (PyArray_CheckScalar(obj)) + { + const bool isParsed = parseNumpyScalar(obj, value); + if (!isParsed) { + failmsg("Argument '%s' can not be safely parsed to 'float'", info.name); + return false; + } + } else - value = (float)PyFloat_AsDouble(obj); + { + failmsg("Argument '%s' can't be treated as a float", info.name); + return false; + } return !PyErr_Occurred(); } @@ -1742,7 +2002,7 @@ static bool init_body(PyObject * m) #pragma GCC visibility push(default) #endif -#if PY_MAJOR_VERSION >= 3 +#if defined(CV_PYTHON_3) // === Python 3 static struct PyModuleDef cv2_moduledef = diff --git a/modules/python/src2/gen2.py b/modules/python/src2/gen2.py index cd1b8f6..d3c8ec3 100755 --- a/modules/python/src2/gen2.py +++ b/modules/python/src2/gen2.py @@ -4,12 +4,14 @@ from __future__ import print_function import hdr_parser, sys, re, os from string import Template from pprint import pprint +from collections import namedtuple if sys.version_info[0] >= 3: from io import StringIO else: from cStringIO import StringIO + forbidden_arg_types = ["void*"] ignored_arg_types = ["RNG*"] @@ -172,18 +174,48 @@ gen_template_prop_init = Template(""" gen_template_rw_prop_init = Template(""" {(char*)"${member}", (getter)pyopencv_${name}_get_${member}, (setter)pyopencv_${name}_set_${member}, (char*)"${member}", NULL},""") +class FormatStrings: + string = 's' + unsigned_char = 'b' + short_int = 'h' + int = 'i' + unsigned_int = 'I' + long = 'l' + unsigned_long = 'k' + long_long = 'L' + unsigned_long_long = 'K' + size_t = 'n' + float = 'f' + double = 'd' + object = 'O' + +ArgTypeInfo = namedtuple('ArgTypeInfo', + ['atype', 'format_str', 'default_value', + 'strict_conversion']) +# strict_conversion is False by default +ArgTypeInfo.__new__.__defaults__ = (False,) + simple_argtype_mapping = { - "bool": ("bool", "b", "0"), - "size_t": ("size_t", "I", "0"), - "int": ("int", "i", "0"), - "float": ("float", "f", "0.f"), - "double": ("double", "d", "0"), - "c_string": ("char*", "s", '(char*)""') + "bool": ArgTypeInfo("bool", FormatStrings.unsigned_char, "0", True), + "size_t": ArgTypeInfo("size_t", FormatStrings.unsigned_long_long, "0", True), + "int": ArgTypeInfo("int", FormatStrings.int, "0", True), + "float": ArgTypeInfo("float", FormatStrings.float, "0.f", True), + "double": ArgTypeInfo("double", FormatStrings.double, "0", True), + "c_string": ArgTypeInfo("char*", FormatStrings.string, '(char*)""') } + def normalize_class_name(name): return re.sub(r"^cv\.", "", name).replace(".", "_") + +def get_type_format_string(arg_type_info): + if arg_type_info.strict_conversion: + return FormatStrings.object + else: + return arg_type_info.format_str + + class ClassProp(object): def __init__(self, decl): self.tp = decl[0].replace("*", "_ptr") @@ -576,7 +608,7 @@ class FuncInfo(object): fullname = selfinfo.wname + "." + fullname all_code_variants = [] - declno = -1 + for v in self.variants: code_decl = "" code_ret = "" @@ -584,7 +616,6 @@ class FuncInfo(object): code_args = "(" all_cargs = [] - parse_arglist = [] if v.isphantom and ismethod and not self.is_static: code_args += "_self_" @@ -617,22 +648,22 @@ class FuncInfo(object): if any(tp in codegen.enums.keys() for tp in tp_candidates): defval0 = "static_cast<%s>(%d)" % (a.tp, 0) - amapping = simple_argtype_mapping.get(tp, (tp, "O", defval0)) + arg_type_info = simple_argtype_mapping.get(tp, ArgTypeInfo(tp, FormatStrings.object, defval0, True)) parse_name = a.name if a.py_inputarg: - if amapping[1] == "O": + if arg_type_info.strict_conversion: code_decl += " PyObject* pyobj_%s = NULL;\n" % (a.name,) parse_name = "pyobj_" + a.name if a.tp == 'char': - code_cvt_list.append("convert_to_char(pyobj_%s, &%s, %s)"% (a.name, a.name, a.crepr())) + code_cvt_list.append("convert_to_char(pyobj_%s, &%s, %s)" % (a.name, a.name, a.crepr())) else: code_cvt_list.append("pyopencv_to(pyobj_%s, %s, %s)" % (a.name, a.name, a.crepr())) - all_cargs.append([amapping, parse_name]) + all_cargs.append([arg_type_info, parse_name]) defval = a.defval if not defval: - defval = amapping[2] + defval = arg_type_info.default_value else: if "UMat" in tp: if "Mat" in defval and "UMat" not in defval: @@ -641,14 +672,14 @@ class FuncInfo(object): if "Mat" in defval and "GpuMat" not in defval: defval = defval.replace("Mat", "cuda::GpuMat") # "tp arg = tp();" is equivalent to "tp arg;" in the case of complex types - if defval == tp + "()" and amapping[1] == "O": + if defval == tp + "()" and arg_type_info.format_str == FormatStrings.object: defval = "" if a.outputarg and not a.inputarg: defval = "" if defval: - code_decl += " %s %s=%s;\n" % (amapping[0], a.name, defval) + code_decl += " %s %s=%s;\n" % (arg_type_info.atype, a.name, defval) else: - code_decl += " %s %s;\n" % (amapping[0], a.name) + code_decl += " %s %s;\n" % (arg_type_info.atype, a.name) if not code_args.endswith("("): code_args += ", " @@ -690,12 +721,16 @@ class FuncInfo(object): if v.rettype: tp = v.rettype tp1 = tp.replace("*", "_ptr") - amapping = simple_argtype_mapping.get(tp, (tp, "O", "0")) - all_cargs.append(amapping) + default_info = ArgTypeInfo(tp, FormatStrings.object, "0") + arg_type_info = simple_argtype_mapping.get(tp, default_info) + all_cargs.append(arg_type_info) if v.args and v.py_arglist: # form the format spec for PyArg_ParseTupleAndKeywords - fmtspec = "".join([all_cargs[argno][0][1] for aname, argno in v.py_arglist]) + fmtspec = "".join([ + get_type_format_string(all_cargs[argno][0]) + for aname, argno in v.py_arglist + ]) if v.py_noptargs > 0: fmtspec = fmtspec[:-v.py_noptargs] + "|" + fmtspec[-v.py_noptargs:] fmtspec += ":" + fullname @@ -723,10 +758,6 @@ class FuncInfo(object): else: # there is more than 1 return parameter; form the tuple out of them fmtspec = "N"*len(v.py_outlist) - backcvt_arg_list = [] - for aname, argno in v.py_outlist: - amapping = all_cargs[argno][0] - backcvt_arg_list.append("%s(%s)" % (amapping[2], aname)) code_ret = "return Py_BuildValue(\"(%s)\", %s)" % \ (fmtspec, ", ".join(["pyopencv_from(" + aname + ")" for aname, argno in v.py_outlist])) diff --git a/modules/python/test/test_misc.py b/modules/python/test/test_misc.py index 0918986..b25ef7e 100644 --- a/modules/python/test/test_misc.py +++ b/modules/python/test/test_misc.py @@ -136,13 +136,12 @@ class Arguments(NewOpenCVTests): msg=get_conversion_error_msg(convertible_false, 'bool: false', actual)) def test_parse_to_bool_not_convertible(self): - for not_convertible in (1.2, np.float(2.3), 's', 'str', (1, 2), [1, 2], complex(1, 1), None, + for not_convertible in (1.2, np.float(2.3), 's', 'str', (1, 2), [1, 2], complex(1, 1), complex(imag=2), complex(1.1), np.array([1, 0], dtype=np.bool)): with self.assertRaises((TypeError, OverflowError), msg=get_no_exception_msg(not_convertible)): _ = cv.utils.dumpBool(not_convertible) - @unittest.skip('Wrong conversion behavior') def test_parse_to_bool_convertible_extra(self): try_to_convert = partial(self._try_to_convert, cv.utils.dumpBool) _, max_size_t = get_limits(ctypes.c_size_t) @@ -151,7 +150,6 @@ class Arguments(NewOpenCVTests): self.assertEqual('bool: true', actual, msg=get_conversion_error_msg(convertible_true, 'bool: true', actual)) - @unittest.skip('Wrong conversion behavior') def test_parse_to_bool_not_convertible_extra(self): for not_convertible in (np.array([False]), np.array([True], dtype=np.bool)): with self.assertRaises((TypeError, OverflowError), @@ -172,12 +170,11 @@ class Arguments(NewOpenCVTests): min_int, max_int = get_limits(ctypes.c_int) for not_convertible in (1.2, np.float(4), float(3), np.double(45), 's', 'str', np.array([1, 2]), (1,), [1, 2], min_int - 1, max_int + 1, - complex(1, 1), complex(imag=2), complex(1.1), None): + complex(1, 1), complex(imag=2), complex(1.1)): with self.assertRaises((TypeError, OverflowError, ValueError), msg=get_no_exception_msg(not_convertible)): _ = cv.utils.dumpInt(not_convertible) - @unittest.skip('Wrong conversion behavior') def test_parse_to_int_not_convertible_extra(self): for not_convertible in (np.bool_(True), True, False, np.float32(2.3), np.array([3, ], dtype=int), np.array([-2, ], dtype=np.int32), @@ -189,7 +186,7 @@ class Arguments(NewOpenCVTests): def test_parse_to_size_t_convertible(self): try_to_convert = partial(self._try_to_convert, cv.utils.dumpSizeT) _, max_uint = get_limits(ctypes.c_uint) - for convertible in (2, True, False, max_uint, (12), np.uint8(34), np.int8(12), np.int16(23), + for convertible in (2, max_uint, (12), np.uint8(34), np.int8(12), np.int16(23), np.int32(123), np.int64(344), np.uint64(3), np.uint16(2), np.uint32(5), np.uint(44)): expected = 'size_t: {0:d}'.format(convertible).lower() @@ -198,14 +195,15 @@ class Arguments(NewOpenCVTests): msg=get_conversion_error_msg(convertible, expected, actual)) def test_parse_to_size_t_not_convertible(self): - for not_convertible in (1.2, np.float(4), float(3), np.double(45), 's', 'str', - np.array([1, 2]), (1,), [1, 2], np.float64(6), complex(1, 1), - complex(imag=2), complex(1.1), None): + min_long, _ = get_limits(ctypes.c_long) + for not_convertible in (1.2, True, False, np.bool_(True), np.float(4), float(3), + np.double(45), 's', 'str', np.array([1, 2]), (1,), [1, 2], + np.float64(6), complex(1, 1), complex(imag=2), complex(1.1), + -1, min_long, np.int8(-35)): with self.assertRaises((TypeError, OverflowError), msg=get_no_exception_msg(not_convertible)): _ = cv.utils.dumpSizeT(not_convertible) - @unittest.skip('Wrong conversion behavior') def test_parse_to_size_t_convertible_extra(self): try_to_convert = partial(self._try_to_convert, cv.utils.dumpSizeT) _, max_size_t = get_limits(ctypes.c_size_t) @@ -215,7 +213,6 @@ class Arguments(NewOpenCVTests): self.assertEqual(expected, actual, msg=get_conversion_error_msg(convertible, expected, actual)) - @unittest.skip('Wrong conversion behavior') def test_parse_to_size_t_not_convertible_extra(self): for not_convertible in (np.bool_(True), True, False, np.array([123, ], dtype=np.uint8),): with self.assertRaises((TypeError, OverflowError), @@ -251,13 +248,12 @@ class Arguments(NewOpenCVTests): msg=get_conversion_error_msg(inf, expected, actual)) def test_parse_to_float_not_convertible(self): - for not_convertible in ('s', 'str', (12,), [1, 2], None, np.array([1, 2], dtype=np.float), + for not_convertible in ('s', 'str', (12,), [1, 2], np.array([1, 2], dtype=np.float), np.array([1, 2], dtype=np.double), complex(1, 1), complex(imag=2), complex(1.1)): with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)): _ = cv.utils.dumpFloat(not_convertible) - @unittest.skip('Wrong conversion behavior') def test_parse_to_float_not_convertible_extra(self): for not_convertible in (np.bool_(False), True, False, np.array([123, ], dtype=int), np.array([1., ]), np.array([False]), @@ -289,13 +285,12 @@ class Arguments(NewOpenCVTests): "Actual: {}".format(type(nan).__name__, actual)) def test_parse_to_double_not_convertible(self): - for not_convertible in ('s', 'str', (12,), [1, 2], None, np.array([1, 2], dtype=np.float), + for not_convertible in ('s', 'str', (12,), [1, 2], np.array([1, 2], dtype=np.float), np.array([1, 2], dtype=np.double), complex(1, 1), complex(imag=2), complex(1.1)): with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)): _ = cv.utils.dumpDouble(not_convertible) - @unittest.skip('Wrong conversion behavior') def test_parse_to_double_not_convertible_extra(self): for not_convertible in (np.bool_(False), True, False, np.array([123, ], dtype=int), np.array([1., ]), np.array([False]), diff --git a/modules/python/test/test_norm.py b/modules/python/test/test_norm.py new file mode 100644 index 0000000..404f19f --- /dev/null +++ b/modules/python/test/test_norm.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python + +from itertools import product +from functools import reduce + +import numpy as np +import cv2 as cv + +from tests_common import NewOpenCVTests + + +def norm_inf(x, y=None): + def norm(vec): + return np.linalg.norm(vec.flatten(), np.inf) + + x = x.astype(np.float64) + return norm(x) if y is None else norm(x - y.astype(np.float64)) + + +def norm_l1(x, y=None): + def norm(vec): + return np.linalg.norm(vec.flatten(), 1) + + x = x.astype(np.float64) + return norm(x) if y is None else norm(x - y.astype(np.float64)) + + +def norm_l2(x, y=None): + def norm(vec): + return np.linalg.norm(vec.flatten()) + + x = x.astype(np.float64) + return norm(x) if y is None else norm(x - y.astype(np.float64)) + + +def norm_l2sqr(x, y=None): + def norm(vec): + return np.square(vec).sum() + + x = x.astype(np.float64) + return norm(x) if y is None else norm(x - y.astype(np.float64)) + + +def norm_hamming(x, y=None): + def norm(vec): + return sum(bin(i).count('1') for i in vec.flatten()) + + return norm(x) if y is None else norm(np.bitwise_xor(x, y)) + + +def norm_hamming2(x, y=None): + def norm(vec): + def element_norm(element): + binary_str = bin(element).split('b')[-1] + if len(binary_str) % 2 == 1: + binary_str = '0' + binary_str + gen = filter(lambda p: p != '00', + (binary_str[i:i+2] + for i in range(0, len(binary_str), 2))) + return sum(1 for _ in gen) + + return sum(element_norm(element) for element in vec.flatten()) + + return norm(x) if y is None else norm(np.bitwise_xor(x, y)) + + +norm_type_under_test = { + cv.NORM_INF: norm_inf, + cv.NORM_L1: norm_l1, + cv.NORM_L2: norm_l2, + cv.NORM_L2SQR: norm_l2sqr, + cv.NORM_HAMMING: norm_hamming, + cv.NORM_HAMMING2: norm_hamming2 +} + +norm_name = { + cv.NORM_INF: 'inf', + cv.NORM_L1: 'L1', + cv.NORM_L2: 'L2', + cv.NORM_L2SQR: 'L2SQR', + cv.NORM_HAMMING: 'Hamming', + cv.NORM_HAMMING2: 'Hamming2' +} + + +def get_element_types(norm_type): + if norm_type in (cv.NORM_HAMMING, cv.NORM_HAMMING2): + return (np.uint8,) + else: + return (np.uint8, np.int8, np.uint16, np.int16, np.int32, np.float32, + np.float64) + + +def generate_vector(shape, dtype): + if np.issubdtype(dtype, np.integer): + return np.random.randint(0, 100, shape).astype(dtype) + else: + return np.random.normal(10., 12.5, shape).astype(dtype) + + +shapes = (1, 2, 3, 5, 7, 16, (1, 1), (2, 2), (3, 5), (1, 7)) + + +class norm_test(NewOpenCVTests): + + def test_norm_for_one_array(self): + np.random.seed(123) + for norm_type, norm in norm_type_under_test.items(): + element_types = get_element_types(norm_type) + for shape, element_type in product(shapes, element_types): + array = generate_vector(shape, element_type) + expected = norm(array) + actual = cv.norm(array, norm_type) + self.assertAlmostEqual( + expected, actual, places=2, + msg='Array {0} of {1} and norm {2}'.format( + array, element_type.__name__, norm_name[norm_type] + ) + ) + + def test_norm_for_two_arrays(self): + np.random.seed(456) + for norm_type, norm in norm_type_under_test.items(): + element_types = get_element_types(norm_type) + for shape, element_type in product(shapes, element_types): + first = generate_vector(shape, element_type) + second = generate_vector(shape, element_type) + expected = norm(first, second) + actual = cv.norm(first, second, norm_type) + self.assertAlmostEqual( + expected, actual, places=2, + msg='Arrays {0} {1} of type {2} and norm {3}'.format( + first, second, element_type.__name__, + norm_name[norm_type] + ) + ) + + def test_norm_fails_for_wrong_type(self): + for norm_type in (cv.NORM_HAMMING, cv.NORM_HAMMING2): + with self.assertRaises(Exception, + msg='Type is not checked {0}'.format( + norm_name[norm_type] + )): + cv.norm(np.array([1, 2], dtype=np.int32), norm_type) + + def test_norm_fails_for_array_and_scalar(self): + for norm_type in norm_type_under_test: + with self.assertRaises(Exception, + msg='Exception is not thrown for {0}'.format( + norm_name[norm_type] + )): + cv.norm(np.array([1, 2], dtype=np.uint8), 123, norm_type) + + def test_norm_fails_for_scalar_and_array(self): + for norm_type in norm_type_under_test: + with self.assertRaises(Exception, + msg='Exception is not thrown for {0}'.format( + norm_name[norm_type] + )): + cv.norm(4, np.array([1, 2], dtype=np.uint8), norm_type) + + def test_norm_fails_for_array_and_norm_type_as_scalar(self): + for norm_type in norm_type_under_test: + with self.assertRaises(Exception, + msg='Exception is not thrown for {0}'.format( + norm_name[norm_type] + )): + cv.norm(np.array([3, 4, 5], dtype=np.uint8), + norm_type, normType=norm_type) + + +if __name__ == '__main__': + NewOpenCVTests.bootstrap() -- 2.7.4