Merge pull request #23224 from VadimLevin:dev/vlevin/cxx-named-arguments

This commit is contained in:
Alexander Alekhin 2023-02-08 17:31:30 +00:00
commit 44290af516
5 changed files with 250 additions and 60 deletions

View File

@ -243,6 +243,33 @@ struct CV_EXPORTS_W_SIMPLE ClassWithKeywordProperties {
} }
}; };
struct CV_EXPORTS_W_PARAMS FunctionParams
{
CV_PROP_RW int lambda = -1;
CV_PROP_RW float sigma = 0.0f;
FunctionParams& setLambda(int value) CV_NOEXCEPT
{
lambda = value;
return *this;
}
FunctionParams& setSigma(float value) CV_NOEXCEPT
{
sigma = value;
return *this;
}
};
CV_WRAP static inline String
copyMatAndDumpNamedArguments(InputArray src, OutputArray dst,
const FunctionParams& params = FunctionParams())
{
src.copyTo(dst);
return format("lambda=%d, sigma=%.1f", params.lambda,
params.sigma);
}
namespace nested { namespace nested {
CV_WRAP static inline bool testEchoBooleanFunction(bool flag) { CV_WRAP static inline bool testEchoBooleanFunction(bool flag) {
return flag; return flag;

View File

@ -459,6 +459,7 @@ Cv64suf;
#define CV_EXPORTS_W_SIMPLE CV_EXPORTS #define CV_EXPORTS_W_SIMPLE CV_EXPORTS
#define CV_EXPORTS_AS(synonym) CV_EXPORTS #define CV_EXPORTS_AS(synonym) CV_EXPORTS
#define CV_EXPORTS_W_MAP CV_EXPORTS #define CV_EXPORTS_W_MAP CV_EXPORTS
#define CV_EXPORTS_W_PARAMS CV_EXPORTS
#define CV_IN_OUT #define CV_IN_OUT
#define CV_OUT #define CV_OUT
#define CV_PROP #define CV_PROP

View File

@ -241,6 +241,7 @@ class ClassProp(object):
def __init__(self, decl): def __init__(self, decl):
self.tp = decl[0].replace("*", "_ptr") self.tp = decl[0].replace("*", "_ptr")
self.name = decl[1] self.name = decl[1]
self.default_value = decl[2]
self.readonly = True self.readonly = True
if "/RW" in decl[3]: if "/RW" in decl[3]:
self.readonly = False self.readonly = False
@ -268,6 +269,7 @@ class ClassInfo(object):
self.cname = name.replace(".", "::") self.cname = name.replace(".", "::")
self.ismap = False self.ismap = False
self.is_parameters = False
self.issimple = False self.issimple = False
self.isalgorithm = False self.isalgorithm = False
self.methods = {} self.methods = {}
@ -300,6 +302,9 @@ class ClassInfo(object):
self.ismap = True self.ismap = True
elif m == "/Simple": elif m == "/Simple":
self.issimple = True self.issimple = True
elif m == "/Params":
self.is_parameters = True
self.issimple = True
self.props = [ClassProp(p) for p in decl[3]] self.props = [ClassProp(p) for p in decl[3]]
if not self.has_export_alias and self.original_name.startswith("Cv"): if not self.has_export_alias and self.original_name.startswith("Cv"):
@ -421,39 +426,55 @@ def handle_ptr(tp):
class ArgInfo(object): class ArgInfo(object):
def __init__(self, arg_tuple): def __init__(self, atype, name, default_value, modifiers=(),
self.tp = handle_ptr(arg_tuple[0]) enclosing_arg=None):
self.name = arg_tuple[1] # type: (ArgInfo, str, str, str, tuple[str, ...], ArgInfo | None) -> None
if self.name in python_reserved_keywords: self.tp = handle_ptr(atype)
self.name += "_" self.name = name
self.defval = arg_tuple[2] self.defval = default_value
self._modifiers = tuple(modifiers)
self.isarray = False self.isarray = False
self.is_smart_ptr = self.tp.startswith('Ptr<') # FIXIT: handle through modifiers - need to modify parser self.is_smart_ptr = self.tp.startswith('Ptr<') # FIXIT: handle through modifiers - need to modify parser
self.arraylen = 0 self.arraylen = 0
self.arraycvt = None self.arraycvt = None
self.inputarg = True for m in self._modifiers:
self.outputarg = False if m.startswith("/A"):
self.returnarg = False
self.isrvalueref = False
for m in arg_tuple[3]:
if m == "/O":
self.inputarg = False
self.outputarg = True
self.returnarg = True
elif m == "/IO":
self.inputarg = True
self.outputarg = True
self.returnarg = True
elif m.startswith("/A"):
self.isarray = True self.isarray = True
self.arraylen = m[2:].strip() self.arraylen = m[2:].strip()
elif m.startswith("/CA"): elif m.startswith("/CA"):
self.isarray = True self.isarray = True
self.arraycvt = m[2:].strip() self.arraycvt = m[2:].strip()
elif m == "/RRef":
self.isrvalueref = True
self.py_inputarg = False self.py_inputarg = False
self.py_outputarg = False self.py_outputarg = False
self.enclosing_arg = enclosing_arg
@property
def export_name(self):
if self.name in python_reserved_keywords:
return self.name + '_'
return self.name
@property
def inputarg(self):
return '/O' not in self._modifiers
@property
def outputarg(self):
return '/O' in self._modifiers or '/IO' in self._modifiers
@property
def returnarg(self):
return self.outputarg
@property
def isrvalueref(self):
return '/RRef' in self._modifiers
@property
def full_name(self):
if self.enclosing_arg is None:
return self.name
return self.enclosing_arg.name + '.' + self.name
def isbig(self): def isbig(self):
return self.tp in ["Mat", "vector_Mat", "cuda::GpuMat", "GpuMat", "vector_GpuMat", "UMat", "vector_UMat"] # or self.tp.startswith("vector") return self.tp in ["Mat", "vector_Mat", "cuda::GpuMat", "GpuMat", "vector_GpuMat", "UMat", "vector_UMat"] # or self.tp.startswith("vector")
@ -462,9 +483,62 @@ class ArgInfo(object):
return "ArgInfo(\"%s\", %d)" % (self.name, self.outputarg) return "ArgInfo(\"%s\", %d)" % (self.name, self.outputarg)
def find_argument_class_info(argument_type, function_namespace,
function_class_name, known_classes):
# type: (str, str, str, dict[str, ClassInfo]) -> ClassInfo | None
"""Tries to find corresponding class info for the provided argument type
Args:
argument_type (str): Function argument type
function_namespace (str): Namespace of the function declaration
function_class_name (str): Name of the class if function is a method of class
known_classes (dict[str, ClassInfo]): Mapping between string class
identifier and ClassInfo struct.
Returns:
Optional[ClassInfo]: class info struct if the provided argument type
refers to a known C++ class, None otherwise.
"""
possible_classes = tuple(filter(lambda cls: cls.endswith(argument_type), known_classes))
# If argument type is not a known class - just skip it
if not possible_classes:
return None
if len(possible_classes) == 1:
return known_classes[possible_classes[0]]
# If there is more than 1 matched class, try to select the most probable one
# Look for a matched class name in different scope, starting from the
# narrowest one
# First try to find argument inside class scope of the function (if any)
if function_class_name:
type_to_match = function_class_name + '_' + argument_type
if type_to_match in possible_classes:
return known_classes[type_to_match]
else:
type_to_match = argument_type
# Trying to find argument type in the namespace of the function
type_to_match = '{}_{}'.format(
function_namespace.lstrip('cv.').replace('.', '_'), type_to_match
)
if type_to_match in possible_classes:
return known_classes[type_to_match]
# Try to find argument name as is
if argument_type in possible_classes:
return known_classes[argument_type]
# NOTE: parser is broken - some classes might not be visible, depending on
# the order of parsed headers.
# print("[WARNING] Can't select an appropriate class for argument: '",
# argument_type, "'. Possible matches: '", possible_classes, "'")
return None
class FuncVariant(object): class FuncVariant(object):
def __init__(self, classname, name, decl, isconstructor, isphantom=False): def __init__(self, namespace, classname, name, decl, isconstructor, known_classes, isphantom=False):
self.classname = classname
self.name = self.wname = name self.name = self.wname = name
self.isconstructor = isconstructor self.isconstructor = isconstructor
self.isphantom = isphantom self.isphantom = isphantom
@ -476,8 +550,14 @@ class FuncVariant(object):
self.rettype = "" self.rettype = ""
self.args = [] self.args = []
self.array_counters = {} self.array_counters = {}
for a in decl[3]: for arg_decl in decl[3]:
ainfo = ArgInfo(a) assert len(arg_decl) == 4, \
'ArgInfo contract is violated. Arg declaration should contain:' \
'"arg_type", "name", "default_value", "modifiers". '\
'Got tuple: {}'.format(arg_decl)
ainfo = ArgInfo(atype=arg_decl[0], name=arg_decl[1],
default_value=arg_decl[2], modifiers=arg_decl[3])
if ainfo.isarray and not ainfo.arraycvt: if ainfo.isarray and not ainfo.arraycvt:
c = ainfo.arraylen c = ainfo.arraylen
c_arrlist = self.array_counters.get(c, []) c_arrlist = self.array_counters.get(c, [])
@ -486,9 +566,9 @@ class FuncVariant(object):
else: else:
self.array_counters[c] = [ainfo.name] self.array_counters[c] = [ainfo.name]
self.args.append(ainfo) self.args.append(ainfo)
self.init_pyproto() self.init_pyproto(namespace, classname, known_classes)
def init_pyproto(self): def init_pyproto(self, namespace, classname, known_classes):
# string representation of argument list, with '[', ']' symbols denoting optional arguments, e.g. # string representation of argument list, with '[', ']' symbols denoting optional arguments, e.g.
# "src1, src2[, dst[, mask]]" for cv.add # "src1, src2[, dst[, mask]]" for cv.add
argstr = "" argstr = ""
@ -510,12 +590,44 @@ class FuncVariant(object):
outlist = [] outlist = []
firstoptarg = 1000000 firstoptarg = 1000000
argno = -1
for a in self.args: # Check if there is params structure in arguments
argno += 1 arguments = []
for arg in self.args:
arg_class_info = find_argument_class_info(
arg.tp, namespace, classname, known_classes
)
# If argument refers to the 'named arguments' structure - instead of
# the argument put its properties
if arg_class_info is not None and arg_class_info.is_parameters:
for prop in arg_class_info.props:
# Convert property to ArgIfno and mark that argument is
# a part of the parameters structure:
arguments.append(
ArgInfo(prop.tp, prop.name, prop.default_value,
enclosing_arg=arg)
)
else:
arguments.append(arg)
# Prevent names duplication after named arguments are merged
# to the main arguments list
argument_names = tuple(arg.name for arg in arguments)
assert len(set(argument_names)) == len(argument_names), \
"Duplicate arguments with names '{}' in function '{}'. "\
"Please, check named arguments used in function interface".format(
argument_names, self.name
)
self.args = arguments
for argno, a in enumerate(self.args):
if a.name in self.array_counters: if a.name in self.array_counters:
continue continue
assert not a.tp in forbidden_arg_types, 'Forbidden type "{}" for argument "{}" in "{}" ("{}")'.format(a.tp, a.name, self.name, self.classname) assert a.tp not in forbidden_arg_types, \
'Forbidden type "{}" for argument "{}" in "{}" ("{}")'.format(
a.tp, a.name, self.name, self.classname
)
if a.tp in ignored_arg_types: if a.tp in ignored_arg_types:
continue continue
if a.returnarg: if a.returnarg:
@ -542,7 +654,7 @@ class FuncVariant(object):
firstoptarg = min(firstoptarg, len(arglist)) firstoptarg = min(firstoptarg, len(arglist))
noptargs = len(arglist) - firstoptarg noptargs = len(arglist) - firstoptarg
argnamelist = [aname for aname, argno in arglist] argnamelist = [self.args[argno].export_name for _, argno in arglist]
argstr = ", ".join(argnamelist[:firstoptarg]) argstr = ", ".join(argnamelist[:firstoptarg])
argstr = "[, ".join([argstr] + argnamelist[firstoptarg:]) argstr = "[, ".join([argstr] + argnamelist[firstoptarg:])
argstr += "]" * noptargs argstr += "]" * noptargs
@ -552,7 +664,6 @@ class FuncVariant(object):
assert outlist == [] assert outlist == []
outlist = [("self", -1)] outlist = [("self", -1)]
if self.isconstructor: if self.isconstructor:
classname = self.classname
if classname.startswith("Cv"): if classname.startswith("Cv"):
classname = classname[2:] classname = classname[2:]
outstr = "<%s object>" % (classname,) outstr = "<%s object>" % (classname,)
@ -566,9 +677,9 @@ class FuncVariant(object):
self.py_prototype = "%s(%s) -> %s" % (self.wname, argstr, outstr) self.py_prototype = "%s(%s) -> %s" % (self.wname, argstr, outstr)
self.py_noptargs = noptargs self.py_noptargs = noptargs
self.py_arglist = arglist self.py_arglist = arglist
for aname, argno in arglist: for _, argno in arglist:
self.args[argno].py_inputarg = True self.args[argno].py_inputarg = True
for aname, argno in outlist: for _, argno in outlist:
if argno >= 0: if argno >= 0:
self.args[argno].py_outputarg = True self.args[argno].py_outputarg = True
self.py_outlist = outlist self.py_outlist = outlist
@ -584,8 +695,11 @@ class FuncInfo(object):
self.is_static = is_static self.is_static = is_static
self.variants = [] self.variants = []
def add_variant(self, decl, isphantom=False): def add_variant(self, decl, known_classes, isphantom=False):
self.variants.append(FuncVariant(self.classname, self.name, decl, self.isconstructor, isphantom)) self.variants.append(
FuncVariant(self.namespace, self.classname, self.name, decl,
self.isconstructor, known_classes, isphantom)
)
def get_wrapper_name(self): def get_wrapper_name(self):
name = self.name name = self.name
@ -698,6 +812,7 @@ class FuncInfo(object):
# add necessary conversions from Python objects to code_cvt_list, # add necessary conversions from Python objects to code_cvt_list,
# form the function/method call, # form the function/method call,
# for the list of type mappings # for the list of type mappings
instantiated_args = set()
for a in v.args: for a in v.args:
if a.tp in ignored_arg_types: if a.tp in ignored_arg_types:
defval = a.defval defval = a.defval
@ -738,17 +853,29 @@ class FuncInfo(object):
arg_type_info = ArgTypeInfo(tp, FormatStrings.object, defval0, True) arg_type_info = ArgTypeInfo(tp, FormatStrings.object, defval0, True)
parse_name = a.name parse_name = a.name
if a.py_inputarg: if a.py_inputarg and arg_type_info.strict_conversion:
if arg_type_info.strict_conversion: parse_name = "pyobj_" + a.full_name.replace('.', '_')
code_decl += " PyObject* pyobj_%s = NULL;\n" % (a.name,) code_decl += " PyObject* %s = NULL;\n" % (parse_name,)
parse_name = "pyobj_" + a.name
if a.tp == 'char': 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(%s, &%s, %s)" % (parse_name, a.full_name, a.crepr()))
else: else:
code_cvt_list.append("pyopencv_to_safe(pyobj_%s, %s, %s)" % (a.name, a.name, a.crepr())) code_cvt_list.append("pyopencv_to_safe(%s, %s, %s)" % (parse_name, a.full_name, a.crepr()))
all_cargs.append([arg_type_info, parse_name]) all_cargs.append([arg_type_info, parse_name])
# Argument is actually a part of the named arguments structure,
# but it is possible to mimic further processing like it is normal arg
if a.enclosing_arg:
a = a.enclosing_arg
arg_type_info = ArgTypeInfo(a.tp, FormatStrings.object,
default_value=a.defval,
strict_conversion=True)
# Skip further actions if enclosing argument is already instantiated
# by its another field
if a.name in instantiated_args:
continue
instantiated_args.add(a.name)
defval = a.defval defval = a.defval
if not defval: if not defval:
defval = arg_type_info.default_value defval = arg_type_info.default_value
@ -773,8 +900,8 @@ class FuncInfo(object):
code_args += ", " code_args += ", "
if a.isrvalueref: if a.isrvalueref:
a.name = 'std::move(' + a.name + ')' code_args += amp + 'std::move(' + a.name + ')'
else:
code_args += amp + a.name code_args += amp + a.name
code_args += ")" code_args += ")"
@ -821,7 +948,7 @@ class FuncInfo(object):
# form the format spec for PyArg_ParseTupleAndKeywords # form the format spec for PyArg_ParseTupleAndKeywords
fmtspec = "".join([ fmtspec = "".join([
get_type_format_string(all_cargs[argno][0]) get_type_format_string(all_cargs[argno][0])
for aname, argno in v.py_arglist for _, argno in v.py_arglist
]) ])
if v.py_noptargs > 0: if v.py_noptargs > 0:
fmtspec = fmtspec[:-v.py_noptargs] + "|" + fmtspec[-v.py_noptargs:] fmtspec = fmtspec[:-v.py_noptargs] + "|" + fmtspec[-v.py_noptargs:]
@ -832,9 +959,9 @@ class FuncInfo(object):
# - calls PyArg_ParseTupleAndKeywords # - calls PyArg_ParseTupleAndKeywords
# - converts complex arguments from PyObject's to native OpenCV types # - converts complex arguments from PyObject's to native OpenCV types
code_parse = gen_template_parse_args.substitute( code_parse = gen_template_parse_args.substitute(
kw_list = ", ".join(['"' + aname + '"' for aname, argno in v.py_arglist]), kw_list=", ".join(['"' + v.args[argno].export_name + '"' for _, argno in v.py_arglist]),
fmtspec=fmtspec, fmtspec=fmtspec,
parse_arglist = ", ".join(["&" + all_cargs[argno][1] for aname, argno in v.py_arglist]), parse_arglist=", ".join(["&" + all_cargs[argno][1] for _, argno in v.py_arglist]),
code_cvt=" &&\n ".join(code_cvt_list)) code_cvt=" &&\n ".join(code_cvt_list))
else: else:
code_parse = "if(PyObject_Size(py_args) == 0 && (!kw || PyObject_Size(kw) == 0))" code_parse = "if(PyObject_Size(py_args) == 0 && (!kw || PyObject_Size(kw) == 0))"
@ -1036,7 +1163,7 @@ class PythonWrapperGenerator(object):
# Add it as a method to the class # Add it as a method to the class
func_map = self.classes[classname].methods func_map = self.classes[classname].methods
func = func_map.setdefault(name, FuncInfo(classname, name, cname, isconstructor, namespace_str, is_static)) func = func_map.setdefault(name, FuncInfo(classname, name, cname, isconstructor, namespace_str, is_static))
func.add_variant(decl, isphantom) func.add_variant(decl, self.classes, isphantom)
# Add it as global function # Add it as global function
g_name = "_".join(classes+[name]) g_name = "_".join(classes+[name])
@ -1053,10 +1180,10 @@ class PythonWrapperGenerator(object):
func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs
# Exports static function with internal name (backward compatibility) # Exports static function with internal name (backward compatibility)
func = func_map.setdefault(g_name, FuncInfo("", g_name, cname, isconstructor, namespace_str, False)) func = func_map.setdefault(g_name, FuncInfo("", g_name, cname, isconstructor, namespace_str, False))
func.add_variant(decl, isphantom) func.add_variant(decl, self.classes, isphantom)
if g_wname != g_name: # TODO OpenCV 5.0 if g_wname != g_name: # TODO OpenCV 5.0
wfunc = func_map.setdefault(g_wname, FuncInfo("", g_wname, cname, isconstructor, namespace_str, False)) wfunc = func_map.setdefault(g_wname, FuncInfo("", g_wname, cname, isconstructor, namespace_str, False))
wfunc.add_variant(decl, isphantom) wfunc.add_variant(decl, self.classes, isphantom)
else: else:
if classname and not isconstructor: if classname and not isconstructor:
if not isphantom: if not isphantom:
@ -1066,7 +1193,7 @@ class PythonWrapperGenerator(object):
func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs
func = func_map.setdefault(name, FuncInfo(classname, name, cname, isconstructor, namespace_str, is_static)) func = func_map.setdefault(name, FuncInfo(classname, name, cname, isconstructor, namespace_str, is_static))
func.add_variant(decl, isphantom) func.add_variant(decl, self.classes, isphantom)
if classname and isconstructor: if classname and isconstructor:
self.classes[classname].constructor = func self.classes[classname].constructor = func

View File

@ -259,6 +259,10 @@ class CppHeaderParser(object):
if "CV_EXPORTS_W_SIMPLE" in l: if "CV_EXPORTS_W_SIMPLE" in l:
l = l.replace("CV_EXPORTS_W_SIMPLE", "") l = l.replace("CV_EXPORTS_W_SIMPLE", "")
modlist.append("/Simple") modlist.append("/Simple")
if "CV_EXPORTS_W_PARAMS" in l:
l = l.replace("CV_EXPORTS_W_PARAMS", "")
modlist.append("/Map")
modlist.append("/Params")
npos = l.find("CV_EXPORTS_AS") npos = l.find("CV_EXPORTS_AS")
if npos < 0: if npos < 0:
npos = l.find('CV_WRAP_AS') npos = l.find('CV_WRAP_AS')
@ -776,7 +780,15 @@ class CppHeaderParser(object):
var_list = [var_name1] + [i.strip() for i in var_list[1:]] var_list = [var_name1] + [i.strip() for i in var_list[1:]]
for v in var_list: for v in var_list:
class_decl[3].append([var_type, v, "", var_modlist]) prop_definition = v.split('=')
prop_name = prop_definition[0].strip()
if len(prop_definition) == 1:
# default value is not provided
prop_default_value = ''
else:
prop_default_value = prop_definition[-1]
class_decl[3].append([var_type, prop_name, prop_default_value,
var_modlist])
return stmt_type, "", False, None return stmt_type, "", False, None
# something unknown # something unknown

View File

@ -738,6 +738,29 @@ class Arguments(NewOpenCVTests):
) )
) )
def test_named_arguments_without_parameters(self):
src = np.ones((5, 5, 3), dtype=np.uint8)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(src)
np.testing.assert_equal(src, src_copy)
self.assertEqual(arguments_dump, 'lambda=-1, sigma=0.0')
def test_named_arguments_without_output_argument(self):
src = np.zeros((2, 2, 3), dtype=np.uint8)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(
src, lambda_=15, sigma=3.5
)
np.testing.assert_equal(src, src_copy)
self.assertEqual(arguments_dump, 'lambda=15, sigma=3.5')
def test_named_arguments_with_output_argument(self):
src = np.zeros((3, 3, 3), dtype=np.uint8)
dst = np.ones_like(src)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(
src, dst, lambda_=25, sigma=5.5
)
np.testing.assert_equal(src, src_copy)
np.testing.assert_equal(dst, src_copy)
self.assertEqual(arguments_dump, 'lambda=25, sigma=5.5')
class CanUsePurePythonModuleFunction(NewOpenCVTests): class CanUsePurePythonModuleFunction(NewOpenCVTests):