diff --git a/modules/matlab/CMakeLists.txt b/modules/matlab/CMakeLists.txt index 261dcf259e..fc397e4658 100644 --- a/modules/matlab/CMakeLists.txt +++ b/modules/matlab/CMakeLists.txt @@ -34,20 +34,29 @@ macro(PREPEND TOKEN OUT IN) endforeach() endmacro() + # WARN_MIXED_PRECISION # Formats a warning message if the compiler and Matlab bitness is different macro(WARN_MIXED_PRECISION COMPILER_BITNESS MATLAB_BITNESS) set(MSG "Your compiler is ${COMPILER_BITNESS}-bit") set(MSG "${MSG} but your version of Matlab is ${MATLAB_BITNESS}-bit.") - set(MSG "${MSG} Mixed preicision pointers are not supported. Disabling Matlab bindings...") + set(MSG "${MSG} To build Matlab bindings, please switch to a ${MATLAB_BITNESS}-bit compiler.") message(WARNING ${MSG}) endmacro() +# ---------------------------------------------------------------------------- +# Architecture checks +# ---------------------------------------------------------------------------- # make sure we're on a supported architecture with Matlab and python installed -if (IOS OR ANDROID OR NOT MATLAB_FOUND OR NOT PYTHONLIBS_FOUND) +if (IOS OR ANDROID OR NOT MATLAB_FOUND) + ocv_module_disable(matlab) + return() +elseif (NOT PYTHONLIBS_FOUND) + message(WARNING "A required dependency of the matlab module (PythonLibs) was not found. Disabling Matlab bindings...") ocv_module_disable(matlab) return() endif() + # If the user built OpenCV as X-bit, but they have a Y-bit version of Matlab, # attempting to link to OpenCV during binding generation will fail, since @@ -63,6 +72,16 @@ elseif (${ARCH} EQUAL 64 AND NOT ${MATLAB_ARCH} MATCHES "64") return() endif() +# If it's MSVC, warn the user that bindings will only be built in Release mode. +# Debug mode seems to cause issues... +if (MSVC) + message(STATUS "Warning: Matlab bindings will only be built in Release configurations") +endif() + + +# ---------------------------------------------------------------------------- +# Configure time components +# ---------------------------------------------------------------------------- set(the_description "The Matlab/Octave bindings") ocv_add_module(matlab BINDINGS OPTIONAL opencv_core @@ -93,9 +112,9 @@ endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) -# ---------------------------------------------------------------------------- -# Configure time components -# ---------------------------------------------------------------------------- +# intersection of available modules and optional dependencies +# 1. populate the command-line include directories (-I/path/to/module/header, ...) +# 2. populate the command-line link libraries (-lopencv_core, ...) for Debug and Release set(MATLAB_DEPS ${OPENCV_MODULE_${the_module}_REQ_DEPS} ${OPENCV_MODULE_${the_module}_OPT_DEPS}) foreach(opencv_module ${MATLAB_DEPS}) if (HAVE_${opencv_module}) @@ -114,7 +133,7 @@ list(APPEND opencv_extra_hdrs "core=${OPENCV_MODULE_opencv_core_LOCATION}/includ # pass the OPENCV_CXX_EXTRA_FLAGS through to the mex compiler # remove the visibility modifiers, so the mex gateway is visible # TODO: get mex working without warnings -#string(REGEX REPLACE "[^\ ]*visibility[^\ ]*" "" MEX_CXXFLAGS "${OPENCV_EXTRA_FLAGS} ${OPENCV_EXTRA_CXX_FLAGS}") +string(REGEX REPLACE "[^\ ]*visibility[^\ ]*" "" MEX_CXXFLAGS "${OPENCV_EXTRA_FLAGS} ${OPENCV_EXTRA_CXX_FLAGS}") # Configure checks # Check to see whether the generator and the mex compiler are working. @@ -149,7 +168,7 @@ if (NOT MEX_WORKS) # attempt to compile a gateway using mex message(STATUS "Trying to compile mex file") execute_process( - COMMAND ${MATLAB_MEX_SCRIPT} ${MEX_OPTS} "CXXFLAGS=\$CXXFLAGS ${MEX_CXXFLAGS}" + COMMAND ${MATLAB_MEX_SCRIPT} ${MEX_OPTS} "CXXFLAGS=\$CXXFLAGS ${MEX_CXX_FLAGS}" ${MEX_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_compiler.cpp WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/junk ERROR_VARIABLE MEX_ERROR @@ -208,6 +227,14 @@ add_custom_command( --modules ${opencv_modules} --configuration "$(Configuration)" ${CMAKE_BUILD_TYPE} --outdir ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${PYTHON_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/generator/cvmex.py + --opts="${MEX_OPTS}" + --include_dirs="${MEX_INCLUDE_DIRS}" + --lib_dir=${MEX_LIB_DIR} + --libs="${MEX_LIBS}" + --flags ${MEX_CXXFLAGS} + --outdir ${CMAKE_CURRENT_BINARY_DIR} COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/test/help.m ${CMAKE_CURRENT_BINARY_DIR}/+cv COMMAND ${CMAKE_COMMAND} -E touch ${GENERATE_PROXY} COMMENT "Generating Matlab source files" @@ -222,7 +249,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -DMATLAB_MEX_SCRIPT=${MATLAB_MEX_SCRIPT} -DMATLAB_MEXEXT=${MATLAB_MEXEXT} -DMEX_OPTS=${MEX_OPTS} - -DMEX_CXXFLAGS=${MEX_CXXFLAGS} + -DMEX_CXXFLAGS=${MEX_CXX_FLAGS} -DMEX_INCLUDE_DIRS="${MEX_INCLUDE_DIRS}" -DMEX_LIB_DIR=${MEX_LIB_DIR} -DCONFIGURATION="$(Configuration)" diff --git a/modules/matlab/compile.cmake b/modules/matlab/compile.cmake index fd4ebd483c..ebc3fd9a68 100644 --- a/modules/matlab/compile.cmake +++ b/modules/matlab/compile.cmake @@ -1,7 +1,12 @@ +# LISTIFY +# Given a string of space-delimited tokens, reparse as a string of +# semi-colon delimited tokens, which in CMake land is exactly equivalent +# to a list macro(listify OUT_LIST IN_STRING) string(REPLACE " " ";" ${OUT_LIST} ${IN_STRING}) endmacro() +# listify multiple-argument inputs listify(MEX_INCLUDE_DIRS_LIST ${MEX_INCLUDE_DIRS}) if (${CONFIGURATION} MATCHES "Debug") listify(MEX_LIBS_LIST ${MEX_DEBUG_LIBS}) @@ -9,11 +14,22 @@ else() listify(MEX_LIBS_LIST ${MEX_LIBS}) endif() +# if it's MSVC building a Debug configuration, don't build bindings +if ("${CONFIGURATION}" MATCHES "Debug") + message(STATUS "Matlab bindings are only available in Release configurations. Skipping...") + return() +endif() + +# for each generated source file: +# 1. check if the file has already been compiled +# 2. attempt compile if required +# 3. if the compile fails, throw an error and cancel compilation file(GLOB SOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/src/*.cpp") foreach(SOURCE_FILE ${SOURCE_FILES}) # strip out the filename get_filename_component(FILENAME ${SOURCE_FILE} NAME_WE) - # compie the source file using mex + # compile the source file using mex + execute_process(COMMAND echo ${FILENAME}) if (NOT EXISTS ${CMAKE_CURRENT_BINARY_DIR}/+cv/${FILENAME}.${MATLAB_MEXEXT}) execute_process( COMMAND ${MATLAB_MEX_SCRIPT} ${MEX_OPTS} "CXXFLAGS=\$CXXFLAGS ${MEX_CXXFLAGS}" ${MEX_INCLUDE_DIRS_LIST} diff --git a/modules/matlab/generator/build_info.py b/modules/matlab/generator/build_info.py index 50d3ffe452..beb91bd108 100644 --- a/modules/matlab/generator/build_info.py +++ b/modules/matlab/generator/build_info.py @@ -19,7 +19,7 @@ def substitute(build, output_dir): os.mkdir(output_dir) # populate template - populated = template.render(build=build) + populated = template.render(build=build, time=time) with open(os.path.join(output_dir, 'buildInformation.m'), 'wb') as f: f.write(populated) @@ -46,7 +46,7 @@ if __name__ == "__main__": """ # parse the input options - import sys, re, os + import sys, re, os, time from argparse import ArgumentParser parser = ArgumentParser() parser.add_argument('--os') diff --git a/modules/matlab/generator/cvmex.py b/modules/matlab/generator/cvmex.py new file mode 100644 index 0000000000..fc47100e17 --- /dev/null +++ b/modules/matlab/generator/cvmex.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +def substitute(cv, output_dir): + + # setup the template engine + template_dir = os.path.join(os.path.dirname(__file__), 'templates') + jtemplate = Environment(loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True) + + # add the filters + jtemplate.filters['cellarray'] = cellarray + jtemplate.filters['split'] = split + jtemplate.filters['csv'] = csv + + # load the template + template = jtemplate.get_template('template_cvmex_base.m') + + # create the build directory + output_dir = output_dir+'/+cv' + if not os.path.isdir(output_dir): + os.mkdir(output_dir) + + # populate template + populated = template.render(cv=cv, time=time) + with open(os.path.join(output_dir, 'mex.m'), 'wb') as f: + f.write(populated) + +if __name__ == "__main__": + """ + Usage: python cvmex.py --opts [-list -of -opts] + --include_dirs [-list -of -opencv_include_directories] + --lib_dir opencv_lib_directory + --libs [-lopencv_core -lopencv_imgproc ...] + --flags [-Wall -opencv_build_flags ...] + --outdir /path/to/generated/output + + cvmex.py generates a custom mex compiler that automatically links OpenCV + libraries to built sources where appropriate. The calling syntax is the + same as the builtin mex compiler, with added cv qualification: + >> cv.mex(..., ...); + """ + + # parse the input options + import sys, re, os, time + from argparse import ArgumentParser + parser = ArgumentParser() + parser.add_argument('--opts') + parser.add_argument('--include_dirs') + parser.add_argument('--lib_dir') + parser.add_argument('--libs') + parser.add_argument('--flags') + parser.add_argument('--outdir') + cv = parser.parse_args() + + from filters import * + from jinja2 import Environment, FileSystemLoader + + # populate the mex base template + substitute(cv, cv.outdir) diff --git a/modules/matlab/generator/filters.py b/modules/matlab/generator/filters.py index e908fe8ff4..f2d59b2b0c 100644 --- a/modules/matlab/generator/filters.py +++ b/modules/matlab/generator/filters.py @@ -139,10 +139,18 @@ def filename(fullpath): ''' return os.path.splitext(os.path.basename(fullpath))[0] +def split(text, delimiter=' '): + '''Split a text string into a list using the specified delimiter''' + return text.split(delimiter) + def csv(items, sep=', '): '''format a list with a separator (comma if not specified)''' return sep.join(item for item in items) +def cellarray(items, escape='\''): + '''format a list of items as a matlab cell array''' + return '{' + ', '.join(escape+item+escape for item in items) + '}' + def stripExtraSpaces(text): '''Removes superfluous whitespace from a string, including the removal of all leading and trailing whitespace''' diff --git a/modules/matlab/generator/templates/template_build_info.m b/modules/matlab/generator/templates/template_build_info.m index 486e0a03d0..1d9124af2f 100644 --- a/modules/matlab/generator/templates/template_build_info.m +++ b/modules/matlab/generator/templates/template_build_info.m @@ -6,6 +6,8 @@ function buildInformation() % run into issues with the Toolbox, it is useful to submit this % information alongside a bug report to the OpenCV team. % +% Copyright {{ time.strftime("%Y", time.localtime()) }} The OpenCV Foundation +% info = { ' ------------------------------------------------------------------------' ' OpenCV Toolbox' diff --git a/modules/matlab/generator/templates/template_cvmex_base.m b/modules/matlab/generator/templates/template_cvmex_base.m new file mode 100644 index 0000000000..8ec6cf5151 --- /dev/null +++ b/modules/matlab/generator/templates/template_cvmex_base.m @@ -0,0 +1,46 @@ +function mex(varargin) +%CV.MEX compile MEX-function with OpenCV linkages +% +% Usage: +% CV.MEX [options ...] file [file file ...] +% +% Description: +% CV.MEX compiles one or more C/C++ source files into a shared-library +% called a mex-file. This function is equivalent to the builtin MEX +% routine, with the notable exception that it automatically resolves +% OpenCV includes, and links in the OpenCV libraries where appropriate. +% It also forwards the flags used to build OpenCV, so architecture- +% specific optimizations can be used. +% +% CV.MEX is designed to be used in situations where the source(s) you +% are compiling contain OpenCV definitions. In such cases, it streamlines +% the finding and including of appropriate OpenCV libraries. +% +% See also: mex +% +% Copyright {{ time.strftime("%Y", time.localtime()) }} The OpenCV Foundation +% + + % forward the OpenCV build flags (C++ only) + EXTRA_FLAGS = ['"CXXFLAGS="\$CXXFLAGS '... + '{{ cv.flags | trim | wordwrap(60, false, '\'...\n \'') }}""']; + + % add the OpenCV include dirs + INCLUDE_DIRS = {{ cv.include_dirs | split | cellarray | wordwrap(60, false, '...\n ') }}; + + % add the lib dir (singular in both build tree and install tree) + LIB_DIR = '{{ cv.lib_dir }}'; + + % add the OpenCV libs. Only the used libs will actually be linked + LIBS = {{ cv.libs | split | cellarray | wordwrap(60, false, '...\n ') }}; + + % add the mex opts (usually at least -largeArrayDims) + OPTS = {{ cv.opts | split | cellarray | wordwrap(60, false, '...\n ') }}; + + % merge all of the default options (EXTRA_FLAGS, LIBS, etc) and the options + % and files passed by the user (varargin) into a single cell array + merged = [ {EXTRA_FLAGS}, INCLUDE_DIRS, {LIB_DIR}, LIBS, OPTS, varargin ]; + + % expand the merged argument list into the builtin mex utility + mex(merged{:}); +end diff --git a/modules/matlab/include/mxarray.hpp b/modules/matlab/include/mxarray.hpp index a2c10199a5..1c34255f99 100644 --- a/modules/matlab/include/mxarray.hpp +++ b/modules/matlab/include/mxarray.hpp @@ -2,9 +2,19 @@ #define OPENCV_MXARRAY_HPP_ #include +#include #include #include +#include +#include #include +#if __cplusplus > 201103 +#include +typedef std::unordered_set StringSet; +#else +#include +typedef std::set StringSet; +#endif #include "mex.h" #include "transpose.hpp" @@ -274,10 +284,10 @@ public: * Explicitly construct a scalar of given type. Since constructors cannot * be explicitly templated, this is a static factory method */ - template - static MxArray Scalar(Scalar value = 0) { - MxArray s(1, 1, 1, Matlab::Traits::ScalarType); - s.real()[0] = value; + template + static MxArray Scalar(ScalarType value = 0) { + MxArray s(1, 1, 1, Matlab::Traits::ScalarType); + s.real()[0] = value; return s; } @@ -410,8 +420,8 @@ public: std::string toString() const { conditionalError(isString(), "Attempted to convert non-string type to string"); - std::string str(size()+1, '\0'); - mxGetString(ptr_, const_cast(str.data()), str.size()); + std::string str(size(), '\0'); + mxGetString(ptr_, const_cast(str.data()), str.size()+1); return str; } @@ -432,6 +442,214 @@ public: }; +/*! @class ArgumentParser + * @brief parses inputs to a method and resolves the argument names. + * + * The ArgumentParser resolves the inputs to a method. It checks that all + * required arguments are specified and also allows named optional arguments. + * For example, the C++ function: + * void randn(Mat& mat, Mat& mean=Mat(), Mat& std=Mat()); + * could be called in Matlab using any of the following signatures: + * \code + * out = randn(in); + * out = randn(in, 0, 1); + * out = randn(in, 'mean', 0, 'std', 1); + * \endcode + * + * ArgumentParser also enables function overloading by allowing users + * to add variants to a method. For example, there may be two C++ sum() methods: + * \code + * double sum(Mat& mat); % sum elements of a matrix + * Mat sum(Mat& A, Mat& B); % add two matrices + * \endcode + * + * by adding two variants to ArgumentParser, the correct underlying sum + * method can be called. If the function call is ambiguous, the + * ArgumentParser will fail with an error message. + * + * The previous example could be parsed as: + * \code + * // set up the Argument parser + * ArgumentParser arguments; + * arguments.addVariant("elementwise", 1); + * arguments.addVariant("matrix", 2); + * + * // parse the arguments + * std::vector inputs; + * inputs = arguments.parse(std::vector(prhs, prhs+nrhs)); + * + * // if we get here, one unique variant is valid + * if (arguments.variantIs("elementwise")) { + * // call elementwise sum() + * } + */ +class ArgumentParser { +private: + struct Variant; + typedef std::string String; + typedef std::vector StringVector; + typedef std::vector MxArrayVector; + typedef std::vector VariantVector; + /* @class Variant + * @brief Describes a variant of arguments to a method + * + * When addVariant() is called on an instance to ArgumentParser, this class + * holds the the information that decribes that variant. The parse() method + * of ArgumentParser then attempts to match a Variant, given a set of + * inputs for a method invocation. + */ + class Variant { + public: + Variant(const String& _name, size_t _nreq, size_t _nopt, const StringVector& _keys) + : name(_name), nreq(_nreq), nopt(_nopt), keys(_keys), using_named(false) {} + String name; + size_t nreq; + size_t nopt; + StringVector keys; + bool using_named; + /*! @brief return true if the named-argument is in the Variant */ + bool count(const String& key) { return std::find(keys.begin(), keys.end(), key) != keys.end(); } + /*! @brief remove a key by index from the Variant */ + void erase(const size_t idx) { keys.erase(keys.begin()+idx); } + /*! @brief remove a key by name from the Variant */ + void erase(const String& key) { keys.erase(std::find(keys.begin(), keys.end(), key)); } + /*! @brief convert a Variant to a string representation */ + String toString(const String& method_name=String("f")) const { + std::ostringstream s; + s << method_name << "("; + for (size_t n = 0; n < nreq; ++n) { + s << "src" << n+1; if (n != nreq-1) s << ", "; + } + if (nreq && nopt) s << ", "; + for (size_t n = 0; n < keys.size(); ++n) { + s << "'" << keys[n] << "', " << keys[n]; + if (n != keys.size()-1) s << ", "; + } + s << ");"; + return s.str(); + } + }; + MxArrayVector filled_; + VariantVector variants_; + String valid_; + String method_name_; +public: + ArgumentParser(const String& method_name) : method_name_(method_name) {} + /*! @brief add a function call variant to the parser + * + * Adds a function-call signature to the parser. The function call *must* be + * unique either in its number of arguments, or in the named-syntax. + * Currently this function does not check whether that invariant stands true. + * + * This function is variadic. If should be called as follows: + * addVariant(2, 2, 'opt_1_name', 'opt_2_name'); + */ + void addVariant(const String& name, size_t nreq, size_t nopt = 0, ...) { + StringVector keys; + va_list opt; + va_start(opt, nopt); + for (size_t n = 0; n < nopt; ++n) keys.push_back(va_arg(opt, const char*)); + addVariant(name, nreq, nopt, keys); + } + void addVariant(const String& name, size_t nreq, size_t nopt, StringVector keys) { + variants_.push_back(Variant(name, nreq, nopt, keys)); + } + /*! @brief check if the valid variant is the key name */ + bool variantIs(const String& name) { + return name.compare(valid_) == 0; + } + /*! @brief parse a vector of input arguments + * + * This method parses a vector of input arguments, attempting to match them + * to a Variant spec. For each input, the method attempts to cull any + * Variants which don't match the given inputs so far. + * + * Once all inputs have been parsed, if there is one unique spec remaining, + * the output MxArray vector gets populated with the arguments, with named + * arguments removed. Any optional arguments that have not been encountered + * are set to an empty array. + * + * If multiple variants or no variants match the given call, an error + * message is emitted + */ + MxArrayVector parse(const MxArrayVector& inputs) { + // allocate the outputs + MxArrayVector outputs; + VariantVector candidates = variants_; + + // iterate over the inputs, attempting to match a variant + for (MxArrayVector::const_iterator input = inputs.begin(); input != inputs.end(); ++input) { + String name = input->isString() ? input->toString() : String(); + for (VariantVector::iterator candidate = candidates.begin(); candidate < candidates.end(); ++candidate) { + // check if the input is a key + bool key = candidate->count(name); + + /* + * FAILURE CASES + * 1. too many inputs, or + * 2. name is not a key and we're expecting a key + * 3. name is a key, and + * we're still expecting required arguments, or + * we're expecting an argument for a previous key + */ + if ((!candidate->nreq && !candidate->nopt) || + (!key && !candidate->nreq && candidate->keys.size() == candidate->nopt && candidate->using_named) || + (key && (candidate->nreq || candidate->keys.size() < candidate->nopt))) { + candidate = candidates.erase(candidate)--; + } + + // VALID CASES + // Still parsing required argments (input is not a key) + else if (!key && candidate->nreq) { + candidate->nreq--; + } + + // Parsing optional arguments and a named argument is encountered + else if (key && !candidate->nreq && candidate->nopt > 0 && candidate->keys.size() == candidate->nopt) { + candidate->erase(name); + candidate->using_named = true; + } + + // Parsing input for a named argument + else if (!key && candidate->keys.size() < candidate->nopt) { + candidate->nopt--; + } + + // Parsing un-named optional arguments + else if (!key && !candidate->nreq && candidate->nopt && candidate->keys.size() && !candidate->using_named) { + candidate->erase(0); + candidate->nopt--; + } + } + } + + // if any candidates remain, check that they have been fully parsed + for (VariantVector::iterator candidate = candidates.begin(); candidate < candidates.end(); ++candidate) { + if (candidate->nreq || candidate->keys.size() < candidate->nopt) { + candidate = candidates.erase(candidate)--; + } + } + + // if there is not a unique candidate, throw an error + String variant_string; + for (VariantVector::iterator variant = variants_.begin(); variant != variants_.end(); ++variant) { + variant_string += "\n" + variant->toString(method_name_); + } + + // if there is not a unique candidate, throw an error + if (candidates.size() > 1) { + error(String("Call to method is ambiguous. Valid variants are:") + .append(variant_string).append("\nUse named arguments to disambiguate call")); + } + if (candidates.size() == 0) { + error(String("No matching method signatures for given arguments. Valid variants are:").append(variant_string)); + } + + return outputs; + } +}; + + /*! * @brief template specialization for inheriting types * diff --git a/modules/matlab/test/cv_exception.cpp b/modules/matlab/test/cv_exception.cpp index 9ae530f2b2..4f63c1e08d 100644 --- a/modules/matlab/test/cv_exception.cpp +++ b/modules/matlab/test/cv_exception.cpp @@ -7,7 +7,7 @@ * Copyright 2013 The OpenCV Foundation */ #include -//#include +#include #include "mex.h" /* @@ -24,9 +24,9 @@ void mexFunction(int nlhs, mxArray* plhs[], // call the opencv function // [out =] namespace.fun(src1, ..., srcn, dst1, ..., dstn, opt1, ..., optn); try { - // throw cv::exception; - //} catch(cv::exception& e) { - // mexErrMsgTxt(e.what()); + throw cv::Exception(-1, "OpenCV exception thrown", __func__, __FILE__, __LINE__); + } catch(cv::Exception& e) { + mexErrMsgTxt(e.what()); } catch(...) { mexErrMsgTxt("Incorrect exception caught!"); }