From b07031b59415e38ad12e0f8aeee27531e7a36c07 Mon Sep 17 00:00:00 2001 From: Vadim Levin Date: Mon, 6 Feb 2023 16:41:20 +0300 Subject: [PATCH] feat: named arguments handling in Python interface --- .../include/opencv2/core/bindings_utils.hpp | 27 ++ modules/core/include/opencv2/core/cvdef.h | 1 + modules/python/src2/gen2.py | 245 +++++++++++++----- modules/python/src2/hdr_parser.py | 14 +- modules/python/test/test_misc.py | 23 ++ 5 files changed, 250 insertions(+), 60 deletions(-) diff --git a/modules/core/include/opencv2/core/bindings_utils.hpp b/modules/core/include/opencv2/core/bindings_utils.hpp index 001d91c381..64f346570a 100644 --- a/modules/core/include/opencv2/core/bindings_utils.hpp +++ b/modules/core/include/opencv2/core/bindings_utils.hpp @@ -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 { CV_WRAP static inline bool testEchoBooleanFunction(bool flag) { return flag; diff --git a/modules/core/include/opencv2/core/cvdef.h b/modules/core/include/opencv2/core/cvdef.h index 957e7bcd1d..8307ca7d1c 100644 --- a/modules/core/include/opencv2/core/cvdef.h +++ b/modules/core/include/opencv2/core/cvdef.h @@ -459,6 +459,7 @@ Cv64suf; #define CV_EXPORTS_W_SIMPLE CV_EXPORTS #define CV_EXPORTS_AS(synonym) CV_EXPORTS #define CV_EXPORTS_W_MAP CV_EXPORTS +#define CV_EXPORTS_W_PARAMS CV_EXPORTS #define CV_IN_OUT #define CV_OUT #define CV_PROP diff --git a/modules/python/src2/gen2.py b/modules/python/src2/gen2.py index d7a54910ba..92629c0e7a 100755 --- a/modules/python/src2/gen2.py +++ b/modules/python/src2/gen2.py @@ -241,6 +241,7 @@ class ClassProp(object): def __init__(self, decl): self.tp = decl[0].replace("*", "_ptr") self.name = decl[1] + self.default_value = decl[2] self.readonly = True if "/RW" in decl[3]: self.readonly = False @@ -268,6 +269,7 @@ class ClassInfo(object): self.cname = name.replace(".", "::") self.ismap = False + self.is_parameters = False self.issimple = False self.isalgorithm = False self.methods = {} @@ -300,6 +302,9 @@ class ClassInfo(object): self.ismap = True elif m == "/Simple": self.issimple = True + elif m == "/Params": + self.is_parameters = True + self.issimple = True self.props = [ClassProp(p) for p in decl[3]] if not self.has_export_alias and self.original_name.startswith("Cv"): @@ -421,39 +426,55 @@ def handle_ptr(tp): class ArgInfo(object): - def __init__(self, arg_tuple): - self.tp = handle_ptr(arg_tuple[0]) - self.name = arg_tuple[1] - if self.name in python_reserved_keywords: - self.name += "_" - self.defval = arg_tuple[2] + def __init__(self, atype, name, default_value, modifiers=(), + enclosing_arg=None): + # type: (ArgInfo, str, str, str, tuple[str, ...], ArgInfo | None) -> None + self.tp = handle_ptr(atype) + self.name = name + self.defval = default_value + self._modifiers = tuple(modifiers) self.isarray = False self.is_smart_ptr = self.tp.startswith('Ptr<') # FIXIT: handle through modifiers - need to modify parser self.arraylen = 0 self.arraycvt = None - self.inputarg = True - self.outputarg = False - 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"): + for m in self._modifiers: + if m.startswith("/A"): self.isarray = True self.arraylen = m[2:].strip() elif m.startswith("/CA"): self.isarray = True self.arraycvt = m[2:].strip() - elif m == "/RRef": - self.isrvalueref = True self.py_inputarg = 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): 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) +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): - def __init__(self, classname, name, decl, isconstructor, isphantom=False): - self.classname = classname + def __init__(self, namespace, classname, name, decl, isconstructor, known_classes, isphantom=False): self.name = self.wname = name self.isconstructor = isconstructor self.isphantom = isphantom @@ -476,8 +550,14 @@ class FuncVariant(object): self.rettype = "" self.args = [] self.array_counters = {} - for a in decl[3]: - ainfo = ArgInfo(a) + for arg_decl in decl[3]: + 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: c = ainfo.arraylen c_arrlist = self.array_counters.get(c, []) @@ -486,9 +566,9 @@ class FuncVariant(object): else: self.array_counters[c] = [ainfo.name] 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. # "src1, src2[, dst[, mask]]" for cv.add argstr = "" @@ -510,12 +590,44 @@ class FuncVariant(object): outlist = [] firstoptarg = 1000000 - argno = -1 - for a in self.args: - argno += 1 + + # Check if there is params structure in arguments + 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: 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: continue if a.returnarg: @@ -542,7 +654,7 @@ class FuncVariant(object): firstoptarg = min(firstoptarg, len(arglist)) 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([argstr] + argnamelist[firstoptarg:]) argstr += "]" * noptargs @@ -552,9 +664,8 @@ class FuncVariant(object): assert outlist == [] outlist = [("self", -1)] if self.isconstructor: - classname = self.classname if classname.startswith("Cv"): - classname=classname[2:] + classname = classname[2:] outstr = "<%s object>" % (classname,) elif outlist: outstr = ", ".join([o[0] for o in outlist]) @@ -566,9 +677,9 @@ class FuncVariant(object): self.py_prototype = "%s(%s) -> %s" % (self.wname, argstr, outstr) self.py_noptargs = noptargs self.py_arglist = arglist - for aname, argno in arglist: + for _, argno in arglist: self.args[argno].py_inputarg = True - for aname, argno in outlist: + for _, argno in outlist: if argno >= 0: self.args[argno].py_outputarg = True self.py_outlist = outlist @@ -584,8 +695,11 @@ class FuncInfo(object): self.is_static = is_static self.variants = [] - def add_variant(self, decl, isphantom=False): - self.variants.append(FuncVariant(self.classname, self.name, decl, self.isconstructor, isphantom)) + def add_variant(self, decl, known_classes, isphantom=False): + self.variants.append( + FuncVariant(self.namespace, self.classname, self.name, decl, + self.isconstructor, known_classes, isphantom) + ) def get_wrapper_name(self): name = self.name @@ -698,6 +812,7 @@ class FuncInfo(object): # add necessary conversions from Python objects to code_cvt_list, # form the function/method call, # for the list of type mappings + instantiated_args = set() for a in v.args: if a.tp in ignored_arg_types: defval = a.defval @@ -738,17 +853,29 @@ class FuncInfo(object): arg_type_info = ArgTypeInfo(tp, FormatStrings.object, defval0, True) parse_name = a.name - if a.py_inputarg: - 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())) - else: - code_cvt_list.append("pyopencv_to_safe(pyobj_%s, %s, %s)" % (a.name, a.name, a.crepr())) + if a.py_inputarg and arg_type_info.strict_conversion: + parse_name = "pyobj_" + a.full_name.replace('.', '_') + code_decl += " PyObject* %s = NULL;\n" % (parse_name,) + if a.tp == 'char': + code_cvt_list.append("convert_to_char(%s, &%s, %s)" % (parse_name, a.full_name, a.crepr())) + else: + 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]) + # 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 if not defval: defval = arg_type_info.default_value @@ -773,9 +900,9 @@ class FuncInfo(object): code_args += ", " if a.isrvalueref: - a.name = 'std::move(' + a.name + ')' - - code_args += amp + a.name + code_args += amp + 'std::move(' + a.name + ')' + else: + code_args += amp + a.name code_args += ")" @@ -821,7 +948,7 @@ class FuncInfo(object): # form the format spec for PyArg_ParseTupleAndKeywords fmtspec = "".join([ 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: fmtspec = fmtspec[:-v.py_noptargs] + "|" + fmtspec[-v.py_noptargs:] @@ -832,10 +959,10 @@ class FuncInfo(object): # - calls PyArg_ParseTupleAndKeywords # - converts complex arguments from PyObject's to native OpenCV types code_parse = gen_template_parse_args.substitute( - kw_list = ", ".join(['"' + aname + '"' for aname, argno in v.py_arglist]), - fmtspec = fmtspec, - parse_arglist = ", ".join(["&" + all_cargs[argno][1] for aname, argno in v.py_arglist]), - code_cvt = " &&\n ".join(code_cvt_list)) + kw_list=", ".join(['"' + v.args[argno].export_name + '"' for _, argno in v.py_arglist]), + fmtspec=fmtspec, + parse_arglist=", ".join(["&" + all_cargs[argno][1] for _, argno in v.py_arglist]), + code_cvt=" &&\n ".join(code_cvt_list)) else: 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 func_map = self.classes[classname].methods 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 g_name = "_".join(classes+[name]) @@ -1053,10 +1180,10 @@ class PythonWrapperGenerator(object): func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs # Exports static function with internal name (backward compatibility) 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 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: if classname and not isconstructor: if not isphantom: @@ -1066,7 +1193,7 @@ class PythonWrapperGenerator(object): func_map = self.namespaces.setdefault(namespace_str, Namespace()).funcs 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: self.classes[classname].constructor = func diff --git a/modules/python/src2/hdr_parser.py b/modules/python/src2/hdr_parser.py index ebe13f05c7..f5df3e2aab 100755 --- a/modules/python/src2/hdr_parser.py +++ b/modules/python/src2/hdr_parser.py @@ -259,6 +259,10 @@ class CppHeaderParser(object): if "CV_EXPORTS_W_SIMPLE" in l: l = l.replace("CV_EXPORTS_W_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") if npos < 0: 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:]] 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 # something unknown diff --git a/modules/python/test/test_misc.py b/modules/python/test/test_misc.py index 765201e973..0e5f5bc018 100644 --- a/modules/python/test/test_misc.py +++ b/modules/python/test/test_misc.py @@ -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): -- GitLab