Merge pull request #15915 from VadimLevin:dev/norm_fix
authorVadim Levin <vadim.levin@xperience.ai>
Mon, 13 Jan 2020 15:11:34 +0000 (18:11 +0300)
committerAlexander Alekhin <alexander.a.alekhin@gmail.com>
Mon, 13 Jan 2020 15:11:34 +0000 (18:11 +0300)
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
modules/python/src2/gen2.py
modules/python/test/test_misc.py
modules/python/test/test_norm.py [new file with mode: 0644]

index d493f1f..0b516a4 100644 (file)
 #   define Py_LIMITED_API 0x03030000
 #endif
 
-#include <math.h>
+#include <cmath>
 #include <Python.h>
+#include <limits>
 
 #if PY_MAJOR_VERSION < 3
 #undef CVPY_DYNAMIC_INIT
+#else
+#define CV_PYTHON_3 1
 #endif
 
 #if defined(_MSC_VER) && (_MSC_VER > 1800)
 #include "pycompat.hpp"
 #include <map>
 
+#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<class T>
+NPY_TYPES asNumpyType()
+{
+    return NPY_OBJECT;
+}
+
+template<>
+NPY_TYPES asNumpyType<bool>()
+{
+    return NPY_BOOL;
+}
+
+#define CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(src, dst) \
+    template<>                                             \
+    NPY_TYPES asNumpyType<src>()                           \
+    {                                                      \
+        return NPY_##dst;                                  \
+    }                                                      \
+    template<>                                             \
+    NPY_TYPES asNumpyType<u##src>()                        \
+    {                                                      \
+        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<float>()
+{
+    return NPY_FLOAT;
+}
+
+template<>
+NPY_TYPES asNumpyType<double>()
+{
+    return NPY_DOUBLE;
+}
+
+template <class T>
+PyArray_Descr* getNumpyTypeDescriptor()
+{
+    return PyArray_DescrFromType(asNumpyType<T>());
+}
+
+template <>
+PyArray_Descr* getNumpyTypeDescriptor<size_t>()
+{
+#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 <class T, class U>
+bool isRepresentable(U value) {
+    return (std::numeric_limits<T>::min() <= value) && (value <= std::numeric_limits<T>::max());
+}
+
+template<class T>
+bool canBeSafelyCasted(PyObject* obj, PyArray_Descr* to)
+{
+    return PyArray_CanCastTo(PyArray_DescrFromScalar(obj), to) != 0;
+}
+
+
+template<>
+bool canBeSafelyCasted<size_t>(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<int64_t>());
+            return (input >= 0) && isRepresentable<size_t>(static_cast<uint64_t>(input));
+        }
+        else
+        {
+            uint64_t input = 0;
+            PyArray_CastScalarToCtype(obj, &input, getNumpyTypeDescriptor<uint64_t>());
+            return isRepresentable<size_t>(input);
+        }
+        return false;
+    }
+}
+
+
+template<class T>
+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<T>();
+        if (canBeSafelyCasted<T>(obj, to))
+        {
+            PyArray_CastScalarToCtype(obj, &value, to);
+            return true;
+        }
+    }
+    return false;
+}
+
+} // namespace
+
 typedef std::vector<uchar> vector_uchar;
 typedef std::vector<char> vector_char;
 typedef std::vector<int> 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<size_t>(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<double>(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<float>(res);
+        }
+        else
+        {
+            double res = PyFloat_AsDouble(obj);
+            value = static_cast<float>(res);
+        }
+    }
+    else if (PyArray_CheckScalar(obj))
+    {
+       const bool isParsed = parseNumpyScalar<float>(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 =
index cd1b8f6..d3c8ec3 100755 (executable)
@@ -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]))
 
index 0918986..b25ef7e 100644 (file)
@@ -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 (file)
index 0000000..404f19f
--- /dev/null
@@ -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()