diff --git a/modules/matlab/generator/build_info.py b/modules/matlab/generator/build_info.py index e02354974..50d3ffe45 100644 --- a/modules/matlab/generator/build_info.py +++ b/modules/matlab/generator/build_info.py @@ -1,4 +1,4 @@ -#/usr/bin/env python +#!/usr/bin/env python def substitute(build, output_dir): @@ -18,12 +18,32 @@ def substitute(build, output_dir): if not os.path.isdir(output_dir): os.mkdir(output_dir) - # populate templates + # populate template populated = template.render(build=build) with open(os.path.join(output_dir, 'buildInformation.m'), 'wb') as f: f.write(populated) if __name__ == "__main__": + """ + Usage: python build_info.py --os os_version_string + --arch [bitness processor] + --compiler [id version] + --mex_arch arch_string + --mex_script /path/to/mex/script + --cxx_flags [-list -of -flags -to -passthrough] + --opencv_version version_string + --commit commit_hash_if_using_git + --modules [core imgproc highgui etc] + --configuration Debug/Release + --outdir /path/to/write/build/info + + build_info.py generates a Matlab function that can be invoked with a call to + >> cv.buildInformation(); + + This function prints a summary of the user's OS, OpenCV and Matlab build + given the information passed to this module. build_info.py invokes Jinja2 + on the template_build_info.m template. + """ # parse the input options import sys, re, os diff --git a/modules/matlab/generator/filters.py b/modules/matlab/generator/filters.py index 0ae371c4c..e908fe8ff 100644 --- a/modules/matlab/generator/filters.py +++ b/modules/matlab/generator/filters.py @@ -1,6 +1,7 @@ from textwrap import TextWrapper from string import split, join import re, os +# precompile a URL matching regular expression urlexpr = re.compile(r"((https?):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.MULTILINE|re.UNICODE) def inputs(args): @@ -35,9 +36,11 @@ def only(args): return d def void(arg): + '''Is the input 'void' ''' return arg == 'void' def flip(arg): + '''flip the sign of the input''' return not arg def noutputs(fun): @@ -45,6 +48,7 @@ def noutputs(fun): return int(not void(fun.rtp)) + len(outputs(fun.req)) + len(outputs(fun.opt)) def convertibleToInt(string): + '''Can the input string be evaluated to an integer?''' salt = '1+' try: exec(salt+string) @@ -53,12 +57,21 @@ def convertibleToInt(string): return False def binaryToDecimal(string): + '''Attempt to convert the input string to floating point representation''' try: return str(eval(string)) except: return string def formatMatlabConstant(string, table): + ''' + Given a string representing a Constant, and a table of all Constants, + attempt to resolve the Constant into a valid Matlab expression + For example, the input + DEPENDENT_VALUE = 1 << FIXED_VALUE + needs to be converted to + DEPENDENT_VALUE = bitshift(1, cv.FIXED_VALUE); + ''' # split the string into expressions words = re.split('(\W+)', string) # add a 'cv' prefix if an expression is also a key in the lookup table @@ -76,20 +89,33 @@ def matlabURL(string): return re.sub(urlexpr, '\\1', string) def capitalizeFirst(text): + '''Capitalize only the first character of the text string''' return text[0].upper() + text[1:] def toUpperCamelCase(text): + '''variable_name --> VariableName''' return ''.join([capitalizeFirst(word) for word in text.split('_')]) def toLowerCamelCase(text): + '''variable_name --> variableName''' upper_camel = toUpperCamelCase(test) return upper_camel[0].lower() + upper_camel[1:] def toUnderCase(text): + '''VariableName --> variable_name''' s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() def stripTags(text): + ''' + strip or convert html tags from a text string + content --> content + --> '' + < --> < + > --> > + &le --> <= + &ge --> >= + ''' upper = lambda pattern: pattern.group(1).upper() text = re.sub('(.*?)', upper, text) text = re.sub('<([^=\s].*?)>', '', text) @@ -100,18 +126,26 @@ def stripTags(text): return text def qualify(text, name): + '''Adds uppercase 'CV.' qualification to any occurrences of name in text''' return re.sub(name.upper(), 'CV.'+name.upper(), text) def slugify(text): + '''A_Function_name --> a-function-name''' return text.lower().replace('_', '-') def filename(fullpath): + '''Returns only the filename without an extension from a file path + eg. /path/to/file.txt --> file + ''' return os.path.splitext(os.path.basename(fullpath))[0] def csv(items, sep=', '): + '''format a list with a separator (comma if not specified)''' return sep.join(item for item in items) def stripExtraSpaces(text): + '''Removes superfluous whitespace from a string, including the removal + of all leading and trailing whitespace''' return ' '.join(text.split()) def comment(text, wrap=80, escape='% ', escape_first='', escape_last=''): diff --git a/modules/matlab/generator/gen_matlab.py b/modules/matlab/generator/gen_matlab.py index 86c5ff6d9..8acc6facb 100644 --- a/modules/matlab/generator/gen_matlab.py +++ b/modules/matlab/generator/gen_matlab.py @@ -1,8 +1,27 @@ -#/usr/bin/env python +#!/usr/bin/env python class MatlabWrapperGenerator(object): + """ + MatlabWrapperGenerator is a class for generating Matlab mex sources from + a set of C++ headers. MatlabWrapperGenerator objects can be default + constructed. Given an instance, the gen() method performs the translation. + """ def gen(self, module_root, modules, extras, output_dir): + """ + Generate a set of Matlab mex source files by parsing exported symbols + in a set of C++ headers. The headers can be input in one (or both) of + two methods: + 1. specify module_root and modules + Given a path to the OpenCV module root and a list of module names, + the headers to parse are implicitly constructed. + 2. specifiy header locations explicitly in extras + Each element in the list of extras must be of the form: + 'namespace=/full/path/to/extra/header.hpp' where 'namespace' is + the namespace in which the definitions should be added. + The output_dir specifies the directory to write the generated sources + to. + """ # parse each of the files and store in a dictionary # as a separate "namespace" parser = CppHeaderParser() @@ -109,8 +128,41 @@ class MatlabWrapperGenerator(object): f.write(populated) - if __name__ == "__main__": + """ + Usage: python gen_matlab.py --hdrparser /path/to/hdr_parser/dir + --rstparser /path/to/rst_parser/dir + --moduleroot /path/to/opencv/modules + --modules [core imgproc objdetect etc] + --extra namespace=/path/to/extra/header.hpp + --outdir /path/to/output/generated/srcs + + gen_matlab.py is the main control script for generating matlab source + files from given set of headers. Internally, gen_matlab: + 1. constructs the headers to parse from the module root and list of modules + 2. parses the headers using CppHeaderParser + 3. refactors the definitions using ParseTree + 4. parses .rst docs using RstParser + 5. populates the templates for classes, function, enums and docs from the + definitions + + gen_matlab.py requires the following inputs: + --hdrparser the path to the header parser directory + (opencv/modules/python/src2) + --rstparser the path to the rst parser directory + (opencv/modules/java/generator) + --moduleroot (optional) path to the opencv directory containing the modules + --modules (optional - required if --moduleroot specified) the modules + to produce bindings for. The path to the include directories + as well as the namespaces are constructed from the modules + and the moduleroot + --extra extra headers explicitly defined to parse. This must be in + the format "namepsace=/path/to/extra/header.hpp". For example, + the core module requires the extra header: + "core=/opencv/modules/core/include/opencv2/core/core/base.hpp" + --outdir the output directory to put the generated matlab sources. In + the OpenCV build this is "${CMAKE_CURRENT_BUILD_DIR}/src" + """ # parse the input options import sys, re, os, time diff --git a/modules/matlab/generator/parse_tree.py b/modules/matlab/generator/parse_tree.py index e70bc30ca..4362f89f2 100644 --- a/modules/matlab/generator/parse_tree.py +++ b/modules/matlab/generator/parse_tree.py @@ -3,6 +3,73 @@ from textwrap import fill from filters import * class ParseTree(object): + """ + The ParseTree class produces a semantic tree of C++ definitions given + the output of the CppHeaderParser (from opencv/modules/python/src2/hdr_parser.py) + + The full hierarchy is as follows: + + Namespaces + | + |- name + |- Classes + | + |- name + |- Methods + |- Constants + |- Methods + | + |- name + |- static (T/F) + |- return type + |- required Arguments + | + |- name + |- const (T/F) + |- reference ('&'/'*') + |- type + |- input + |- output (pass return by reference) + |- default value + |- optional Arguments + |- Constants + | + |- name + |- const (T/F) + |- reference ('&'/'*') + |- type + |- value + + The semantic tree contains substantial information for easily introspecting + information about objects. How many methods does the 'core' namespace have? + Does the 'randn' method have any return by reference (output) arguments? + How many required and optional arguments does the 'add' method have? Is the + variable passed by reference or raw pointer? + + Individual definitions from the parse tree (Classes, Functions, Constants) + are passed to the Jinja2 template engine where they are manipulated to + produce Matlab mex sources. + + A common call tree for constructing and using a ParseTree object is: + + # parse a set of definitions into a dictionary of namespaces + parser = CppHeaderParser() + ns['core'] = parser.parse('path/to/opencv/core.hpp') + + # refactor into a semantic tree + parse_tree = ParseTree() + parse_tree.build(ns) + + # iterate over the tree + for namespace in parse_tree.namespaces: + for clss in namespace.classes: + # do stuff + for method in namespace.methods: + # do stuff + + Calling 'print' on a ParseTree object will reconstruct the definitions + to produce an output resembling the original C++ code. + """ def __init__(self, namespaces=None): self.namespaces = namespaces if namespaces else [] @@ -48,6 +115,15 @@ class ParseTree(object): class Translator(object): + """ + The Translator class does the heavy lifting of translating the nested + list representation of the hdr_parser into individual definitions that + are inserted into the ParseTree. + Translator consists of a top-level method: translate() + along with a number of helper methods: translateClass(), translateMethod(), + translateArgument(), translateConstant(), translateName(), and + translateClassName() + """ def translate(self, defn): # --- class --- # classes have 'class' prefixed on their name @@ -116,6 +192,14 @@ class Translator(object): class Namespace(object): + """ + Namespace + | + |- name + |- Constants + |- Methods + |- Constants + """ def __init__(self, name='', constants=None, classes=None, methods=None): self.name = name self.constants = constants if constants else [] @@ -129,6 +213,13 @@ class Namespace(object): (join((o.__str__() for o in self.classes), '\n\n') if self.classes else '')+'\n};' class Class(object): + """ + Class + | + |- name + |- Methods + |- Constants + """ def __init__(self, name='', namespace='', constants=None, methods=None): self.name = name self.namespace = namespace @@ -141,6 +232,21 @@ class Class(object): (join((f.__str__() for f in self.methods), '\n\t') if self.methods else '')+'\n};' class Method(object): + """ + Method + int VideoWriter::read( cv::Mat& frame, const cv::Mat& mask=cv::Mat() ); + --- ----- ---- -------- ---------------- + rtp class name required optional + + name the method name + clss the class the method belongs to ('' if free) + static static? + namespace the namespace the method belongs to ('' if free) + rtp the return type + const const? + req list of required arguments + opt list of optional arguments + """ def __init__(self, name='', clss='', static=False, namespace='', rtp='', const=False, req=None, opt=None): self.name = name self.clss = clss @@ -158,6 +264,20 @@ class Method(object): ')'+(' const' if self.const else '')+';' class Argument(object): + """ + Argument + const cv::Mat& mask=cv::Mat() + ----- ---- --- ---- ------- + const tp ref name default + + name the argument name + tp the argument type + const const? + I is the argument treated as an input? + O is the argument treated as an output (return by reference) + ref is the argument passed by reference? ('*'/'&') + default the default value of the argument ('' if required) + """ def __init__(self, name='', tp='', const=False, I=True, O=False, ref='', default=''): self.name = name self.tp = tp @@ -172,6 +292,19 @@ class Argument(object): ' '+self.name+('='+self.default if self.default else '') class Constant(object): + """ + Constant + DFT_COMPLEX_OUTPUT = 12; + ---- ------- + name default + + name the name of the constant + clss the class that the constant belongs to ('' if free) + tp the type of the constant ('' if int) + const const? + ref is the constant a reference? ('*'/'&') + default default value, required for constants + """ def __init__(self, name='', clss='', tp='', const=False, ref='', default=''): self.name = name self.clss = clss @@ -185,6 +318,10 @@ class Constant(object): ' '+self.name+('='+self.default if self.default else '')+';' def constants(tree): + """ + recursive generator to strip all Constant objects from the ParseTree + and place them into a flat dictionary of { name, value (default) } + """ if isinstance(tree, dict) and 'constants' in tree and isinstance(tree['constants'], list): for node in tree['constants']: yield (node['name'], node['default']) @@ -198,6 +335,10 @@ def constants(tree): yield gen def todict(obj, classkey=None): + """ + Convert the ParseTree to a dictionary, stripping all objects of their + methods and converting class names to strings + """ if isinstance(obj, dict): for k in obj.keys(): obj[k] = todict(obj[k], classkey)