Commit 31289d2f authored by Vadim Levin's avatar Vadim Levin Committed by Alexander Alekhin

Merge pull request #15915 from VadimLevin:dev/norm_fix

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
parent 4cc458eb
This diff is collapsed.
......@@ -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]))
......
......@@ -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]),
......
#!/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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment