diff --git a/.gitignore b/.gitignore index 4afe45317a..32d750fcd7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ modules/java/\.idea/ .scannerwork build *.blend1 +**/results/** diff --git a/CMakeLists.txt b/CMakeLists.txt index 452ab4c4aa..433a2e4868 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -272,6 +272,28 @@ if(NOT IOS) include(cmake/VISPDetectPython.cmake) endif() +# --- Python Bindings requirements --- + +# this avoids non-active conda from getting picked anyway on Windows +#set(Python_FIND_REGISTRY LAST) +# Use environment variable PATH to decide preference for Python +#set(Python_FIND_VIRTUALENV FIRST) +#set(Python_FIND_STRATEGY LOCATION) + +#find_package(Python 3.7 COMPONENTS Interpreter Development) # TODO: use visp function to find python? +#if(Python_FOUND) +# set(VISP_PYTHON_BINDINGS_EXECUTABLE "${Python_EXECUTABLE}") +#endif() +#find_package(pybind11) +VP_OPTION(USE_PYBIND11 pybind11 QUIET "Include pybind11 to create Python bindings" "" ON) + +#if(pybind11_FOUND) +# set(VISP_PYBIND11_DIR "${pybind11_DIR}") +#endif() +#message("${pybind11_FOUND}") +# --- + + include_directories(${VISP_INCLUDE_DIR}) #---------------------------------------------------------------------- @@ -413,6 +435,11 @@ VP_OPTION(BUILD_ANDROID_PROJECTS "" "" "Build Android projects providing .apk f VP_OPTION(BUILD_ANDROID_EXAMPLES "" "" "Build examples for Android platform" "" ON IF ANDROID ) VP_OPTION(INSTALL_ANDROID_EXAMPLES "" "" "Install Android examples" "" OFF IF ANDROID ) +# Build python bindings as an option +VP_OPTION(BUILD_PYTHON_BINDINGS "" "" "Build Python bindings" "" ON IF (PYTHON3INTERP_FOUND AND USE_PYBIND11) ) +VP_OPTION(BUILD_PYTHON_BINDINGS_DOC "" "" "Build the documentation for the Python bindings" "" ON IF BUILD_PYTHON_BINDINGS ) + + # Build demos as an option. VP_OPTION(BUILD_DEMOS "" "" "Build ViSP demos" "" ON) # Build tutorials as an option. @@ -730,10 +757,38 @@ if(DOXYGEN_FOUND) set(DOXYGEN_USE_MATHJAX "NO") endif() + # HTML version of the doc + set(DOXYGEN_GENERATE_HTML "YES") + set(DOXYGEN_GENERATE_XML "NO") + set(DOXYGEN_GENERATE_TEST_LIST "YES") + set(DOXYGEN_QUIET "NO") + set(DOXYGEN_INPUTS + "${VISP_SOURCE_DIR}/modules" + "${VISP_SOURCE_DIR}/example" + "${VISP_SOURCE_DIR}/tutorial" + "${VISP_SOURCE_DIR}/demo" + "${VISP_SOURCE_DIR}/doc" + "${VISP_BINARY_DIR}/doc" + "${VISP_CONTRIB_MODULES_PATH}" + ) + string (REPLACE ";" " " DOXYGEN_INPUTS "${DOXYGEN_INPUTS}") configure_file(${VISP_SOURCE_DIR}/doc/config-doxygen.in ${VISP_DOC_DIR}/config-doxygen @ONLY ) + # XML version of the doc + set(DOXYGEN_GENERATE_HTML "NO") + set(DOXYGEN_GENERATE_XML "YES") + set(DOXYGEN_GENERATE_TEST_LIST "NO") + set(DOXYGEN_QUIET "YES") + set(DOXYGEN_INPUTS + "${VISP_SOURCE_DIR}/modules" + ) + string (REPLACE ";" " " DOXYGEN_INPUTS "${DOXYGEN_INPUTS}") + configure_file(${VISP_SOURCE_DIR}/doc/config-doxygen.in + ${VISP_DOC_DIR}/config-doxygen-xml + @ONLY ) + # set vars used in mainpage.dox.in # - VISP_MAINPAGE_EXTENSION set(VISP_MAINPAGE_EXTENSION "") @@ -826,6 +881,8 @@ if(BUILD_JAVA) endif() endif() + + if(ANDROID AND ANDROID_EXECUTABLE AND ANT_EXECUTABLE AND (ANT_VERSION VERSION_GREATER 1.7) AND (ANDROID_TOOLS_Pkg_Revision GREATER 13)) SET(CAN_BUILD_ANDROID_PROJECTS TRUE) else() @@ -1172,6 +1229,14 @@ if(BUILD_TUTORIALS) add_subdirectory(tutorial) vp_add_subdirectories(VISP_CONTRIB_MODULES_PATH tutorial) endif() +if(BUILD_APPS) + vp_add_subdirectories(VISP_CONTRIB_MODULES_PATH apps) +endif() +if(BUILD_PYTHON_BINDINGS) + add_subdirectory(modules/python) +endif() + + # ---------------------------------------------------------------------------- # Make some cmake vars advanced @@ -1442,7 +1507,8 @@ endif() # ========================== java ========================== status("") -status(" Python (for build):" PYTHON_DEFAULT_AVAILABLE THEN "${PYTHON_DEFAULT_EXECUTABLE}" ELSE "no") +status(" Python 3:") +status(" Interpreter:" PYTHON3INTERP_FOUND THEN "${PYTHON3_EXECUTABLE} (ver ${PYTHON3_VERSION_STRING})" ELSE "no") if(BUILD_JAVA OR BUILD_visp_java) status("") @@ -1453,6 +1519,17 @@ if(BUILD_JAVA OR BUILD_visp_java) endif() endif() +# ======================= Python bindings ======================== +status("") +status(" Python3 bindings:" BUILD_PYTHON_BINDINGS THEN "yes" ELSE "no") +if(BUILD_PYTHON_BINDINGS) + status(" Python3 interpreter:" PYTHON3INTERP_FOUND THEN "${PYTHON3_EXECUTABLE} (ver ${PYTHON3_VERSION_STRING})" ELSE "no") + status(" Pybind11:" USE_PYBIND11 THEN "${pybind11_DIR} (${pybind11_VERSION})" ELSE "no") + status(" Package version:" "${VISP_PYTHON_PACKAGE_VERSION}") + status(" Wrapped modules:" "${VISP_PYTHON_BOUND_MODULES}") + status(" Generated input config:" "${VISP_PYTHON_GENERATED_CONFIG_FILE}") +endif() + # ============================ Options =========================== status("") status(" Build options: ") diff --git a/cmake/VISPDetectPython.cmake b/cmake/VISPDetectPython.cmake index ee0489fee9..7601715c3c 100644 --- a/cmake/VISPDetectPython.cmake +++ b/cmake/VISPDetectPython.cmake @@ -85,11 +85,7 @@ if(NOT ${found}) endif() vp_clear_vars(PYTHONINTERP_FOUND PYTHON_EXECUTABLE PYTHON_VERSION_STRING PYTHON_VERSION_MAJOR PYTHON_VERSION_MINOR PYTHON_VERSION_PATCH) if(NOT CMAKE_VERSION VERSION_LESS "3.12") - if(_python_version_major STREQUAL "2") - set(__PYTHON_PREFIX Python2) - else() - set(__PYTHON_PREFIX Python3) - endif() + set(__PYTHON_PREFIX Python3) find_host_package(${__PYTHON_PREFIX} "${preferred_version}" COMPONENTS Interpreter) if(${__PYTHON_PREFIX}_EXECUTABLE) set(PYTHON_EXECUTABLE "${${__PYTHON_PREFIX}_EXECUTABLE}") @@ -208,9 +204,6 @@ if(NOT ${found}) if(CMAKE_CROSSCOMPILING) message(STATUS "Cannot probe for Python/Numpy support (because we are cross-compiling ViSP)") message(STATUS "If you want to enable Python/Numpy support, set the following variables:") - message(STATUS " PYTHON2_INCLUDE_PATH") - message(STATUS " PYTHON2_LIBRARIES (optional on Unix-like systems)") - message(STATUS " PYTHON2_NUMPY_INCLUDE_DIRS") message(STATUS " PYTHON3_INCLUDE_PATH") message(STATUS " PYTHON3_LIBRARIES (optional on Unix-like systems)") message(STATUS " PYTHON3_NUMPY_INCLUDE_DIRS") @@ -258,7 +251,7 @@ if(NOT ${found}) set(${include_path} "${_include_path}" CACHE INTERNAL "") set(${include_dir} "${_include_dir}" CACHE PATH "Python include dir") set(${include_dir2} "${_include_dir2}" CACHE PATH "Python include dir 2") - set(${packages_path} "${_packages_path}" CACHE PATH "Where to install the python packages.") + set(${packages_path} "${_packages_path}" CACHE STRING "Where to install the python packages.") set(${numpy_include_dirs} ${_numpy_include_dirs} CACHE PATH "Path to numpy headers") set(${numpy_version} "${_numpy_version}" CACHE INTERNAL "") endif() @@ -268,14 +261,6 @@ if(VISP_PYTHON_SKIP_DETECTION) return() endif() -find_python("" "${MIN_VER_PYTHON2}" PYTHON2_LIBRARY PYTHON2_INCLUDE_DIR - PYTHON2INTERP_FOUND PYTHON2_EXECUTABLE PYTHON2_VERSION_STRING - PYTHON2_VERSION_MAJOR PYTHON2_VERSION_MINOR PYTHON2LIBS_FOUND - PYTHON2LIBS_VERSION_STRING PYTHON2_LIBRARIES PYTHON2_LIBRARY - PYTHON2_DEBUG_LIBRARIES PYTHON2_LIBRARY_DEBUG PYTHON2_INCLUDE_PATH - PYTHON2_INCLUDE_DIR PYTHON2_INCLUDE_DIR2 PYTHON2_PACKAGES_PATH - PYTHON2_NUMPY_INCLUDE_DIRS PYTHON2_NUMPY_VERSION) - option(VISP_PYTHON3_VERSION "Python3 version" "") find_python("${VISP_PYTHON3_VERSION}" "${MIN_VER_PYTHON3}" PYTHON3_LIBRARY PYTHON3_INCLUDE_DIR PYTHON3INTERP_FOUND PYTHON3_EXECUTABLE PYTHON3_VERSION_STRING @@ -285,31 +270,16 @@ find_python("${VISP_PYTHON3_VERSION}" "${MIN_VER_PYTHON3}" PYTHON3_LIBRARY PYTHO PYTHON3_INCLUDE_DIR PYTHON3_INCLUDE_DIR2 PYTHON3_PACKAGES_PATH PYTHON3_NUMPY_INCLUDE_DIRS PYTHON3_NUMPY_VERSION) -mark_as_advanced(PYTHON2_LIBRARY PYTHON2_INCLUDE_DIR - PYTHON2INTERP_FOUND PYTHON2_EXECUTABLE PYTHON2_VERSION_STRING - PYTHON2_VERSION_MAJOR PYTHON2_VERSION_MINOR PYTHON2LIBS_FOUND - PYTHON2LIBS_VERSION_STRING PYTHON2_LIBRARIES PYTHON2_LIBRARY - PYTHON2_DEBUG_LIBRARIES PYTHON2_LIBRARY_DEBUG PYTHON2_INCLUDE_PATH - PYTHON2_INCLUDE_DIR PYTHON2_INCLUDE_DIR2 PYTHON2_PACKAGES_PATH - PYTHON2_NUMPY_INCLUDE_DIRS PYTHON2_NUMPY_VERSION) - -mark_as_advanced(PYTHON3_LIBRARY PYTHON3_INCLUDE_DIR - PYTHON3INTERP_FOUND PYTHON3_EXECUTABLE PYTHON3_VERSION_STRING - PYTHON3_VERSION_MAJOR PYTHON3_VERSION_MINOR PYTHON3LIBS_FOUND - PYTHON3LIBS_VERSION_STRING PYTHON3_LIBRARIES PYTHON3_LIBRARY - PYTHON3_DEBUG_LIBRARIES PYTHON3_LIBRARY_DEBUG PYTHON3_INCLUDE_PATH - PYTHON3_INCLUDE_DIR PYTHON3_INCLUDE_DIR2 PYTHON3_PACKAGES_PATH - PYTHON3_NUMPY_INCLUDE_DIRS PYTHON3_NUMPY_VERSION - VISP_PYTHON3_VERSION) - if(PYTHON_DEFAULT_EXECUTABLE) set(PYTHON_DEFAULT_AVAILABLE "TRUE") -elseif(PYTHON2_EXECUTABLE AND PYTHON2INTERP_FOUND) - # Use Python 2 as default Python interpreter - set(PYTHON_DEFAULT_AVAILABLE "TRUE") - set(PYTHON_DEFAULT_EXECUTABLE "${PYTHON2_EXECUTABLE}") elseif(PYTHON3_EXECUTABLE AND PYTHON3INTERP_FOUND) - # Use Python 3 as fallback Python interpreter (if there is no Python 2) set(PYTHON_DEFAULT_AVAILABLE "TRUE") set(PYTHON_DEFAULT_EXECUTABLE "${PYTHON3_EXECUTABLE}") endif() + +if(PYTHON_DEFAULT_AVAILABLE) + execute_process(COMMAND ${PYTHON_DEFAULT_EXECUTABLE} --version + OUTPUT_VARIABLE PYTHON_DEFAULT_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + string(REGEX MATCH "[0-9]+.[0-9]+.[0-9]+" PYTHON_DEFAULT_VERSION "${PYTHON_DEFAULT_VERSION}") +endif() diff --git a/cmake/VISPExtraTargets.cmake b/cmake/VISPExtraTargets.cmake index 19d4ffb6c1..0b30a5267c 100644 --- a/cmake/VISPExtraTargets.cmake +++ b/cmake/VISPExtraTargets.cmake @@ -57,17 +57,25 @@ if(DOXYGEN_FOUND) COMMAND "${DOXYGEN_EXECUTABLE}" "${VISP_DOC_DIR}/config-doxygen" DEPENDS "${VISP_DOC_DIR}/config-doxygen" ) + add_custom_target(visp_doc_xml + COMMAND "${DOXYGEN_EXECUTABLE}" "${VISP_DOC_DIR}/config-doxygen-xml" + DEPENDS "${VISP_DOC_DIR}/config-doxygen-xml" + ) if(CMAKE_GENERATOR MATCHES "Xcode") add_dependencies(visp_doc man) # developer_scripts not available when Xcode + add_dependencies(visp_doc man) elseif(UNIX AND NOT ANDROID) # man target available only on unix add_dependencies(visp_doc man developer_scripts) + add_dependencies(visp_doc_xml man developer_scripts) elseif(NOT(MINGW OR IOS)) add_dependencies(visp_doc developer_scripts) + add_dependencies(visp_doc_xml developer_scripts) endif() if(ENABLE_SOLUTION_FOLDERS) set_target_properties(visp_doc PROPERTIES FOLDER "extra") + set_target_properties(visp_doc_xml PROPERTIES FOLDER "extra") set_target_properties(html-doc PROPERTIES FOLDER "extra") endif() endif() diff --git a/doc/config-doxygen.in b/doc/config-doxygen.in index f55c416da7..ab7ac43a11 100644 --- a/doc/config-doxygen.in +++ b/doc/config-doxygen.in @@ -1,4 +1,4 @@ -# Doxyfile 1.8.17 +# Doxyfile 1.9.8 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -12,6 +12,16 @@ # For lists, items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] #--------------------------------------------------------------------------- # Project related configuration options @@ -51,7 +61,7 @@ PROJECT_BRIEF = # pixels and the maximum width should not exceed 200 pixels. Doxygen will copy # the logo to the output directory. -PROJECT_LOGO = "@VISP_SOURCE_DIR@/doc/image/logo/img-logo-visp.png" +PROJECT_LOGO = @VISP_SOURCE_DIR@/doc/image/logo/img-logo-visp.png # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path # into which the generated documentation will be written. If a relative path is @@ -60,16 +70,28 @@ PROJECT_LOGO = "@VISP_SOURCE_DIR@/doc/image/logo/img-logo-visp.png" OUTPUT_DIRECTORY = doc -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where # putting all generated files in the same directory would otherwise causes -# performance problems for the file system. +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. # The default value is: NO. CREATE_SUBDIRS = NO +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# number of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + # If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII # characters to appear in the names of generated files. If set to NO, non-ASCII # characters will be escaped, for example _xE3_x81_x84 will be used for Unicode @@ -81,26 +103,18 @@ ALLOW_UNICODE_NAMES = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. # The default value is: English. OUTPUT_LANGUAGE = English -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - # If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. @@ -171,7 +185,6 @@ STRIP_FROM_PATH = STRIP_FROM_INC_PATH = @DOXYGEN_STRIP_FROM_INC_PATH@ - # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't # support long names like on DOS, Mac, or CD-ROM. @@ -218,6 +231,14 @@ QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. @@ -241,25 +262,19 @@ TAB_SIZE = 4 # the documentation. An alias has the form: # name=value # For example adding -# "sideeffect=@par Side Effects:\n" +# "sideeffect=@par Side Effects:^^" # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all @@ -301,18 +316,21 @@ OPTIMIZE_OUTPUT_SLICE = NO # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and # language is one of the parsers supported by doxygen: IDL, Java, JavaScript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: # FortranFree, unknown formatted Fortran: Fortran. In the later case the parser # tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is -# Fortran), use: inc=Fortran f=C. +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. # # Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. EXTENSION_MAPPING = json=JavaScript @@ -335,6 +353,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 0 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -446,6 +475,27 @@ TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 1 +# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -509,6 +559,13 @@ EXTRACT_LOCAL_METHODS = NO EXTRACT_ANON_NSPACES = NO +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + # If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all # undocumented members inside documented classes or files. If set to NO these # members will be included in the various overviews, but no documentation @@ -520,7 +577,8 @@ HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set # to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. +# will also hide undocumented C++ concepts if enabled. This option has no effect +# if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = NO @@ -546,12 +604,20 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# (including Cygwin) ands Mac users are advised to set this option to NO. -# The default value is: system dependent. +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# Possible values are: SYSTEM, NO and YES. +# The default value is: SYSTEM. CASE_SENSE_NAMES = YES @@ -569,6 +635,12 @@ HIDE_SCOPE_NAMES = NO HIDE_COMPOUND_REFERENCE= NO +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -658,7 +730,7 @@ GENERATE_TODOLIST = YES # list. This list is created by putting \test commands in the documentation. # The default value is: YES. -GENERATE_TESTLIST = YES +GENERATE_TESTLIST = @DOXYGEN_GENERATE_TEST_LIST@ # The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug # list. This list is created by putting \bug commands in the documentation. @@ -726,7 +798,8 @@ FILE_VERSION_FILTER = # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. You can # optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. # # Note that if you run doxygen from a directory containing a file called # DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE @@ -753,7 +826,7 @@ CITE_BIB_FILES = @DOXYGEN_CITE_BIB_FILES@ # messages are off. # The default value is: NO. -QUIET = NO +QUIET = @DOXYGEN_QUIET@ # The WARNINGS tag can be used to turn on/off the warning messages that are # generated to standard error (stderr) by doxygen. If WARNINGS is set to YES @@ -772,24 +845,50 @@ WARNINGS = YES WARN_IF_UNDOCUMENTED = YES # If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. # The default value is: YES. WARN_IF_DOC_ERROR = YES +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC # The default value is: NO. WARN_NO_PARAMDOC = NO +# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about +# undocumented enumeration values. If set to NO, doxygen will accept +# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: NO. + +WARN_IF_UNDOC_ENUM_VAL = NO + # If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -800,13 +899,27 @@ WARN_AS_ERROR = NO # and the warning text. Optionally the format may contain $version, which will # be replaced by the version of the file (if it could be obtained via # FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT # The default value is: $file:$line: $text. WARN_FORMAT = "$file:$line: $text" +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + # The WARN_LOGFILE tag can be used to specify a file to which warning and error # messages should be written. If left blank the output is written to standard -# error (stderr). +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). WARN_LOGFILE = warning.log @@ -820,23 +933,28 @@ WARN_LOGFILE = warning.log # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = "@VISP_SOURCE_DIR@/modules" \ - "@VISP_SOURCE_DIR@/example" \ - "@VISP_SOURCE_DIR@/tutorial" \ - "@VISP_SOURCE_DIR@/demo" \ - "@VISP_SOURCE_DIR@/doc" \ - "@VISP_BINARY_DIR@/doc" \ - "@VISP_CONTRIB_MODULES_PATH@" +INPUT = @DOXYGEN_INPUTS@ # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: https://www.gnu.org/software/libiconv/) for the list of -# possible encodings. +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# See also: INPUT_FILE_ENCODING # The default value is: UTF-8. INPUT_ENCODING = UTF-8 +# This tag can be used to specify the character encoding of the source files +# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify +# character encoding on a per file pattern basis. Doxygen will compare the file +# name with each pattern and apply the encoding instead of the default +# INPUT_ENCODING) if there is a match. The character encodings are a list of the +# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding +# "INPUT_ENCODING" for further information on supported encodings. + +INPUT_FILE_ENCODING = + # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and # *.h) to filter out the source-files in the directories. @@ -845,13 +963,15 @@ INPUT_ENCODING = UTF-8 # need to set EXTENSION_MAPPING for the extension otherwise the files are not # read by doxygen. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), -# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen -# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f, *.for, *.tcl, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, +# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.h \ *.cpp \ @@ -901,10 +1021,7 @@ EXCLUDE_PATTERNS = *_impl.h \ # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* +# ANamespace::AClass, ANamespace::*Test EXCLUDE_SYMBOLS = @@ -916,8 +1033,7 @@ EXAMPLE_PATH = "@VISP_SOURCE_DIR@/example" \ "@VISP_SOURCE_DIR@/tutorial" \ "@VISP_SOURCE_DIR@/demo" \ "@VISP_SOURCE_DIR@/modules" \ - "@VISP_SOURCE_DIR@/script" \ - + "@VISP_SOURCE_DIR@/script" # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and @@ -954,6 +1070,11 @@ IMAGE_PATH = @DOXYGEN_IMAGE_PATH@ # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. # +# Note that doxygen will use the data processed and written to standard output +# for further processing, therefore nothing else, like debug statements or used +# commands (so in case of a Windows batch file always use @echo OFF), should be +# written to standard output. +# # Note that for custom extensions or not directly supported extensions you also # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by doxygen. @@ -995,6 +1116,15 @@ FILTER_SOURCE_PATTERNS = USE_MDFILE_AS_MAINPAGE = +# The Fortran standard specifies that for fixed formatted Fortran code all +# characters from position 72 are to be considered as comment. A common +# extension is to allow longer lines before the automatic comment starts. The +# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can +# be processed before the automatic comment starts. +# Minimum value: 7, maximum value: 10000, default value: 72. + +FORTRAN_COMMENT_AFTER = 72 + #--------------------------------------------------------------------------- # Configuration options related to source browsing #--------------------------------------------------------------------------- @@ -1092,17 +1222,11 @@ VERBATIM_HEADERS = YES ALPHABETICAL_INDEX = YES -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 4 - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. +# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes) +# that should be ignored while generating the index headers. The IGNORE_PREFIX +# tag works for classes, function and member names. The entity will be placed in +# the alphabetical list under the first letter of the entity name that remains +# after removing the prefix. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. IGNORE_PREFIX = vp @@ -1114,7 +1238,7 @@ IGNORE_PREFIX = vp # If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. -GENERATE_HTML = YES +GENERATE_HTML = @DOXYGEN_GENERATE_HTML@ # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -1181,7 +1305,12 @@ HTML_STYLESHEET = # Doxygen will copy the style sheet files to the output directory. # Note: The order of the extra style sheet files is of importance (e.g. the last # style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. +# list). +# Note: Since the styling of scrollbars can currently not be overruled in +# Webkit/Chromium, the styling will be left out of the default doxygen.css if +# one or more extra stylesheets have been specified. So if scrollbar +# customization is desired it has to be added explicitly. For an example see the +# documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = @@ -1196,9 +1325,22 @@ HTML_EXTRA_STYLESHEET = HTML_EXTRA_FILES = +# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output +# should be rendered with a dark or light theme. +# Possible values are: LIGHT always generate light mode output, DARK always +# generate dark mode output, AUTO_LIGHT automatically set the mode according to +# the user preference, use light mode if no preference is set (the default), +# AUTO_DARK automatically set the mode according to the user preference, use +# dark mode if no preference is set and TOGGLE allow to user to switch between +# light and dark mode via a button. +# The default value is: AUTO_LIGHT. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE = AUTO_LIGHT + # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen # will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see +# this color. Hue is specified as an angle on a color-wheel, see # https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. @@ -1208,7 +1350,7 @@ HTML_EXTRA_FILES = HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A +# in the HTML output. For a value of 0 the output will use gray-scales only. A # value of 255 will produce the most vivid colors. # Minimum value: 0, maximum value: 255, default value: 100. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1226,15 +1368,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1254,6 +1387,13 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = YES +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1269,10 +1409,11 @@ HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: https://developer.apple.com/xcode/), introduced with OSX -# 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at # startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy # genXcode/_index.html for more information. @@ -1289,6 +1430,13 @@ GENERATE_DOCSET = NO DOCSET_FEEDNAME = "Doxygen generated docs" +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + # This tag specifies a string that should uniquely identify the documentation # set bundle. This should be a reverse domain-name style string, e.g. # com.mycompany.MyDocSet. Doxygen will append .docset to the name. @@ -1314,8 +1462,12 @@ DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). # # The HTML Help Workshop contains a compiler that can convert all HTML output # generated by doxygen into a single compiled HTML file (.chm). Compiled HTML @@ -1345,7 +1497,7 @@ CHM_FILE = HHC_LOCATION = # The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). +# (YES) or that it should be included in the main .chm file (NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. @@ -1372,6 +1524,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -1390,7 +1552,8 @@ QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace -# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1398,8 +1561,8 @@ QHP_NAMESPACE = # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- -# folders). +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1407,16 +1570,16 @@ QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom -# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_CUST_FILTER_ATTRS = @@ -1428,9 +1591,9 @@ QHP_CUST_FILTER_ATTRS = QHP_SECT_FILTER_ATTRS = -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. # This tag requires that the tag GENERATE_QHP is set to YES. QHG_LOCATION = @@ -1473,16 +1636,28 @@ DISABLE_INDEX = NO # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the # HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. GENERATE_TREEVIEW = YES +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. # @@ -1507,6 +1682,24 @@ TREEVIEW_WIDTH = 250 EXT_LINKS_IN_WINDOW = NO +# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful # doxygen run you need to manually remove any form_*.png images from the HTML @@ -1516,17 +1709,6 @@ EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - # The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands # to create new LaTeX commands to be used in formulas as building blocks. See # the section "Including formulas" for details. @@ -1544,11 +1726,29 @@ FORMULA_MACROFILE = USE_MATHJAX = @DOXYGEN_USE_MATHJAX@ +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + # When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). # Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. # The default value is: HTML-CSS. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1561,22 +1761,29 @@ MATHJAX_FORMAT = HTML-CSS # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): # MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams # This tag requires that the tag USE_MATHJAX is set to YES. MATHJAX_EXTENSIONS = # The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces # of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an # example see the documentation. # This tag requires that the tag USE_MATHJAX is set to YES. @@ -1623,7 +1830,8 @@ SERVER_BASED_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). +# Xapian (see: +# https://xapian.org/). # # See the section "External Indexing and Searching" for details. # The default value is: NO. @@ -1636,8 +1844,9 @@ EXTERNAL_SEARCH = NO # # Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). See the section "External Indexing and -# Searching" for details. +# Xapian (see: +# https://xapian.org/). See the section "External Indexing and Searching" for +# details. # This tag requires that the tag SEARCHENGINE is set to YES. SEARCHENGINE_URL = @@ -1695,7 +1904,7 @@ LATEX_OUTPUT = latex # the output language. # This tag requires that the tag GENERATE_LATEX is set to YES. -LATEX_CMD_NAME = "@LATEX_COMPILER@" +LATEX_CMD_NAME = @LATEX_COMPILER@ # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate # index for LaTeX. @@ -1705,7 +1914,7 @@ LATEX_CMD_NAME = "@LATEX_COMPILER@" # The default file is: makeindex. # This tag requires that the tag GENERATE_LATEX is set to YES. -MAKEINDEX_CMD_NAME = "@MAKEINDEX_COMPILER@" +MAKEINDEX_CMD_NAME = @MAKEINDEX_COMPILER@ # The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to # generate index for LaTeX. In case there is no backslash (\) as first character @@ -1732,7 +1941,7 @@ COMPACT_LATEX = NO # The default value is: a4. # This tag requires that the tag GENERATE_LATEX is set to YES. -PAPER_TYPE = a4wide +PAPER_TYPE = a4 # The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names # that should be included in the LaTeX output. The package can be specified just @@ -1748,29 +1957,31 @@ EXTRA_PACKAGES = amsmath \ xr \ amsfonts -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -# generated LaTeX document. The header should contain everything until the first -# chapter. If it is left blank doxygen will generate a standard header. See -# section "Doxygen usage" for information on how to let doxygen write the -# default header to a separate file. +# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for +# the generated LaTeX document. The header should contain everything until the +# first chapter. If it is left blank doxygen will generate a standard header. It +# is highly recommended to start with a default header using +# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty +# and then modify the file new_header.tex. See also section "Doxygen usage" for +# information on how to generate the default header that doxygen normally uses. # -# Note: Only use a user-defined header if you know what you are doing! The -# following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -# string, for the replacement values of the other commands the user is referred -# to HTML_HEADER. +# Note: Only use a user-defined header if you know what you are doing! +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. The following +# commands have a special meaning inside the header (and footer): For a +# description of the possible markers and block names see the documentation. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HEADER = -# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -# generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. See +# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for +# the generated LaTeX document. The footer should contain everything after the +# last chapter. If it is left blank doxygen will generate a standard footer. See # LATEX_HEADER for more information on how to generate a default footer and what -# special commands can be used inside the footer. -# -# Note: Only use a user-defined footer if you know what you are doing! +# special commands can be used inside the footer. See also section "Doxygen +# usage" for information on how to generate the default footer that doxygen +# normally uses. Note: Only use a user-defined footer if you know what you are +# doing! # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_FOOTER = @@ -1803,18 +2014,26 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = NO -# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES, to get a -# higher quality PDF documentation. +# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. # The default value is: YES. # This tag requires that the tag GENERATE_LATEX is set to YES. USE_PDFLATEX = NO -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. This option is also used -# when generating formulas in HTML. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -1827,16 +2046,6 @@ LATEX_BATCHMODE = NO LATEX_HIDE_INDICES = NO -# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -# code with syntax highlighting in the LaTeX output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_SOURCE_CODE = NO - # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See # https://en.wikipedia.org/wiki/BibTeX and \cite for more info. @@ -1845,14 +2054,6 @@ LATEX_SOURCE_CODE = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -1917,16 +2118,6 @@ RTF_STYLESHEET_FILE = RTF_EXTENSIONS_FILE = -# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -# with syntax highlighting in the RTF output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_SOURCE_CODE = NO - #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- @@ -1979,7 +2170,7 @@ MAN_LINKS = NO # captures the structure of the code including all documentation. # The default value is: NO. -GENERATE_XML = NO +GENERATE_XML = @DOXYGEN_GENERATE_XML@ # The XML_OUTPUT tag is used to specify where the XML pages will be put. If a # relative path is entered the value of OUTPUT_DIRECTORY will be put in front of @@ -1996,7 +2187,7 @@ XML_OUTPUT = xml # The default value is: YES. # This tag requires that the tag GENERATE_XML is set to YES. -XML_PROGRAMLISTING = YES +XML_PROGRAMLISTING = NO # If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include # namespace members in file scope as well, matching the HTML output. @@ -2023,27 +2214,44 @@ GENERATE_DOCBOOK = NO DOCBOOK_OUTPUT = docbook -# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -# program listings (including syntax highlighting and cross-referencing -# information) to the DOCBOOK output. Note that enabling this will significantly -# increase the size of the DOCBOOK output. -# The default value is: NO. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. - -DOCBOOK_PROGRAMLISTING = NO - #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. GENERATE_AUTOGEN_DEF = NO +#--------------------------------------------------------------------------- +# Configuration options related to Sqlite3 output +#--------------------------------------------------------------------------- + +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if an a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2118,7 +2326,8 @@ SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by the -# preprocessor. +# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of +# RECURSIVE has no effect here. # This tag requires that the tag SEARCH_INCLUDES is set to YES. INCLUDE_PATH = "@VISP_BINARY_DIR@/modules/core" @@ -2303,15 +2512,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2325,25 +2534,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -# NO turns the diagrams off. Note that this option also works with HAVE_DOT -# disabled, but it is recommended to install and use dot, since it yields more -# powerful graphs. -# The default value is: YES. - -CLASS_DIAGRAMS = YES - -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2352,7 +2545,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2369,49 +2562,73 @@ HAVE_DOT = NO DOT_NUM_THREADS = 0 -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. +# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of +# subgraphs. When you want a differently looking font in the dot files that +# doxygen generates you can specify fontname, fontcolor and fontsize attributes. +# For details please see Node, +# Edge and Graph Attributes specification You need to make sure dot is able +# to find the font, which can be done by putting it in a standard location or by +# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. Default graphviz fontsize is 14. +# The default value is: fontname=Helvetica,fontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTNAME = +DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10" -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. +# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can +# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. Complete documentation about +# arrows shapes. +# The default value is: labelfontname=Helvetica,labelfontsize=10. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_FONTSIZE = 10 +DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10" -# By default doxygen will tell dot to use the default font as specified with -# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -# the path where dot can find it using this tag. +# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes +# around nodes set 'shape=plain' or 'shape=plaintext' Shapes specification +# The default value is: shape=box,height=0.2,width=0.4. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" + +# You can set the path where dot can find font specified with fontname in +# DOT_COMMON_ATTR and others dot attributes. # This tag requires that the tag HAVE_DOT is set to YES. DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -# each documented class showing the direct and indirect inheritance relations. -# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2434,10 +2651,32 @@ UML_LOOK = NO # but if the number exceeds 15, the total amount of fields shown is limited to # 10. # Minimum value: 0, maximum value: 100, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. +# This tag requires that the tag UML_LOOK is set to YES. UML_LIMIT_NUM_FIELDS = 10 +# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and +# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS +# tag is set to YES, doxygen will add type and arguments for attributes and +# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen +# will not generate fields with class member information in the UML graphs. The +# class diagrams will look similar to the default class diagrams but using UML +# notation for the relationships. +# Possible values are: NO, YES and NONE. +# The default value is: NO. +# This tag requires that the tag UML_LOOK is set to YES. + +DOT_UML_DETAILS = NO + +# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters +# to display on a single line. If the actual line length exceeds this threshold +# significantly it will wrapped across multiple lines. Some heuristics are apply +# to avoid ugly line breaks. +# Minimum value: 0, maximum value: 1000, default value: 17. +# This tag requires that the tag HAVE_DOT is set to YES. + +DOT_WRAP_THRESHOLD = 17 + # If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and # collaboration graphs will show the relations between templates and their # instances. @@ -2449,7 +2688,9 @@ TEMPLATE_RELATIONS = YES # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2458,7 +2699,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2498,16 +2742,26 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. DIRECTORY_GRAPH = YES +# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels +# of child directories generated in directory dependency graphs by dot. +# Minimum value: 1, maximum value: 25, default value: 1. +# This tag requires that the tag DIRECTORY_GRAPH is set to YES. + +DIR_GRAPH_MAX_DEPTH = 1 + # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2535,7 +2789,7 @@ INTERACTIVE_SVG = NO # found. If left blank, it is assumed the dot tool can be found in the path. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_PATH = "@DOXYGEN_DOT_EXECUTABLE_PATH@" +DOT_PATH = @DOXYGEN_DOT_EXECUTABLE_PATH@ # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the \dotfile @@ -2544,11 +2798,12 @@ DOT_PATH = "@DOXYGEN_DOT_EXECUTABLE_PATH@" DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2557,10 +2812,10 @@ MSCFILE_DIRS = DIAFILE_DIRS = # When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -# path where java can find the plantuml.jar file. If left blank, it is assumed -# PlantUML is not used or called during a preprocessing step. Doxygen will -# generate a warning when it encounters a \startuml command in this case and -# will not generate output for the diagram. +# path where java can find the plantuml.jar file or to the filename of jar file +# to be used. If left blank, it is assumed PlantUML is not used or called during +# a preprocessing step. Doxygen will generate a warning when it encounters a +# \startuml command in this case and will not generate output for the diagram. PLANTUML_JAR_PATH = @@ -2598,18 +2853,6 @@ DOT_GRAPH_MAX_NODES = 150 MAX_DOT_GRAPH_DEPTH = 0 -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = NO - # Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support @@ -2622,14 +2865,34 @@ DOT_MULTI_TARGETS = YES # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated # graphs. +# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal +# graphical representation for inheritance and collaboration diagrams is used. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate # files that are used to generate the various graphs. +# +# Note: This setting is not only used for dot files but also for msc temporary +# files. # The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/doc/tutorial/misc/tutorial-synthetic-blenderproc.dox b/doc/tutorial/misc/tutorial-synthetic-blenderproc.dox index 46e7b2ca49..91f04a807a 100644 --- a/doc/tutorial/misc/tutorial-synthetic-blenderproc.dox +++ b/doc/tutorial/misc/tutorial-synthetic-blenderproc.dox @@ -5,21 +5,29 @@ \section dnn_synthetic_intro Introduction -In this tutorial, we will show how to generate synthetic data that can be used to train a neural network, thanks to blenderproc. +In this tutorial, we will show how to generate synthetic data that can be used to train a neural network, thanks to +blenderproc. -Most of the (manual) work when training a neural network resides in acquiring and labelling data. This process can be slow, tedious and error prone. -A solution to avoid this step is to use synthetic data, generated by a simulator/computer program. This approach comes with multiple advantages: +Most of the (manual) work when training a neural network resides in acquiring and labelling data. This process can be +slow, tedious and error prone. +A solution to avoid this step is to use synthetic data, generated by a simulator/computer program. This approach comes +with multiple advantages: - Data acquisition is fast - It is easy to acquire accurate ground truth labels - Variations in the training data can be easily added There are however, some drawbacks: -- More knowledge of the scene is required: in the case of detection, we require a 3D model of the object, which is not the case for true images -- A difference between simulated and real data can be apparent and negatively impact network performance (this is called the Sim2Real gap) +- More knowledge of the scene is required: in the case of detection, we require a 3D model of the object, which is not + the case for true images +- A difference between simulated and real data can be apparent and negatively impact network performance (this is + called the Sim2Real gap) -The latter point is heavily dependent on the quality of the generated images and the more realistic the images, the better the expected results. +The latter point is heavily dependent on the quality of the generated images and the more realistic the images, the +better the expected results. -Blender, using ray tracing, can generate realistic images. To perform data generation, Blenderproc has been developed and is an extremely useful and flexible tool to generate realistic scenes from Python code. +Blender, using ray tracing, can generate realistic images. To perform data generation, +Blenderproc has been developed and is an extremely useful and +flexible tool to generate realistic scenes from Python code. Along with RGB images, Blenderproc can generate different labels or inputs: - Depth map @@ -29,13 +37,17 @@ Along with RGB images, Blenderproc can generate different labels or inputs: - Bounding box - Optical flow (not provided in our generation script) -In this tutorial, we will install blenderproc and use it to generate simple but varied scenes containing objects of interest. +In this tutorial, we will install blenderproc and use it to generate simple but varied scenes containing objects of +interest. We provide a simple, object-centric generation script that should suffice in many cases. -However, since Blenderproc is easy to use, with many examples included in the documentation, readapting this script to your needs should be easy. +However, since Blenderproc is easy to use, with many examples included in the +documentation, readapting this script to your needs +should be easy. \section dnn_synthetic_install Requirements -First, you should start by installing blenderproc. First, start by creating a new conda environment to avoid potential conflicts with other Python packages. +First, you should start by installing blenderproc. First, start by creating a new conda environment to avoid potential +conflicts with other Python packages. \code{.sh} $ conda create --name blenderproc python=3.10 pip $ conda activate blenderproc @@ -47,7 +59,8 @@ You can then run the Blenderproc sample example with: \code{.sh} (blenderproc) $ blenderproc quickstart \endcode -This may take some time, as Blenderproc downloads its own version of Blender and sets up its own environment. This setup will only be performed once. +This may take some time, as Blenderproc downloads its own version of Blender and sets up its own environment. +This setup will only be performed once. Once Blenderproc is done, you can check its output with: \code{.sh} @@ -55,24 +68,31 @@ Once Blenderproc is done, you can check its output with: \endcode -Blenderproc stores its output in HDF5 file format. Each HDF5 **may** contain the RGB image, along with depth, normals, and other modalities. +Blenderproc stores its output in HDF5 file format. Each HDF5 **may** contain the RGB image, along with depth, normals, +and other modalities. -For the simulator to provide useful data, we should obtain a set of realistic textures (thus helping close the Sim2Real gap). -Thankfully, Blenderproc provides a helpful script to download a dataset of materials from cc0textures.com, containing more than 1500 high resolution materials. +For the simulator to provide useful data, we should obtain a set of realistic textures +(thus helping close the Sim2Real gap). +Thankfully, Blenderproc provides a helpful script to download a dataset of materials from cc0textures.com, +containing more than 1500 high resolution materials. To download the materials, run \code{.sh} (blenderproc) $ blenderproc download cc_textures path/to/folder/where/to/save/materials \endcode -\warning Because the materials are in high definition, downloading the full dataset may take a large amount of disk space (30+ GB). If this is too much for you, you can safely delete some of the materials or stop the script after it has acquired enough materials. While using a small number of materials can be useful when performing quick tests, using the full set should be preferred as variety helps when transferring your deep learning model to real world data. +\warning Because the materials are in high definition, downloading the full dataset may take a large amount of disk +space (30+ GB). If this is too much for you, you can safely delete some of the materials or stop the script after it +has acquired enough materials. While using a small number of materials can be useful when performing quick tests, +using the full set should be preferred as variety helps when transferring your deep learning model to real world data. \section dnn_synthetic_script Running the object-centric generation script We will now run the generation script. -The script places a random set of objects in a simple cubic room, with added distractors. Materials of the walls and distractors are randomized. - -This script and an example configuration file can be found in the `script/dataset_generator` folder of your ViSP source directory. +The script places a random set of objects in a simple cubic room, with added distractors. Materials of the walls and +distractors are randomized. +This script and an example configuration file can be found in the `script/dataset_generator` folder of your ViSP +source directory. The basic algorithm is: \verbatim @@ -114,12 +134,9 @@ For each scene: \endverbatim Many randomization parameters can be modified to alter the rendering, as explained in \ref dnn_input_configuration. - - With this simple approach, we can obtain images such as: \image html misc/blenderproc_rgb_example.png - \subsection dnn_input_objects 3D model format To use this data generation tool, you should first provide the 3D models. You can provide multiple models, which will be sampled randomly during generation. @@ -136,9 +153,12 @@ The models should be contained in a folder as such: - another_model.mtl \endverbatim -When setting up the configuration file in \ref dnn_input_configuration, "models_path" should point to the root folder, models. -Each subfolder should contain a single object, in `.obj` format (with potential materials and textures). Each object will be considered as having its own class, the class name being the name of the subfolder (e.g., objectA or objectB). -The class indices start with 1, and are sorted alphabetically depending on the name of the class (e.g., objectA = 1, objectB = 2). +When setting up the configuration file in \ref dnn_input_configuration, "models_path" should point to the root folder, +models. +Each subfolder should contain a single object, in `.obj` format (with potential materials and textures). Each object +will be considered as having its own class, the class name being the name of the subfolder (e.g., objectA or objectB). +The class indices start with 1, and are sorted alphabetically depending on the name of the class (e.g., objectA = 1, +objectB = 2). \subsection dnn_input_configuration Generation configuration Configuring the dataset generation is done through a JSON file. An example configuration file can be seen below: @@ -169,7 +189,8 @@ The general parameters are: -You can also control some of the rendering parameters. This will impact the rendering time and the quality of the generated RGB images. +You can also control some of the rendering parameters. This will impact the rendering time and the quality of the +generated RGB images. These parameters are located in the "rendering" field. @@ -181,12 +202,14 @@ These parameters are located in the "rendering" field. -
NameType, possible valuesDescription
denoiser One of [null, "INTEL", "OPTIX"]Which denoiser to use after performing ray tracing. null indicates that no denoiser is used. "OPTIX" requires a compatible Nvidia GPU. + Which denoiser to use after performing ray tracing. null indicates that no denoiser is used. "OPTIX" requires + a compatible Nvidia GPU. Using a denoiser allows to obtain a clean image, with a low number of rays per pixels.
-You can also modify the camera's intrinsic parameters. The camera uses an undistorted perspective projection model. For more information on camera parameters, see vpCameraParameters. +You can also modify the camera's intrinsic parameters. The camera uses an undistorted perspective projection model. +For more information on camera parameters, see vpCameraParameters. These parameters are found in the "camera" field of the configuration. @@ -224,8 +247,10 @@ These parameters are found in the "camera" field of the configuration.
NameType, possible valuesDescription
randomize_params_percent Float, [0, 100) - Controls the randomization of the camera parameters \f$p_x, p_y, u_0, v_0\f$. If randomize_params_percent > 0, then, each time a scene is created the intrinsics are perturbed around the given values. - For example, if this parameters is equal to 0.10 and \f$p_x = 500\f$, then the used \f$p_x\f$ when generating images will be in the range [450, 550]. + Controls the randomization of the camera parameters \f$p_x, p_y, u_0, v_0\f$. If randomize_params_percent > 0, + then, each time a scene is created the intrinsics are perturbed around the given values. + For example, if this parameters is equal to 0.10 and \f$p_x = 500\f$, then the used \f$p_x\f$ when generating + images will be in the range [450, 550].
@@ -239,8 +264,10 @@ To customize the scene, you can change the parameters in the "scene" field: Float > 1.0, < room_size_multiplier_max Minimum room size as a factor of the biggest sampled target object. The room is cubic. - The size of the biggest object is the length of the largest diagonal of its axis-aligned bounding box. This tends to overestimate the size of the object. - If the size of the biggest object is 0.5m, room_size_multiplier_max = 2 and room_size_multiplier_max = 4, then the room's size will be randomly sampled to be between 1m and 2m. + The size of the biggest object is the length of the largest diagonal of its axis-aligned bounding box. + This tends to overestimate the size of the object. + If the size of the biggest object is 0.5m, room_size_multiplier_max = 2 and room_size_multiplier_max = 4, + then the room's size will be randomly sampled to be between 1m and 2m. @@ -248,20 +275,25 @@ To customize the scene, you can change the parameters in the "scene" field: Float > room_size_multiplier_min Minimum room size as a factor of the biggest sampled target object. The room is cubic. - The size of the biggest object is the length of the largest diagonal of its axis-aligned bounding box. This tends to overestimate the size of the object. - If the size of the biggest object is 0.5m, room_size_multiplier_max = 2 and room_size_multiplier_max = 4, then the room's size will be randomly sampled to be between 1m and 2m. + The size of the biggest object is the length of the largest diagonal of its axis-aligned bounding box. + This tends to overestimate the size of the object. + If the size of the biggest object is 0.5m, room_size_multiplier_max = 2 and room_size_multiplier_max = 4, + then the room's size will be randomly sampled to be between 1m and 2m. simulate_physics Boolean - Whether to simulate physics. If false, then objects will be floating across the room. If true, then objects will fall to the ground. + Whether to simulate physics. If false, then objects will be floating across the room. If true, + then objects will fall to the ground. max_num_textures Int > 0 - Max number of textures per blenderproc run. If scenes_per_run is 1, max_num_textures = 50 and the number of distractors is more than 50, then the 50 textures will be used across all distractors (and walls). In this case, new materials will be sampled for each scene. + Max number of textures per blenderproc run. If scenes_per_run is 1, max_num_textures = 50 and the number of + distractors is more than 50, then the 50 textures will be used across all distractors (and walls). In this case, + new materials will be sampled for each scene. distractors @@ -280,7 +312,8 @@ To customize the scene, you can change the parameters in the "scene" field: -Distractors are small, simple objects that are added along with the target objects to create some variations and occlusions. You can also load custom objects as distractors. +Distractors are small, simple objects that are added along with the target objects to create some variations and +occlusions. You can also load custom objects as distractors. To modify their properties, you can change the "distractors" field of the scene @@ -335,7 +368,8 @@ To modify their properties, you can change the "distractors" field of the scene @@ -344,14 +378,16 @@ To modify their properties, you can change the "distractors" field of the scene @@ -449,23 +485,28 @@ To change the sampling behaviour of target objects, see the properties below:
NameType, possible valuesDescription
Float >= 0.0 Amount of displacement to apply to distractors. - Displacement subdivides the mesh and displaces each of the distractor's vertices according to a random noise pattern. + Displacement subdivides the mesh and displaces each of the distractor's vertices according to a random noise + pattern. This option greatly slows down rendering: set it to 0 if needed.
Float >= 0.0 Amount of noise to add to the material properties of the distractors. - These properties include the specularity, the "metallicness" and the roughness of the material, according to Blender's principled BSDF. + These properties include the specularity, the "metallicness" and the roughness of the material, according to + Blender's principled BSDF.
emissive_prob Float >= 0.0 , <= 1.0 - Probability that a distractor becomes a light source: its surface emits light. Set to more than 0 to add more light variations and shadows. + Probability that a distractor becomes a light source: its surface emits light. Set to more than 0 to add more + light variations and shadows.
Float >= 0.0 Amount of noise to add to the material properties of the target objects. - These properties include the specularity, the "metallicness" and the roughness of the material, according to Blender's principled BSDF. + These properties include the specularity, the "metallicness" and the roughness of the material, according to + Blender's principled BSDF.
cam_min_dist_rel Float >= 0.0, < cam_max_dist_rel - Minimum distance of the camera to the point of interest of the object when sampling camera poses. This is expressed in terms of the size of the target object. - If the target object has a size of 0.5m and cam_min_dist_rel = 1.5, then the closest possible camera will be at 0.75m away from the point of interest. + Minimum distance of the camera to the point of interest of the object when sampling camera poses. + This is expressed in terms of the size of the target object. + If the target object has a size of 0.5m and cam_min_dist_rel = 1.5, then the closest possible camera will be + at 0.75m away from the point of interest.
cam_max_dist_rel Float >= cam_min_dist_rel - Maximum distance of the camera to the point of interest of the object when sampling camera poses. This is expressed in terms of the size of the target object. - If the target object has a size of 0.5m and cam_max_dist_rel = 2.0, then the farthest possible camera will be 1m away from the point of interest. + Maximum distance of the camera to the point of interest of the object when sampling camera poses. + This is expressed in terms of the size of the target object. + If the target object has a size of 0.5m and cam_max_dist_rel = 2.0, then the farthest possible camera will + be 1m away from the point of interest.
@@ -481,41 +522,48 @@ To customize the dataset, modify the options in the "dataset" field: save_path String - Path to the folder that will contain the final dataset. This folder will contain one folder per scene, and each sample of a scene will be its own HDF5 file. + Path to the folder that will contain the final dataset. This folder will contain one folder per scene, + and each sample of a scene will be its own HDF5 file. scenes_per_run Int > 0 - Number of scenes to generate per blenderproc run. Between blenderproc runs, Blender is restarted in order to avoid memory issues. + Number of scenes to generate per blenderproc run. Between blenderproc runs, Blender is restarted in order to + avoid memory issues. num_scenes Int > 0 - Total number of scenes to generate. Generating many scenes will add more diversity to the dataset as object placement, materials and lighting are randomized once per scene. + Total number of scenes to generate. Generating many scenes will add more diversity to the dataset as object + placement, materials and lighting are randomized once per scene. images_per_scene Int > 0 - Number of images to generate per scene. The total number of samples in the dataset will be num_scenes * (images_per_scene + empty_images_per_scene). + Number of images to generate per scene. The total number of samples in the dataset will be + num_scenes * (images_per_scene + empty_images_per_scene). empty_images_per_scene Int >= 0, <= images_per_scene - Number of images without target objects to generate per scene. The camera poses for these images are sampled from the poses used to generate images with target objects. Thus, the only difference will be that the objects are not present, the rest of the scene is left untouched. + Number of images without target objects to generate per scene. The camera poses for these images are sampled + from the poses used to generate images with target objects. Thus, the only difference will be that the objects + are not present, the rest of the scene is left untouched. pose Boolean - Whether to save the pose of target objects that are visible in the camera. The pose of the objects are expressed in the camera frame as an homogeneous matrix \f$^{c}\mathbf{T}_{o}\f$ + Whether to save the pose of target objects that are visible in the camera. The pose of the objects are expressed + in the camera frame as an homogeneous matrix \f$^{c}\mathbf{T}_{o}\f$ @@ -546,7 +594,8 @@ To customize the dataset, modify the options in the "dataset" field: detection Boolean - Whether to save the bounding box detections. In this case, bounding boxes are not computed from the segmentation map (also possible with Blenderproc), but rather in way such that occlusion does not influence the final bounding box. + Whether to save the bounding box detections. In this case, bounding boxes are not computed from the segmentation + map (also possible with Blenderproc), but rather in way such that occlusion does not influence the final bounding box. The detections can be filtered with the parameters in "detection_params". @@ -554,15 +603,19 @@ To customize the dataset, modify the options in the "dataset" field: detection_params:min_size_size_px Int >= 0 - Minimum side length of a detection for it to be considered as valid. Used to filter really far or small objects, for which detection would be hard. + Minimum side length of a detection for it to be considered as valid. Used to filter really far or small objects, + for which detection would be hard. detection_params:min_visibility Float [0.0, 1.0] - Percentage of the object that must be visible for a detection to be considered as valid. The visibility score is computed as such: - First, the vertices of the mesh that are behind the camera are filtered. Then, the vertices that are outside of the camera's field of view are filtered. Then, we randomly sample "detection_params:points_sampling_occlusion" points to test whether the object is occluded (test done through ray casting). + Percentage of the object that must be visible for a detection to be considered as valid. The visibility score is + computed as such: + First, the vertices of the mesh that are behind the camera are filtered. Then, the vertices that are outside of + the camera's field of view are filtered. Then, we randomly sample "detection_params:points_sampling_occlusion" + points to test whether the object is occluded (test done through ray casting). If too many points are filtered, then the object is considered as not visible and detection is invalid. @@ -571,7 +624,8 @@ To customize the dataset, modify the options in the "dataset" field: \section dnn_run_script Running the script to generate data -Once you have configured the generation to your liking, navigate to the `script/dataset_generator` located in your ViSP source directory. +Once you have configured the generation to your liking, navigate to the `script/dataset_generator` located in your +ViSP source directory. You can then run the `generate_dataset.py` script as such \code{.sh} @@ -581,15 +635,18 @@ You can then run the `generate_dataset.py` script as such If all is well setup, then the dataset generation should start and run. -\warning If during generation, you encounter a message about invalid camera placement, try to make room_size_multiplier_min and room_size_multiplier_max larger, so that more space is available for object placement. +\warning If during generation, you encounter a message about invalid camera placement, try to make +room_size_multiplier_min and room_size_multiplier_max larger, so that more space is available for object placement. -To give an idea of generation time, generating 1000 images (with a resolution of 640 x 480) and detections of a single object, with a few added distractors, takes around 30mins on a Quadro RTX 6000. +To give an idea of generation time, generating 1000 images (with a resolution of 640 x 480) and detections of a +single object, with a few added distractors, takes around 30mins on a Quadro RTX 6000. Once generation is finished, you are ready to leverage the data to train your neural network. \section dnn_output Using and parsing the generation output -The dataset generated by Blender is located in the "dataset:save_path" path that you specified in your JSON configuration file. +The dataset generated by Blender is located in the "dataset:save_path" path that you specified in your JSON +configuration file. The dataset has the following structure \verbatim @@ -627,7 +684,11 @@ This script can be run like this: (blenderproc) $ python export_for_yolov7.py --input path/to/dataset --output path/to/yolodataset --train-split 0.8 \endcode -here "--input" indicates the path to the location of the blenderproc dataset, while "--output" points to the folder where the dataset in the format that YoloV7 expects will be saved. "--train-split" is an argument that indicates how much of the dataset is kept for training. A value of 0.8 indicates that 80% of the dataset is used for training, while 20% is used for validation. The split is performed randomly across all scenes (a scene may be visible in both train and validation sets). +here "--input" indicates the path to the location of the blenderproc dataset, while "--output" points to the folder +where the dataset in the format that YoloV7 expects will be saved. "--train-split" is an argument that indicates how +much of the dataset is kept for training. A value of 0.8 indicates that 80% of the dataset is used for training, +while 20% is used for validation. The split is performed randomly across all scenes (a scene may be visible in both + train and validation sets). Once the script has run, the folder "path/to/yolodataset" should be created and contain the dataset as expected by YoloV7. This folder contains a "dataset.yml" file, which will be used when training a YoloV7. It contains: @@ -637,12 +698,14 @@ the following: names: - esa nc: 1 -train: /local/sfelton/yolov7_esa_dataset/images/train -val: /local/sfelton/yolov7_esa_dataset/images/val +train: /local/user/yolov7_esa_dataset/images/train +val: /local/user/yolov7_esa_dataset/images/val ``` where nc is the number of class, "names" are the class names, and "train" and "val" are the paths to the dataset splits. -To start training a YoloV7, you should download the repository and install the required dependencies. Again, we will create a conda environment. You can also use a docker container, as explained in the documentation. We also download the pretrained yolo model, that we will finetune on our own dataset. +To start training a YoloV7, you should download the repository and install the required dependencies. +Again, we will create a conda environment. You can also use a docker container, as explained in the documentation. +We also download the pretrained yolo model, that we will finetune on our own dataset. ``` ~ $ git clone https://github.com/WongKinYiu/yolov7.git ~ $ cd yolov7 @@ -652,7 +715,8 @@ To start training a YoloV7, you should download the repository an (yolov7) ~/yolov7 $ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt ``` -To fine-tune a YoloV7, we should create two new files: the network configuration and the hyperparameters. We will reuse the ones provided for the tiny model. +To fine-tune a YoloV7, we should create two new files: the network configuration and the hyperparameters. +We will reuse the ones provided for the tiny model. ``` CFG=cfg/training/yolov7-tiny-custom.yaml cp cfg/training/yolov7-tiny.yaml $CFG @@ -660,7 +724,7 @@ cp cfg/training/yolov7-tiny.yaml $CFG HYP=data/hyp.scratch.custom.yaml cp data/hyp.scratch.tiny.yaml $HYP ``` -Next open the new cfg file, and modify the number of classes (set "nc" from 80 to the number classes you have in your dataset) +Next open the new cfg file, and modify the number of classes (set "nc" from 80 to the number classes you have in your dataset). You can also modify the hyperparameters file to add more augmentation during training. @@ -684,9 +748,6 @@ Here is an overview of the generated images and the resulting detections for a s

\endhtmlonly - - - \subsection dnn_output_custom_parsing Parsing HDF5 with a custom script In Python, an HDF5 file can be read like a dictionary. @@ -770,7 +831,8 @@ Reading scene 4 \endverbatim - Both depth and normals are represented as floating points, conserving accuracy. -- The object data is represented as a JSON document. Which you can directly save or reparse to save only the information of interest. +- The object data is represented as a JSON document. Which you can directly save or reparse to save only the + information of interest. - Object poses are expressed in the camera frame and are represented as homogeneous matrix. - Bounding boxes coordinates are in pixels, and the values are [x_min, y_min, width, height] @@ -778,6 +840,7 @@ You can modify this script to export the dataset to another format, as it was do \section dnn_synthetic_next Next steps -If you use this generator to train a detection network, you can combine it with Megapose to perform 6D pose estimation and tracking. See \ref tutorial-tracking-megapose. +If you use this generator to train a detection network, you can combine it with Megapose to perform 6D pose estimation +and tracking. See \ref tutorial-tracking-megapose. */ diff --git a/modules/core/include/visp3/core/vpArray2D.h b/modules/core/include/visp3/core/vpArray2D.h index 98192e58d3..0d15edd28d 100644 --- a/modules/core/include/visp3/core/vpArray2D.h +++ b/modules/core/include/visp3/core/vpArray2D.h @@ -823,7 +823,7 @@ template class vpArray2D \code vpArray2D M(3,4); vpArray2D::saveYAML("matrix.yml", M, "example: a YAML-formatted header"); - vpArray2D::saveYAML("matrixIndent.yml", M, "example:\n - a YAML-formatted + vpArray2D::saveYAML("matrixIndent.yml", M, "example:\n - a YAML-formatted \ header\n - with inner indentation"); \endcode Content of matrix.yml: \code example: a YAML-formatted header diff --git a/modules/core/include/visp3/core/vpCannyEdgeDetection.h b/modules/core/include/visp3/core/vpCannyEdgeDetection.h index 18f4920c5b..6cc7689212 100644 --- a/modules/core/include/visp3/core/vpCannyEdgeDetection.h +++ b/modules/core/include/visp3/core/vpCannyEdgeDetection.h @@ -210,7 +210,7 @@ class VISP_EXPORT vpCannyEdgeDetection * \param[in] j : The JSON object, resulting from the parsing of a JSON file. * \param[out] detector : The detector that will be initialized from the JSON data. */ - inline friend void from_json(const json &j, vpCannyEdgeDetection &detector) + friend inline void from_json(const json &j, vpCannyEdgeDetection &detector) { std::string filteringAndGradientName = vpImageFilter::vpCannyFilteringAndGradientTypeToString(detector.m_filteringAndGradientType); filteringAndGradientName = j.value("filteringAndGradientType", filteringAndGradientName); @@ -230,7 +230,7 @@ class VISP_EXPORT vpCannyEdgeDetection * \param[out] j : A JSON parser object. * \param[in] detector : The vpCannyEdgeDetection object that must be parsed into JSON format. */ - inline friend void to_json(json &j, const vpCannyEdgeDetection &detector) + friend inline void to_json(json &j, const vpCannyEdgeDetection &detector) { std::string filteringAndGradientName = vpImageFilter::vpCannyFilteringAndGradientTypeToString(detector.m_filteringAndGradientType); j = json { diff --git a/modules/core/include/visp3/core/vpColVector.h b/modules/core/include/visp3/core/vpColVector.h index 56111c54f3..518b461043 100644 --- a/modules/core/include/visp3/core/vpColVector.h +++ b/modules/core/include/visp3/core/vpColVector.h @@ -319,7 +319,7 @@ class VISP_EXPORT vpColVector : public vpArray2D * ofs.close(); * } * \endcode - * produces `log.csvè file that contains: + * produces `log.csv` file that contains: * \code * 0 * 1 diff --git a/modules/core/include/visp3/core/vpFrameGrabber.h b/modules/core/include/visp3/core/vpFrameGrabber.h index 91ab6fd7c6..0648906be2 100644 --- a/modules/core/include/visp3/core/vpFrameGrabber.h +++ b/modules/core/include/visp3/core/vpFrameGrabber.h @@ -110,7 +110,7 @@ class VISP_EXPORT vpFrameGrabber public: vpFrameGrabber() : init(false), height(0), width(0) { }; - + virtual ~vpFrameGrabber() = default; virtual void open(vpImage &I) = 0; virtual void open(vpImage &I) = 0; diff --git a/modules/core/include/visp3/core/vpImage.h b/modules/core/include/visp3/core/vpImage.h index ff73fe7eca..0d5cbf9894 100644 --- a/modules/core/include/visp3/core/vpImage.h +++ b/modules/core/include/visp3/core/vpImage.h @@ -482,6 +482,7 @@ inline std::ostream &operator<<(std::ostream &s, const vpImage &I) #if defined(VISP_HAVE_PTHREAD) || (defined(_WIN32) && !defined(WINRT_8_0)) namespace { + struct vpImageLut_Param_t { unsigned int m_start_index; diff --git a/modules/core/include/visp3/core/vpImageFilter.h b/modules/core/include/visp3/core/vpImageFilter.h index eb671845d6..902ee0e73f 100644 --- a/modules/core/include/visp3/core/vpImageFilter.h +++ b/modules/core/include/visp3/core/vpImageFilter.h @@ -602,9 +602,11 @@ class VISP_EXPORT vpImageFilter } static void filterX(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); +#ifndef DOXYGEN_SHOULD_SKIP_THIS static void filterXR(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterXG(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterXB(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); +#endif template static inline FilterType filterX(const vpImage &I, unsigned int r, unsigned int c, const FilterType *filter, unsigned int size) @@ -618,7 +620,7 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c]; } - +#ifndef DOXYGEN_SHOULD_SKIP_THIS static inline double filterXR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -784,12 +786,15 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c].B; } +#endif + static void filterY(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); +#ifndef DOXYGEN_SHOULD_SKIP_THIS static void filterYR(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterYG(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); static void filterYB(const vpImage &I, vpImage &dIx, const double *filter, unsigned int size); - +#endif template static void filterY(const vpImage &I, vpImage &dIy, const FilterType *filter, unsigned int size) { @@ -823,7 +828,7 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c]; } - +#ifndef DOXYGEN_SHOULD_SKIP_THIS static inline double filterYR(const vpImage &I, unsigned int r, unsigned int c, const double *filter, unsigned int size) { double result; @@ -985,19 +990,21 @@ class VISP_EXPORT vpImageFilter } return result + filter[0] * I[r][c].B; } +#endif + /*! - * Apply a Gaussian blur to an image. - * \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. - * \param I : Input image. - * \param GI : Filtered image. - * \param size : Filter size. This value should be odd. - * \param sigma : Gaussian standard deviation. If it is equal to zero or - * negative, it is computed from filter size as sigma = (size-1)/6. - * \param normalize : Flag indicating whether to normalize the filter coefficients or not. - * - * \sa getGaussianKernel() to know which kernel is used. - */ + * Apply a Gaussian blur to an image. + * \tparam FilterType : Either float, to accelerate the computation time, or double, to have greater precision. + * \param I : Input image. + * \param GI : Filtered image. + * \param size : Filter size. This value should be odd. + * \param sigma : Gaussian standard deviation. If it is equal to zero or + * negative, it is computed from filter size as sigma = (size-1)/6. + * \param normalize : Flag indicating whether to normalize the filter coefficients or not. + * + * \sa getGaussianKernel() to know which kernel is used. + */ template static void gaussianBlur(const vpImage &I, vpImage &GI, unsigned int size = 7, FilterType sigma = 0., bool normalize = true) { diff --git a/modules/core/include/visp3/core/vpMatrix.h b/modules/core/include/visp3/core/vpMatrix.h index 3fac0d971c..09b7145244 100644 --- a/modules/core/include/visp3/core/vpMatrix.h +++ b/modules/core/include/visp3/core/vpMatrix.h @@ -978,6 +978,7 @@ vpMatrix M(R); //@} #if defined(VISP_BUILD_DEPRECATED_FUNCTIONS) + vp_deprecated double euclideanNorm() const; /*! diff --git a/modules/detection/include/visp3/detection/vpDetectorAprilTag.h b/modules/detection/include/visp3/detection/vpDetectorAprilTag.h index 92fdda86f2..964dd4a0de 100644 --- a/modules/detection/include/visp3/detection/vpDetectorAprilTag.h +++ b/modules/detection/include/visp3/detection/vpDetectorAprilTag.h @@ -288,20 +288,10 @@ class VISP_EXPORT vpDetectorAprilTag : public vpDetectorBase void setAprilTagQuadSigma(float quadSigma); void setAprilTagRefineEdges(bool refineEdges); -#if defined(VISP_BUILD_DEPRECATED_FUNCTIONS) - /*! - * @name Deprecated functions - */ - //@{ - vp_deprecated void setAprilTagRefineDecode(bool refineDecode); - vp_deprecated void setAprilTagRefinePose(bool refinePose); - //@} -#endif -/*! - * Allow to enable the display of overlay tag information in the windows - * (vpDisplay) associated to the input image. - */ + + /*! Allow to enable the display of overlay tag information in the windows + * (vpDisplay) associated to the input image. */ inline void setDisplayTag(bool display, const vpColor &color = vpColor::none, unsigned int thickness = 2) { m_displayTag = display; @@ -313,6 +303,16 @@ class VISP_EXPORT vpDetectorAprilTag : public vpDetectorBase void setZAlignedWithCameraAxis(bool zAlignedWithCameraFrame); +#if defined(VISP_BUILD_DEPRECATED_FUNCTIONS) + /*! + @name Deprecated functions + */ + //@{ + vp_deprecated void setAprilTagRefinePose(bool refinePose); + vp_deprecated void setAprilTagRefineDecode(bool refineDecode); + //@} +#endif + protected: bool m_displayTag; vpColor m_displayTagColor; diff --git a/modules/detection/include/visp3/detection/vpDetectorDNNOpenCV.h b/modules/detection/include/visp3/detection/vpDetectorDNNOpenCV.h index 57e955e8e2..87606ba1cf 100644 --- a/modules/detection/include/visp3/detection/vpDetectorDNNOpenCV.h +++ b/modules/detection/include/visp3/detection/vpDetectorDNNOpenCV.h @@ -201,7 +201,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV * \param j The JSON object, resulting from the parsing of a JSON file. * \param config The configuration of the network, that will be initialized from the JSON data. */ - inline friend void from_json(const json &j, NetConfig &config) + friend inline void from_json(const json &j, NetConfig &config) { config.m_confThreshold = j.value("confidenceThreshold", config.m_confThreshold); if (config.m_confThreshold <= 0) { @@ -241,7 +241,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV * \param j A JSON parser object. * \param config The vpDetectorDNNOpenCV::NetConfig that must be parsed into JSON format. */ - inline friend void to_json(json &j, const NetConfig &config) + friend inline void to_json(json &j, const NetConfig &config) { std::pair resolution = { config.m_inputSize.width, config.m_inputSize.height }; std::vector v_mean = { config.m_mean[0], config.m_mean[1], config.m_mean[2] }; @@ -440,7 +440,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV return text; } - inline friend std::ostream &operator<<(std::ostream &os, const NetConfig &config) + friend inline std::ostream &operator<<(std::ostream &os, const NetConfig &config) { os << config.toString(); return os; @@ -515,7 +515,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV * \param j The JSON object, resulting from the parsing of a JSON file. * \param network The network, that will be initialized from the JSON data. */ - inline friend void from_json(const json &j, vpDetectorDNNOpenCV &network) + friend inline void from_json(const json &j, vpDetectorDNNOpenCV &network) { network.m_netConfig = j.value("networkSettings", network.m_netConfig); } @@ -526,7 +526,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV * \param j The JSON parser. * \param network The network we want to parse the configuration. */ - inline friend void to_json(json &j, const vpDetectorDNNOpenCV &network) + friend inline void to_json(json &j, const vpDetectorDNNOpenCV &network) { j = json { {"networkSettings", network.m_netConfig} @@ -534,7 +534,7 @@ class VISP_EXPORT vpDetectorDNNOpenCV } #endif - inline friend std::ostream &operator<<(std::ostream &os, const vpDetectorDNNOpenCV &network) + friend inline std::ostream &operator<<(std::ostream &os, const vpDetectorDNNOpenCV &network) { os << network.m_netConfig; return os; diff --git a/modules/gui/include/visp3/gui/vpColorBlindFriendlyPalette.h b/modules/gui/include/visp3/gui/vpColorBlindFriendlyPalette.h index b95e7f5dcc..f708605cd4 100755 --- a/modules/gui/include/visp3/gui/vpColorBlindFriendlyPalette.h +++ b/modules/gui/include/visp3/gui/vpColorBlindFriendlyPalette.h @@ -139,13 +139,6 @@ class VISP_EXPORT vpColorBlindFriendlyPalette */ std::string to_string() const; - /** - * \brief Cast the object into an unsigned int that matches the value of its \b _colorID member. - * - * \return unsigned int that matches the value of its \b _colorID member. - */ - unsigned int to_uint() const; - /** * \brief Get the list of available colors names. * diff --git a/modules/gui/include/visp3/gui/vpPlot.h b/modules/gui/include/visp3/gui/vpPlot.h index ffd1dbb128..2943e37869 100644 --- a/modules/gui/include/visp3/gui/vpPlot.h +++ b/modules/gui/include/visp3/gui/vpPlot.h @@ -101,7 +101,7 @@ * } * * return 0; - $ #endif + * #endif * } * \endcode */ diff --git a/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h b/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h index c711968932..04391deea2 100644 --- a/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h +++ b/modules/imgproc/include/visp3/imgproc/vpCircleHoughTransform.h @@ -295,7 +295,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] j : The JSON object, resulting from the parsing of a JSON file. * \param[out] params : The circle Hough transform parameters that will be initialized from the JSON data. */ - inline friend void from_json(const json &j, vpCircleHoughTransformParameters ¶ms) + friend inline void from_json(const json &j, vpCircleHoughTransformParameters ¶ms) { std::string filteringAndGradientName = vpImageFilter::vpCannyFilteringAndGradientTypeToString(params.m_filteringAndGradientType); filteringAndGradientName = j.value("filteringAndGradientType", filteringAndGradientName); @@ -363,7 +363,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[out] j : A JSON parser object. * \param[in] params : The circle Hough transform parameters that will be serialized in the json object. */ - inline friend void to_json(json &j, const vpCircleHoughTransformParameters ¶ms) + friend inline void to_json(json &j, const vpCircleHoughTransformParameters ¶ms) { std::pair radiusLimits = { params.m_minRadius, params.m_maxRadius }; @@ -486,7 +486,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[in] j The JSON object, resulting from the parsing of a JSON file. * \param[out] detector The detector, that will be initialized from the JSON data. */ - inline friend void from_json(const json &j, vpCircleHoughTransform &detector) + friend inline void from_json(const json &j, vpCircleHoughTransform &detector) { detector.m_algoParams = j; } @@ -497,7 +497,7 @@ class VISP_EXPORT vpCircleHoughTransform * \param[out] j A JSON parser object. * \param[in] detector The vpCircleHoughTransform that must be parsed into JSON format. */ - inline friend void to_json(json &j, const vpCircleHoughTransform &detector) + friend inline void to_json(json &j, const vpCircleHoughTransform &detector) { j = detector.m_algoParams; } diff --git a/modules/python/.gitignore b/modules/python/.gitignore new file mode 100644 index 0000000000..f015fe73c8 --- /dev/null +++ b/modules/python/.gitignore @@ -0,0 +1,10 @@ +*.egg-info +bindings/src +build +stubs/visp +stubs/build +*.eggs +doc/_build +doc/_autosummary/* +doc/generated +doc/api.rst diff --git a/modules/python/CMakeLists.txt b/modules/python/CMakeLists.txt new file mode 100644 index 0000000000..b5aa701ece --- /dev/null +++ b/modules/python/CMakeLists.txt @@ -0,0 +1,139 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +# Prevent CMAKE from interpreting this directory as a standard module. +if(NOT VISP_DIR) + return() +endif() + +# As we need all the others modules to already be configured, +# we should configure the python directory by add_subdirectory("modules/python") in the main cmake. +find_package(VISP REQUIRED) + +# TODO: check for pip + +# Set pip args +if(DEFINED ENV{VIRTUAL_ENV} OR DEFINED ENV{CONDA_PREFIX}) + set(_pip_args) +else() + set(_pip_args "--user") +endif() + +# Step 1: Generate configuration file +# Define modules for which to generate python bindings +set(python_ignored_modules "visp_python" "visp_java_bindings_generator" "visp_java" ) +set(python_bound_modules ${VISP_MODULES_BUILD}) +list(REMOVE_ITEM python_bound_modules ${python_ignored_modules}) + +# Configure the different directories +set(bindgen_package_location "${CMAKE_CURRENT_SOURCE_DIR}/generator") +set(bindings_package_location "${CMAKE_CURRENT_SOURCE_DIR}/bindings") +set(bindings_gen_location "${CMAKE_CURRENT_BINARY_DIR}/bindings") +file(MAKE_DIRECTORY "${bindings_gen_location}/src") +#file(TOUCH "${bindings_gen_location}/src/main.cpp") +set(python_bindings_cpp_src "${bindings_gen_location}/src/main.cpp") + +foreach(module ${python_bound_modules}) + get_target_property(dirs "${module}" INCLUDE_DIRECTORIES) + string(REPLACE "visp_" "" clean_module_name ${module}) + set(cpp_src "${bindings_gen_location}/src/${clean_module_name}.cpp") + list(APPEND python_bindings_cpp_src "${cpp_src}") +endforeach() + +include("${CMAKE_CURRENT_SOURCE_DIR}/GenerateConfig.cmake") + +# Step 2: Generate bindings +# First, we install the bindings generator as an editable pip package +# Then, we call it with the configuration files as argument. The .cpp files are generated in the cmake build directory + +# Get dependencies of the bindings generator +# We should only run the generator when the config files, the sources or the C++ modules have changed +file(GLOB config_files "${CMAKE_CURRENT_SOURCE_DIR}/config/*.json") +file(GLOB_RECURSE python_sources "${CMAKE_CURRENT_SOURCE_DIR}/generator/visp_python_bindgen/*.py") +set(pip_files "${CMAKE_CURRENT_SOURCE_DIR}/generator/pyproject.toml") + +set(bindings_dependencies + ${python_bound_modules} + ${json_config_file_path} ${config_files} + ${python_sources} ${pip_files} +) + +# If we have doxygen, we should first generate the XML documentation +# so that the binding stubs and doc is as complete as possible +if(DOXYGEN_FOUND) + list(APPEND bindings_dependencies visp_doc_xml) +endif() + +add_custom_command( + OUTPUT ${python_bindings_cpp_src} + COMMAND ${PYTHON3_EXECUTABLE} -m pip install ${_pip_args} ${bindgen_package_location} + COMMAND ${PYTHON3_EXECUTABLE} -m visp_python_bindgen.generator --config "${CMAKE_CURRENT_SOURCE_DIR}/config" --build-folder ${bindings_gen_location} --main-config "${json_config_file_path}" + DEPENDS ${bindings_dependencies} + COMMENT "Installing the bindings generator and running it..." +) +add_custom_target( + visp_python_bindings_generator_run + DEPENDS ${python_bindings_cpp_src} +) + +set(VISP_PYTHON_VERSION "${VISP_VERSION}") +# Step 3: Compile and install bindings as a python package +add_subdirectory(bindings) + +# Step 4: Copy stubs dir and install stubs for autocompletion +add_subdirectory(stubs) + +# Global target: compile and install the Python bindings +add_custom_target( + visp_python_bindings + DEPENDS visp_python_bindings_stubs +) + +# Step 5: Build documentation +if(BUILD_PYTHON_BINDINGS_DOC) + add_subdirectory(doc) +endif() + + +# Export Variables to parent cmake +set(VISP_PYTHON_BOUND_MODULES "") +foreach(module ${python_bound_modules}) + string(REPLACE "visp_" "" clean_module_name ${module}) + list(APPEND VISP_PYTHON_BOUND_MODULES "${clean_module_name}") +endforeach() +set(VISP_PYTHON_BOUND_MODULES "${VISP_PYTHON_BOUND_MODULES}" PARENT_SCOPE) +set(VISP_PYTHON_GENERATED_CONFIG_FILE "${json_config_file_path}" PARENT_SCOPE) + +set(VISP_PYTHON_PACKAGE_VERSION "${VISP_PYTHON_VERSION}" PARENT_SCOPE) diff --git a/modules/python/GenerateConfig.cmake b/modules/python/GenerateConfig.cmake new file mode 100644 index 0000000000..9281405e0b --- /dev/null +++ b/modules/python/GenerateConfig.cmake @@ -0,0 +1,131 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +set(json_config_file "{}") +set(json_config_file_path "${CMAKE_CURRENT_BINARY_DIR}/cmake_config.json") + +# Paths to important directories +string(JSON json_config_file SET ${json_config_file} "xml_doc_path" "\"${VISP_DOC_DIR}/xml\"") +string(JSON json_config_file SET ${json_config_file} "build_dir" "\"${CMAKE_BINARY_DIR}\"") +string(JSON json_config_file SET ${json_config_file} "source_dir" "\"${CMAKE_SOURCE_DIR}\"") + +# Add include directories to config file +set(json_include_dirs "[]") +set(include_dirs_count 0) +foreach(include_dir ${VISP_INCLUDE_DIRS}) + string(JSON json_include_dirs SET ${json_include_dirs} "${include_dirs_count}" "\"${include_dir}\"") + MATH(EXPR include_dirs_count "${include_dirs_count}+1") +endforeach() +string(JSON json_config_file SET ${json_config_file} "include_dirs" "${json_include_dirs}") + +# For each bound module, add its headers and dependencies to config file +set(json_modules "{}") +foreach(module ${python_bound_modules}) + string(REPLACE "visp_" "" clean_module_name ${module}) + string(JSON json_modules SET ${json_modules} ${clean_module_name} "{}") + # Get module headers + set(json_header_list "[]") + set(header_count 0) + foreach(module_header ${VISP_MODULE_${module}_HEADERS}) + string(JSON json_header_list SET ${json_header_list} "${header_count}" "\"${module_header}\"") + MATH(EXPR header_count "${header_count}+1") + endforeach() + string(JSON json_modules SET ${json_modules} ${clean_module_name} "headers" "${json_header_list}") + # Get module dependencies + set(json_deps_list "[]") + set(dep_count 0) + foreach(dep ${VISP_MODULE_${module}_DEPS}) + string(REPLACE "visp_" "" clean_dep ${dep}) + string(JSON json_deps_list SET ${json_deps_list} "${dep_count}" "\"${clean_dep}\"") + MATH(EXPR dep_count "${dep_count}+1") + endforeach() + string(JSON json_modules SET ${json_modules} ${clean_module_name} "dependencies" "${json_deps_list}") +endforeach() +string(JSON json_config_file SET ${json_config_file} "modules" ${json_modules}) + +# Define platform specific macros +# These should be the same as those defined when compiling the visp libraries +# The impact will only be visible if the macros defined (or not) below appear in ViSP's headers +# See https://github.com/cpredef/predef/tree/master for compiler/OS specific #defines +set(json_defines "{}") +string(JSON json_defines SET ${json_defines} "__cplusplus" "${VISP_CXX_STANDARD}") +# Compiler +if(CMAKE_COMPILER_IS_GNUCXX) + string(REPLACE "." ";" GCC_VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET GCC_VERSION_LIST 0 GCC_MAJOR) + list(GET GCC_VERSION_LIST 1 GCC_MINOR) + list(GET GCC_VERSION_LIST 2 GCC_PATCH) + + string(JSON json_defines SET ${json_defines} "__GNUC__" "${GCC_MAJOR}") + string(JSON json_defines SET ${json_defines} "__GNUC_MINOR__" "${GCC_MINOR}") + string(JSON json_defines SET ${json_defines} "__GNUC_PATCHLEVEL__" "${GCC_PATCH}") +endif() + +if(CMAKE_COMPILER_IS_CLANGCXX) + string(REPLACE "." ";" CLANG_VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET CLANG_VERSION_LIST 0 CLANG_MAJOR) + list(GET CLANG_VERSION_LIST 1 CLANG_MINOR) + list(GET CLANG_VERSION_LIST 2 CLANG_PATCH) + + string(JSON json_defines SET ${json_defines} "__clang__" "${CLANG_MAJOR}") + string(JSON json_defines SET ${json_defines} "__clang_minor__" "${CLANG_MINOR}") + string(JSON json_defines SET ${json_defines} "__clang_patchlevel__" "${CLANG_PATCH}") + string(JSON json_defines SET ${json_defines} "__clang_version__" "${CMAKE_CXX_COMPILER_VERSION}") +endif() + +if(MSVC) + string(JSON json_defines SET ${json_defines} "_MSC_VER" "${MSVC_VERSION}") +endif() + +if(MINGW) + string(JSON json_defines SET ${json_defines} "__MINGW32__" "null") +endif() +# OS +if(WIN32) + string(JSON json_defines SET ${json_defines} "_WIN32" "null") +endif() +if(UNIX) + string(JSON json_defines SET ${json_defines} "__linux__" "null") + string(JSON json_defines SET ${json_defines} "__unix__" "null") + string(JSON json_defines SET ${json_defines} "_unix" "null") +endif() +if(APPLE) + string(JSON json_defines SET ${json_defines} "__APPLE__" "null") + string(JSON json_defines SET ${json_defines} "__MACH__" "null") +endif() + +string(JSON json_config_file SET ${json_config_file} "defines" ${json_defines}) + +file(WRITE ${json_config_file_path} "${json_config_file}") diff --git a/modules/python/bindings/CMakeLists.txt b/modules/python/bindings/CMakeLists.txt new file mode 100644 index 0000000000..644d148970 --- /dev/null +++ b/modules/python/bindings/CMakeLists.txt @@ -0,0 +1,63 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +# Declare the cpp source files as explicitely generated so that pybind11_add_module does not look for them when they are not yet created +set_source_files_properties(${python_bindings_cpp_src} PROPERTIES GENERATED TRUE) + +pybind11_add_module(_visp ${python_bindings_cpp_src}) + +# Place library in binary/visp dir so that it doesn't pollute lib dir +# This .so file is not treated the same as the others and we shouldn't link against it when compiling in C++ +# when installing the python module, pip will look into the "visp" subfolder for .so files to copy into the site-packages + +file(MAKE_DIRECTORY "${bindings_gen_location}/src") +set_target_properties(_visp PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" +) + +target_include_directories(_visp PRIVATE include) # Include directory containing custom bindings +target_include_directories(_visp PRIVATE ${VISP_INCLUDE_DIRS}) +target_link_libraries(_visp PRIVATE ${VISP_LIBRARIES}) +add_dependencies(_visp visp_python_bindings_generator_run) + +# Setup pip install +if(PYTHON3INTERP_FOUND) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in" "${CMAKE_CURRENT_BINARY_DIR}/setup.py" @ONLY) + add_custom_target( visp_python_bindings_install + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/visp" "${CMAKE_CURRENT_BINARY_DIR}/visp" + COMMAND ${PYTHON3_EXECUTABLE} -m pip install ${_pip_args} "${CMAKE_CURRENT_BINARY_DIR}" + DEPENDS _visp + ) +endif() diff --git a/modules/python/bindings/include/blob.hpp b/modules/python/bindings/include/blob.hpp new file mode 100644 index 0000000000..5dd634a473 --- /dev/null +++ b/modules/python/bindings/include/blob.hpp @@ -0,0 +1,69 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_BLOB_HPP +#define VISP_PYTHON_BLOB_HPP + +#include +#include +#include + +#include +#include + +namespace py = pybind11; + +void bindings_vpDot2(py::class_ &pyDot2) +{ + pyDot2.def_static("defineDots", [](std::vector &dots, + const std::string &dotFile, + vpImage &I, + vpColor col = vpColor::blue, + bool trackDot = true) { + return vpDot2::defineDots(&dots[0], dots.size(), dotFile, I, col, trackDot); + }, R"doc( +Wrapper for the defineDots method, see the C++ ViSP documentation. +)doc", py::arg("dots"), py::arg("dotFile"), py::arg("I"), py::arg("color"), py::arg("trackDot") = true); + + pyDot2.def_static("trackAndDisplay", [](std::vector &dots, + vpImage &I, + std::vector &cogs, + std::optional> cogStar) { + vpImagePoint *desireds = cogStar ? &((*cogStar)[0]) : nullptr; + vpDot2::trackAndDisplay(&dots[0], dots.size(), I, cogs, desireds); + }, R"doc( +Wrapper for the trackAndDisplay method, see the C++ ViSP documentation. +)doc", py::arg("dots"), py::arg("I"), py::arg("cogs"), py::arg("desiredCogs")); +} + +#endif diff --git a/modules/python/bindings/include/core.hpp b/modules/python/bindings/include/core.hpp new file mode 100644 index 0000000000..99c8b7e8ee --- /dev/null +++ b/modules/python/bindings/include/core.hpp @@ -0,0 +1,44 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_HPP +#define VISP_PYTHON_CORE_HPP + +#include "core/utils.hpp" +#include "core/arrays.hpp" +#include "core/images.hpp" +#include "core/pixel_meter.hpp" +#include "core/image_conversions.hpp" + + +#endif diff --git a/modules/python/bindings/include/core/arrays.hpp b/modules/python/bindings/include/core/arrays.hpp new file mode 100644 index 0000000000..7c862728c2 --- /dev/null +++ b/modules/python/bindings/include/core/arrays.hpp @@ -0,0 +1,352 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_ARRAYS_HPP +#define VISP_PYTHON_CORE_ARRAYS_HPP + +#include "core/utils.hpp" + +#include +#include +#include + +#include +#include +#include + +/* + * Array2D and its children. + */ + +/* + * Get buffer infos : used in def_buffer and the .numpy() function. + */ +template py::buffer_info get_buffer_info(T &) = delete; +template class Array, + typename std::enable_if, Array>::value, bool>::type = true> +py::buffer_info get_buffer_info(Array &array) +{ + return make_array_buffer(array.data, { array.getRows(), array.getCols() }, false); +} + +template<> +py::buffer_info get_buffer_info(vpMatrix &array) +{ + return make_array_buffer(array.data, { array.getRows(), array.getCols() }, false); +} + +template<> +py::buffer_info get_buffer_info(vpColVector &array) +{ + return make_array_buffer(array.data, { array.getRows() }, false); +} +template<> +py::buffer_info get_buffer_info(vpRowVector &array) +{ + return make_array_buffer(array.data, { array.getCols() }, false); +} +template<> +py::buffer_info get_buffer_info(vpTranslationVector &array) +{ + return make_array_buffer(array.data, { 3 }, false); +} +template<> +py::buffer_info get_buffer_info(vpRotationMatrix &array) +{ + return make_array_buffer(array.data, { array.getRows(), array.getCols() }, true); +} +template<> +py::buffer_info get_buffer_info(vpHomogeneousMatrix &array) +{ + return make_array_buffer(array.data, { array.getRows(), array.getCols() }, true); +} + +/* + * Array 2D indexing + */ +template +void define_get_item_2d_array(PyClass &pyClass) +{ + pyClass.def("__getitem__", [](const Class &self, std::pair pair) -> Item { + int i = pair.first, j = pair.second; + const int rows = (int)self.getRows(), cols = (int)self.getCols(); + if (i >= rows || j >= cols || i < -rows || j < -cols) { + std::stringstream ss; + ss << "Invalid indexing into a 2D array: got indices " << shape_to_string({ i, j }) + << " but array has dimensions " << shape_to_string({ rows, cols }); + throw std::runtime_error(ss.str()); + } + if (i < 0) { + i = rows + i; + } + if (j < 0) { + j = cols + j; + } + return self[i][j]; + }); + pyClass.def("__getitem__", [](const Class &self, int i) -> np_array_cf { + const int rows = (int)self.getRows(); + if (i >= rows || i < -rows) { + std::stringstream ss; + ss << "Invalid indexing into a 2D array: got row index " << shape_to_string({ i }) + << " but array has " << rows << " rows"; + throw std::runtime_error(ss.str()); + } + if (i < 0) { + i = rows + i; + } + return (py::cast(self).template cast >())[py::cast(i)].template cast>(); + }, py::keep_alive<0, 1>()); + pyClass.def("__getitem__", [](const Class &self, py::slice slice) -> py::array_t { + return (py::cast(self).template cast >())[slice].template cast>(); + }, py::keep_alive<0, 1>()); + pyClass.def("__getitem__", [](const Class &self, py::tuple tuple) { + return (py::cast(self).template cast >())[tuple].template cast>(); + }, py::keep_alive<0, 1>()); +} + +/* + * Array 2D indexing + */ +template +void define_get_item_1d_array(PyClass &pyClass) +{ + pyClass.def("__getitem__", [](const Class &self, int i) -> Item { + + const int elems = (int)self.getRows() * (int)self.getCols(); + if (i >= elems || i < -elems) { + std::stringstream ss; + ss << "Invalid indexing into a 1D array: got indices " << shape_to_string({ i }) + << " but array has dimensions " << shape_to_string({ elems }); + throw std::runtime_error(ss.str()); + } + if (i < 0) { + i = elems + i; + } + return self[i]; + }); + pyClass.def("__getitem__", [](const Class &self, py::slice slice) -> py::array_t { + return (py::cast(self).template cast >())[slice].template cast>(); + }, py::keep_alive<0, 1>()); +} + +const char *numpy_fn_doc_writable = R"doc( + Numpy view of the underlying array data. + This numpy view can be used to directly modify the array. +)doc"; + +const char *numpy_fn_doc_nonwritable = R"doc( + Numpy view of the underlying array data. + This numpy view cannot be modified. + If you try to modify the array, an exception will be raised. +)doc"; + +template +void bindings_vpArray2D(py::class_> &pyArray2D) +{ + pyArray2D.def_buffer(&get_buffer_info); + + pyArray2D.def("numpy", [](vpArray2D &self) -> np_array_cf { + return py::cast(self).template cast >(); + }, numpy_fn_doc_writable, py::keep_alive<0, 1>()); + + pyArray2D.def(py::init([](np_array_cf &np_array) { + verify_array_shape_and_dims(np_array, 2, "ViSP 2D array"); + const std::vector shape = np_array.request().shape; + vpArray2D result(shape[0], shape[1]); + copy_data_from_np(np_array, result.data); + return result; + }), R"doc( +Construct a 2D ViSP array by **copying** a 2D numpy array. + +:param np_array: The numpy array to copy. + +)doc", py::arg("np_array")); + + define_get_item_2d_array>, vpArray2D, T>(pyArray2D); +} + +void bindings_vpMatrix(py::class_> &pyMatrix) +{ + pyMatrix.def_buffer(&get_buffer_info); + + pyMatrix.def("numpy", [](vpMatrix &self) -> np_array_cf { + return py::cast(self).cast>(); + }, numpy_fn_doc_writable, py::keep_alive<0, 1>()); + + pyMatrix.def(py::init([](np_array_cf np_array) { + verify_array_shape_and_dims(np_array, 2, "ViSP Matrix"); + const std::vector shape = np_array.request().shape; + vpMatrix result(shape[0], shape[1]); + copy_data_from_np(np_array, result.data); + return result; + }), R"doc( +Construct a matrix by **copying** a 2D numpy array. + +:param np_array: The numpy array to copy. + +)doc", py::arg("np_array")); + + define_get_item_2d_array>, vpMatrix, double>(pyMatrix); +} + + +void bindings_vpRotationMatrix(py::class_> &pyRotationMatrix) +{ + + pyRotationMatrix.def_buffer(&get_buffer_info); + pyRotationMatrix.def("numpy", [](vpRotationMatrix &self) -> np_array_cf { + return py::cast(self).cast>(); + }, numpy_fn_doc_nonwritable, py::keep_alive<0, 1>()); + pyRotationMatrix.def(py::init([](np_array_cf np_array) { + verify_array_shape_and_dims(np_array, { 3, 3 }, "ViSP rotation matrix"); + const std::vector shape = np_array.request().shape; + vpRotationMatrix result; + copy_data_from_np(np_array, result.data); + if (!result.isARotationMatrix()) { + throw std::runtime_error("Input numpy array is not a valid rotation matrix"); + } + return result; + }), R"doc( +Construct a rotation matrix by **copying** a 2D numpy array. +This numpy array should be of dimensions :math:`3 \times 3` and be a valid rotation matrix. +If it is not a rotation matrix, an exception will be raised. + +:param np_array: The numpy 1D array to copy. + +)doc", py::arg("np_array")); + define_get_item_2d_array>, vpRotationMatrix, double>(pyRotationMatrix); +} + +void bindings_vpHomogeneousMatrix(py::class_> &pyHomogeneousMatrix) +{ + pyHomogeneousMatrix.def_buffer(get_buffer_info); + pyHomogeneousMatrix.def("numpy", [](vpHomogeneousMatrix &self) -> np_array_cf { + return py::cast(self).cast>(); + }, numpy_fn_doc_nonwritable, py::keep_alive<0, 1>()); + + pyHomogeneousMatrix.def(py::init([](np_array_cf np_array) { + verify_array_shape_and_dims(np_array, { 4, 4 }, "ViSP homogeneous matrix"); + const std::vector shape = np_array.request().shape; + vpHomogeneousMatrix result; + copy_data_from_np(np_array, result.data); + if (!result.isAnHomogeneousMatrix()) { + throw std::runtime_error("Input numpy array is not a valid homogeneous matrix"); + } + return result; + }), R"doc( +Construct a homogeneous matrix by **copying** a 2D numpy array. +This numpy array should be of dimensions :math:`4 \times 4` and be a valid homogeneous matrix. +If it is not a homogeneous matrix, an exception will be raised. + +:param np_array: The numpy 1D array to copy. + +)doc", py::arg("np_array")); + define_get_item_2d_array>, vpHomogeneousMatrix, double>(pyHomogeneousMatrix); +} + + + +void bindings_vpTranslationVector(py::class_> &pyTranslationVector) +{ + pyTranslationVector.def_buffer(&get_buffer_info); + + pyTranslationVector.def("numpy", [](vpTranslationVector &self) -> np_array_cf { + return py::cast(self).cast>(); + }, numpy_fn_doc_writable, py::keep_alive<0, 1>()); + + pyTranslationVector.def(py::init([](np_array_cf np_array) { + const std::vector required_shape = { 3 }; + verify_array_shape_and_dims(np_array, required_shape, "ViSP translation vector"); + const std::vector shape = np_array.request().shape; + vpTranslationVector result; + copy_data_from_np(np_array, result.data); + return result; + }), R"doc( +Construct a Translation vector by **copying** a 1D numpy array of size 3. + +:param np_array: The numpy 1D array to copy. + +)doc", py::arg("np_array")); + define_get_item_1d_array>, vpTranslationVector, double>(pyTranslationVector); +} + + +void bindings_vpColVector(py::class_> &pyColVector) +{ + pyColVector.def_buffer(&get_buffer_info); + + pyColVector.def("numpy", [](vpColVector &self) -> np_array_cf { + return py::cast(self).cast>(); + }, numpy_fn_doc_writable, py::keep_alive<0, 1>()); + + pyColVector.def(py::init([](np_array_cf np_array) { + verify_array_shape_and_dims(np_array, 1, "ViSP column vector"); + const std::vector shape = np_array.request().shape; + vpColVector result(shape[0]); + copy_data_from_np(np_array, result.data); + return result; + }), R"doc( +Construct a column vector by **copying** a 1D numpy array. + +:param np_array: The numpy 1D array to copy. + +)doc", py::arg("np_array")); + define_get_item_1d_array>, vpColVector, double>(pyColVector); + +} + +void bindings_vpRowVector(py::class_> &pyRowVector) +{ + pyRowVector.def_buffer(&get_buffer_info); + pyRowVector.def("numpy", [](vpRowVector &self) -> np_array_cf { + return np_array_cf(get_buffer_info(self), py::cast(self)); + }, numpy_fn_doc_writable, py::keep_alive<0, 1>()); + pyRowVector.def(py::init([](np_array_cf np_array) { + verify_array_shape_and_dims(np_array, 1, "ViSP row vector"); + const std::vector shape = np_array.request().shape; + vpRowVector result(shape[0]); + copy_data_from_np(np_array, result.data); + return result; + }), R"doc( +Construct a row vector by **copying** a 1D numpy array. + +:param np_array: The numpy 1D array to copy. + +)doc", py::arg("np_array")); + define_get_item_1d_array>, vpRowVector, double>(pyRowVector); +} + + +#endif diff --git a/modules/python/bindings/include/core/image_conversions.hpp b/modules/python/bindings/include/core/image_conversions.hpp new file mode 100644 index 0000000000..68099d9d15 --- /dev/null +++ b/modules/python/bindings/include/core/image_conversions.hpp @@ -0,0 +1,242 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_IMAGE_CONVERT_HPP +#define VISP_PYTHON_CORE_IMAGE_CONVERT_HPP + +#include +#include +#include + +#include + +namespace +{ +using ConversionFunction1D = void(*)(unsigned char *, unsigned char *, unsigned int); +using ConversionFunction2D = void(*)(unsigned char *, unsigned char *, unsigned int, unsigned int); +using ComputeBytesFunction = unsigned(*)(unsigned int, unsigned int); + +void call_conversion_fn(ConversionFunction2D fn, unsigned char *src, unsigned char *dest, unsigned int h, unsigned int w) +{ + fn(src, dest, h, w); +} +void call_conversion_fn(ConversionFunction1D fn, unsigned char *src, unsigned char *dest, unsigned int h, unsigned int w) +{ + fn(src, dest, h * w); +} + +template +struct SimpleConversionStruct +{ + SimpleConversionStruct(const std::string &name, ConversionFn fn, unsigned int srcBytesPerPixel, unsigned int destBytesPerPixel) : + name(name), fn(fn), srcBytesPerPixel(srcBytesPerPixel), destBytesPerPixel(destBytesPerPixel) + { } + std::string name; + ConversionFn fn; + unsigned int srcBytesPerPixel; + unsigned int destBytesPerPixel; + + void add_conversion_binding(py::class_ &pyImageConvert) + { + pyImageConvert.def_static(name.c_str(), [this](py::array_t &src, + py::array_t &dest) { + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + if (bufsrc.ndim < 2 || bufdest.ndim < 2) { + throw std::runtime_error("Expected to have src and dest arrays with at least two dimensions."); + } + if (bufsrc.shape[0] != bufdest.shape[0] || bufsrc.shape[1] != bufdest.shape[1]) { + std::stringstream ss; + ss << "src and dest must have the same number of pixels, but got src = " << shape_to_string(bufsrc.shape); + ss << "and dest = " << shape_to_string(bufdest.shape); + throw std::runtime_error(ss.str()); + } + if (srcBytesPerPixel > 1 && (bufsrc.ndim != 3 || bufsrc.shape[2] != srcBytesPerPixel)) { + std::stringstream ss; + ss << "Source array should be a 3D array of shape (H, W, " << srcBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (srcBytesPerPixel == 1 && bufsrc.ndim == 3 && bufsrc.shape[2] > 1) { + throw std::runtime_error("Source array should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { + std::stringstream ss; + ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { + throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + + + unsigned char *src_ptr = static_cast(bufsrc.ptr); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + call_conversion_fn(fn, src_ptr, dest_ptr, bufsrc.shape[0], bufsrc.shape[1]); + }, py::arg("src"), py::arg("dest")); + } + +}; + +template +struct ConversionFromYUVLike +{ + ConversionFromYUVLike(const std::string &name, ConversionFn fn, ComputeBytesFunction sourceBytesFn, unsigned int destBytesPerPixel) : + name(name), fn(fn), sourceBytesFn(sourceBytesFn), destBytesPerPixel(destBytesPerPixel) + { } + std::string name; + ConversionFn fn; + ComputeBytesFunction sourceBytesFn; + + unsigned int destBytesPerPixel; + + void add_conversion_binding(py::class_ &pyImageConvert) + { + pyImageConvert.def_static(name.c_str(), [this](py::array_t &src, + py::array_t &dest) { + py::buffer_info bufsrc = src.request(), bufdest = dest.request(); + if (bufdest.ndim < 2) { + throw std::runtime_error("Expected to have dest array with at least two dimensions."); + } + + unsigned int height = bufdest.shape[0], width = bufdest.shape[1]; + + unsigned expectedSourceBytes = sourceBytesFn(height, width); + + unsigned actualBytes = 1; + for (unsigned int i = 0; i < bufsrc.ndim; ++i) { + actualBytes *= bufsrc.shape[i]; + } + + if (actualBytes != expectedSourceBytes) { + std::stringstream ss; + ss << "Expected to have " << expectedSourceBytes << " bytes in the input array, but got " << actualBytes << " elements."; + throw std::runtime_error(ss.str()); + } + + if (destBytesPerPixel > 1 && (bufdest.ndim != 3 || bufdest.shape[2] != destBytesPerPixel)) { + std::stringstream ss; + ss << "Destination array should be a 3D array of shape (H, W, " << destBytesPerPixel << ")"; + throw std::runtime_error(ss.str()); + } + else if (destBytesPerPixel == 1 && bufdest.ndim == 3 && bufdest.shape[2] > 1) { + throw std::runtime_error("Destination should be a either a 2D array of shape H x W or a 3D array of shape (H, W, 1)"); + } + + + unsigned char *src_ptr = static_cast(bufsrc.ptr); + unsigned char *dest_ptr = static_cast(bufdest.ptr); + call_conversion_fn(fn, src_ptr, dest_ptr, bufdest.shape[0], bufdest.shape[1]); + }, py::arg("src"), py::arg("dest")); + } + +}; + +unsigned size422(unsigned h, unsigned w) +{ + return h * w + (h * (w / 2)) * 2; +} +unsigned size420(unsigned h, unsigned w) +{ + return h * w + ((h / 2) * (w / 2)) * 2; +} +unsigned size411(unsigned h, unsigned w) +{ + return h * w + ((h / 4) * (w / 4)) * 2; +} + +} + + + +void bindings_vpImageConvert(py::class_ &pyImageConvert) +{ + // Simple conversions where the size input is a single argument + { + std::vector> conversions = { + SimpleConversionStruct("YUV444ToGrey", &vpImageConvert::YUV444ToGrey, 3, 1), + SimpleConversionStruct("YUV444ToRGB", &vpImageConvert::YUV444ToRGB, 3, 3), + SimpleConversionStruct("YUV444ToRGBa", &vpImageConvert::YUV444ToRGBa, 3, 4), + SimpleConversionStruct("RGBToRGBa", static_cast(&vpImageConvert::RGBToRGBa), 3, 4), + SimpleConversionStruct("RGBaToRGB", &vpImageConvert::RGBaToRGB, 4, 3), + SimpleConversionStruct("GreyToRGB", &vpImageConvert::GreyToRGB, 1, 3), + SimpleConversionStruct("GreyToRGBa", static_cast(&vpImageConvert::GreyToRGBa), 1, 4), + SimpleConversionStruct("RGBToGrey", static_cast(&vpImageConvert::RGBToGrey), 3, 1), + }; + for (auto &conversion: conversions) { + conversion.add_conversion_binding(pyImageConvert); + } + } + + //YUV conversions + { + using Conv = ConversionFromYUVLike; + std::vector conversions = { + Conv("YUYVToRGBa", &vpImageConvert::YUYVToRGBa, &size422, 4), + Conv("YUYVToRGB", &vpImageConvert::YUYVToRGB, &size422, 3), + + Conv("YV12ToRGBa", &vpImageConvert::YV12ToRGBa, &size420, 4), + Conv("YV12ToRGB", &vpImageConvert::YV12ToRGB, &size420, 3), + Conv("YUV420ToRGBa", &vpImageConvert::YUV420ToRGBa, &size420, 4), + Conv("YUV420ToRGB", &vpImageConvert::YUV420ToRGB, &size420, 3), + + Conv("YVU9ToRGBa", &vpImageConvert::YVU9ToRGBa, &size411, 4), + Conv("YVU9ToRGB", &vpImageConvert::YVU9ToRGB, &size411, 3), + }; + for (auto &conversion: conversions) { + conversion.add_conversion_binding(pyImageConvert); + } + } + { + using Conv = ConversionFromYUVLike; + std::vector conversions = { + + Conv("YUYVToGrey", &vpImageConvert::YUYVToGrey, &size422, 1), + Conv("YUV422ToRGBa", &vpImageConvert::YUV422ToRGBa, &size422, 4), + Conv("YUV422ToRGB", &vpImageConvert::YUV422ToRGB, &size422, 3), + Conv("YUV422ToGrey", &vpImageConvert::YUV422ToGrey, &size422, 1), + Conv("YCbCrToRGBa", &vpImageConvert::YCbCrToRGBa, &size422, 4), + Conv("YCbCrToRGB", &vpImageConvert::YCbCrToRGB, &size422, 3), + Conv("YCbCrToGrey", &vpImageConvert::YCbCrToGrey, &size422, 1), + Conv("YCrCbToRGBa", &vpImageConvert::YCrCbToRGBa, &size422, 4), + Conv("YCrCbToRGB", &vpImageConvert::YCrCbToRGB, &size422, 3), + Conv("YUV420ToGrey", &vpImageConvert::YUV420ToGrey, &size420, 1), + Conv("YUV411ToRGBa", &vpImageConvert::YUV411ToRGBa, &size411, 4), + Conv("YUV411ToRGB", &vpImageConvert::YUV411ToRGB, &size411, 3), + Conv("YUV411ToGrey", &vpImageConvert::YUV411ToGrey, &size411, 1), + }; + for (auto &conversion: conversions) { + conversion.add_conversion_binding(pyImageConvert); + } + } +} + +#endif diff --git a/modules/python/bindings/include/core/images.hpp b/modules/python/bindings/include/core/images.hpp new file mode 100644 index 0000000000..53e0a08e19 --- /dev/null +++ b/modules/python/bindings/include/core/images.hpp @@ -0,0 +1,228 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_IMAGES_HPP +#define VISP_PYTHON_CORE_IMAGES_HPP + +#include +#include +#include +#include +#include +#include + +namespace +{ +const char *numpy_fn_doc_image = R"doc( + Numpy view of the underlying image data. + This numpy view can be used to directly modify the array. +)doc"; +} + +/* + * Image 2D indexing + */ +template +void define_get_item_2d_image(py::class_> &pyClass) +{ + pyClass.def("__getitem__", [](const vpImage &self, std::pair pair) -> T { + int i = pair.first, j = pair.second; + const int rows = (int)self.getHeight(), cols = (int)self.getRows(); + if (i >= rows || j >= cols || i < -rows || j < -cols) { + std::stringstream ss; + ss << "Invalid indexing into a 2D image: got indices " << shape_to_string({ i, j }) + << " but image has dimensions " << shape_to_string({ rows, cols }); + throw std::runtime_error(ss.str()); + } + if (i < 0) { + i = rows + i; + } + if (j < 0) { + j = cols + j; + } + return self[i][j]; + }); + pyClass.def("__getitem__", [](const vpImage &self, int i) -> np_array_cf { + const int rows = (int)self.getRows(); + if (i >= rows || i < -rows) { + std::stringstream ss; + ss << "Invalid indexing into a 2D image: got row index " << shape_to_string({ i }) + << " but array has " << rows << " rows"; + throw std::runtime_error(ss.str()); + } + if (i < 0) { + i = rows + i; + } + return (py::cast(self).template cast >())[py::cast(i)].template cast>(); + }); + pyClass.def("__getitem__", [](const vpImage &self, py::slice slice) -> py::array_t { + return (py::cast(self).template cast >())[slice].template cast>(); + }, py::keep_alive<0, 1>()); + pyClass.def("__getitem__", [](const vpImage &self, py::tuple tuple) { + return (py::cast(self).template cast >())[tuple].template cast>(); + }, py::keep_alive<0, 1>()); +} + +/* + * vpImage + */ +template +typename std::enable_if::value, void>::type +bindings_vpImage(py::class_> &pyImage) +{ + pyImage.def_buffer([](vpImage &image) -> py::buffer_info { + return make_array_buffer(image.bitmap, { image.getHeight(), image.getWidth() }, false); + }); + pyImage.def("numpy", [](vpImage &self) -> np_array_cf { + return py::cast(self).template cast>(); + }, numpy_fn_doc_image, py::keep_alive<0, 1>()); + + pyImage.def(py::init([](np_array_cf &np_array) { + verify_array_shape_and_dims(np_array, 2, "ViSP Image"); + const std::vector shape = np_array.request().shape; + vpImage result(shape[0], shape[1]); + copy_data_from_np(np_array, result.bitmap); + return result; + }), R"doc( +Construct an image by **copying** a 2D numpy array. + +:param np_array: The numpy array to copy. + +)doc", py::arg("np_array")); + + define_get_item_2d_image(pyImage); + + pyImage.def("__repr__", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << ""; + return ss.str(); + }); + + pyImage.def("_visp_repr", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << self; + return ss.str(); + }, R"doc(Get the full ViSP image string representation.)doc"); + +} + +template +typename std::enable_if::value, void>::type +bindings_vpImage(py::class_> &pyImage) +{ + using NpRep = unsigned char; + static_assert(sizeof(T) == 4 * sizeof(NpRep)); + pyImage.def_buffer([](vpImage &image) -> py::buffer_info { + return make_array_buffer(reinterpret_cast(image.bitmap), { image.getHeight(), image.getWidth(), 4 }, false); + }); + pyImage.def("numpy", [](vpImage &self) -> np_array_cf { + return py::cast(self).template cast>(); + }, numpy_fn_doc_image, py::keep_alive<0, 1>()); + + pyImage.def(py::init([](np_array_cf &np_array) { + verify_array_shape_and_dims(np_array, 3, "ViSP RGBa image"); + const std::vector shape = np_array.request().shape; + if (shape[2] != 4) { + throw std::runtime_error("Tried to copy a 3D numpy array that does not have 4 elements per pixel into a ViSP RGBA image"); + } + vpImage result(shape[0], shape[1]); + copy_data_from_np(np_array, (NpRep *)result.bitmap); + return result; + }), R"doc( +Construct an image by **copying** a 3D numpy array. this numpy array should be of the form :math:`H \times W \times 4` +where the 4 denotes the red, green, blue and alpha components of the image. + +:param np_array: The numpy array to copy. + +)doc", py::arg("np_array")); + define_get_item_2d_image(pyImage); + + pyImage.def("__repr__", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << ""; + return ss.str(); + }); + + pyImage.def("_visp_repr", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << self; + return ss.str(); + }, R"doc(Get the full ViSP image string representation.)doc"); + +} +template +typename std::enable_if::value, void>::type +bindings_vpImage(py::class_> &pyImage) +{ + using NpRep = float; + static_assert(sizeof(T) == 3 * sizeof(NpRep)); + pyImage.def_buffer([](vpImage &image) -> py::buffer_info { + return make_array_buffer(reinterpret_cast(image.bitmap), { image.getHeight(), image.getWidth(), 3 }, false); + }); + + pyImage.def("numpy", [](vpImage &self) -> np_array_cf { + return py::cast(self).template cast>(); + }, numpy_fn_doc_image, py::keep_alive<0, 1>()); + + pyImage.def(py::init([](np_array_cf &np_array) { + verify_array_shape_and_dims(np_array, 3, "ViSP RGBa image"); + const std::vector shape = np_array.request().shape; + if (shape[2] != 3) { + throw std::runtime_error("Tried to copy a 3D numpy array that does not have 3 elements per pixel into a ViSP RGBf image"); + } + vpImage result(shape[0], shape[1]); + copy_data_from_np(np_array, (NpRep *)result.bitmap); + return result; + }), R"doc( +Construct an image by **copying** a 3D numpy array. this numpy array should be of the form :math:`H \times W \times 3` +where the 3 denotes the red, green and blue components of the image. + +:param np_array: The numpy array to copy. + +)doc", py::arg("np_array")); + define_get_item_2d_image(pyImage); + + pyImage.def("__repr__", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << ""; + return ss.str(); + }); + + pyImage.def("_visp_repr", [](const vpImage &self) -> std::string { + std::stringstream ss; + ss << self; + return ss.str(); + }, R"doc(Get the full ViSP image string representation.)doc"); +} + +#endif diff --git a/modules/python/bindings/include/core/pixel_meter.hpp b/modules/python/bindings/include/core/pixel_meter.hpp new file mode 100644 index 0000000000..c55b272e2d --- /dev/null +++ b/modules/python/bindings/include/core/pixel_meter.hpp @@ -0,0 +1,172 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_PIXEL_METER_HPP +#define VISP_PYTHON_CORE_PIXEL_METER_HPP + +#include +#include +#include + +#include +#include + +#include "core/utils.hpp" + +void bindings_vpPixelMeterConversion(py::class_ &pyPM) +{ + pyPM.def_static("convertPoints", [](const vpCameraParameters &cam, const py::array_t &us, const py::array_t &vs) { + py::buffer_info bufu = us.request(), bufv = vs.request(); + if (bufu.ndim != bufv.ndim || bufu.shape != bufv.shape) { + std::stringstream ss; + ss << "us and vs must have the same number of dimensions and same number of elements, but got us = " << shape_to_string(bufu.shape); + ss << "and vs = " << shape_to_string(bufv.shape); + throw std::runtime_error(ss.str()); + } + py::array_t xs(bufu.shape); + py::array_t ys(bufv.shape); + + const double *u_ptr = static_cast(bufu.ptr); + const double *v_ptr = static_cast(bufv.ptr); + double *x_ptr = static_cast(xs.request().ptr); + double *y_ptr = static_cast(ys.request().ptr); + + for (ssize_t i = 0; i < bufu.size; ++i) { + vpPixelMeterConversion::convertPoint(cam, u_ptr[i], v_ptr[i], x_ptr[i], y_ptr[i]); + } + + return std::make_tuple(std::move(xs), std::move(ys)); + + }, R"doc( +Convert a set of 2D pixel coordinates to normalized coordinates. + +:param cam: The camera intrinsics with which to convert pixels to normalized coordinates. + +:param us: The pixel coordinates along the horizontal axis. + +:param vs: The pixel coordinates along the vertical axis. + +:raises RuntimeError: If us and vs do not have the same dimensions and shape. + +:return: A tuple containing the x and y normalized coordinates of the input pixels. +Both arrays have the same shape as xs and ys. + +Example usage: + +.. testcode:: + + from visp.core import PixelMeterConversion, CameraParameters + import numpy as np + + h, w = 240, 320 + cam = CameraParameters(px=600, py=600, u0=320, v0=240) + + vs, us = np.meshgrid(range(h), range(w), indexing='ij') # vs and us are 2D arrays + vs.shape == (h, w) and us.shape == (h, w) + + xs, ys = PixelMeterConversion.convertPoints(cam, us, vs) + # xs and ys have the same shape as us and vs + assert xs.shape == (h, w) and ys.shape == (h, w) + + # Converting a numpy array to normalized coords has the same effect as calling on a single image point + u, v = 120, 120 + x, y = PixelMeterConversion.convertPoint(cam, u, v) + assert x == xs[v, u] and y == ys[v, u] + +)doc", py::arg("cam"), py::arg("us"), py::arg("vs")); +} + +void bindings_vpMeterPixelConversion(py::class_ &pyMP) +{ + pyMP.def_static("convertPoints", [](const vpCameraParameters &cam, const py::array_t &xs, const py::array_t &ys) { + py::buffer_info bufx = xs.request(), bufy = ys.request(); + if (bufx.ndim != bufy.ndim || bufx.shape != bufy.shape) { + std::stringstream ss; + ss << "xs and ys must have the same number of dimensions and same number of elements, but got xs = " << shape_to_string(bufx.shape); + ss << "and ys = " << shape_to_string(bufy.shape); + throw std::runtime_error(ss.str()); + } + py::array_t us(bufx.shape); + py::array_t vs(bufy.shape); + + const double *x_ptr = static_cast(bufx.ptr); + const double *y_ptr = static_cast(bufy.ptr); + double *u_ptr = static_cast(us.request().ptr); + double *v_ptr = static_cast(vs.request().ptr); + + for (ssize_t i = 0; i < bufx.size; ++i) { + vpMeterPixelConversion::convertPoint(cam, x_ptr[i], y_ptr[i], u_ptr[i], v_ptr[i]); + } + + return std::make_tuple(std::move(us), std::move(vs)); + + }, R"doc( +Convert a set of 2D normalized coordinates to pixel coordinates. + +:param cam: The camera intrinsics with which to convert normalized coordinates to pixels. + +:param xs: The normalized coordinates along the horizontal axis. + +:param ys: The normalized coordinates along the vertical axis. + +:raises RuntimeError: If xs and ys do not have the same dimensions and shape. + +:return: A tuple containing the u,v pixel coordinate arrays of the input normalized coordinates. +Both arrays have the same shape as xs and ys. + +Example usage: + +.. testcode:: + + from visp.core import MeterPixelConversion, CameraParameters + import numpy as np + + cam = CameraParameters(px=600, py=600, u0=320, v0=240) + n = 20 + xs, ys = np.random.rand(n), np.random.rand(n) + + + us, vs = MeterPixelConversion.convertPoints(cam, xs, ys) + + # xs and ys have the same shape as us and vs + assert us.shape == (n,) and vs.shape == (n,) + + # Converting a numpy array to pixel coords has the same effect as calling on a single image point + x, y = xs[0], ys[0] + u, v = MeterPixelConversion.convertPoint(cam, x, y) + assert u == us[0] and v == vs[0] + +)doc", py::arg("cam"), py::arg("xs"), py::arg("ys")); +} + +#endif diff --git a/modules/python/bindings/include/core/utils.hpp b/modules/python/bindings/include/core/utils.hpp new file mode 100644 index 0000000000..3bb413bdf0 --- /dev/null +++ b/modules/python/bindings/include/core/utils.hpp @@ -0,0 +1,138 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_CORE_UTILS_HPP +#define VISP_PYTHON_CORE_UTILS_HPP + +#include +#include + +#include +#include +#include + +namespace py = pybind11; + +template +using np_array_cf = py::array_t; + +/* + * Create a buffer info for a row major array + */ +template +py::buffer_info make_array_buffer(T *data, std::array dims, bool readonly) +{ + std::array strides; + for (unsigned i = 0; i < N; i++) { + unsigned s = sizeof(T); + for (unsigned j = i + 1; j < N; ++j) { + s *= dims[j]; + } + strides[i] = s; + } + return py::buffer_info( + data, /* Pointer to data (nullptr -> ask NumPy to allocate!) */ + sizeof(T), /* Size of one item */ + py::format_descriptor::value, /* Buffer format */ + N, /* How many dimensions? */ + dims, /* Number of elements for each dimension */ + strides, /* Strides for each dimension */ + readonly + ); +} + +std::string shape_to_string(const std::vector &shape) +{ + std::stringstream ss; + ss << "("; + for (int i = 0; i < int(shape.size()) - 1; ++i) { + ss << shape[i] << ","; + } + if (shape.size() > 0) { + ss << shape[shape.size() - 1]; + } + ss << ")"; + return ss.str(); +} + +template +void verify_array_shape_and_dims(np_array_cf np_array, unsigned dims, const char *class_name) +{ + py::buffer_info buffer = np_array.request(); + std::vector shape = buffer.shape; + if (shape.size() != dims) { + std::stringstream ss; + ss << "Tried to instanciate " << class_name + << " that expects a " << dims << "D array but got a numpy array of shape " + << shape_to_string(shape); + + throw std::runtime_error(ss.str()); + } +} +template +void verify_array_shape_and_dims(np_array_cf np_array, std::vector expected_dims, const char *class_name) +{ + verify_array_shape_and_dims(np_array, expected_dims.size(), class_name); + py::buffer_info buffer = np_array.request(); + std::vector shape = buffer.shape; + bool invalid_shape = false; + for (unsigned int i = 0; i < expected_dims.size(); ++i) { + if (shape[i] != expected_dims[i]) { + invalid_shape = true; + break; + } + } + if (invalid_shape) { + std::stringstream ss; + ss << "Tried to instanciate " << class_name + << " that expects an array of dimensions " << shape_to_string(expected_dims) + << " but got a numpy array of shape " << shape_to_string(shape); + + throw std::runtime_error(ss.str()); + } +} +template +void copy_data_from_np(np_array_cf src, Item *dest) +{ + py::buffer_info buffer = src.request(); + std::vector shape = buffer.shape; + unsigned int elements = 1; + for (ssize_t dim : shape) { + elements *= dim; + } + const Item *data = (Item *)buffer.ptr; + std::memcpy(dest, data, elements * sizeof(Item)); + +} + +#endif diff --git a/modules/python/bindings/include/mbt.hpp b/modules/python/bindings/include/mbt.hpp new file mode 100644 index 0000000000..2dc974c728 --- /dev/null +++ b/modules/python/bindings/include/mbt.hpp @@ -0,0 +1,83 @@ +/* + * ViSP, open source Visual Servoing Platform software. + * Copyright (C) 2005 - 2023 by Inria. All rights reserved. + * + * This software is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * See the file LICENSE.txt at the root directory of this source + * distribution for additional information about the GNU GPL. + * + * For using ViSP with software that can not be combined with the GNU + * GPL, please contact Inria about acquiring a ViSP Professional + * Edition License. + * + * See https://visp.inria.fr for more information. + * + * This software was developed at: + * Inria Rennes - Bretagne Atlantique + * Campus Universitaire de Beaulieu + * 35042 Rennes Cedex + * France + * + * If you have questions regarding the use of this file, please contact + * Inria at visp@inria.fr + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * Description: + * Python bindings. + */ + +#ifndef VISP_PYTHON_MBT_HPP +#define VISP_PYTHON_MBT_HPP + +#include +#include +#include +#include + +namespace py = pybind11; + +void bindings_vpMbGenericTracker(py::class_ &pyMbGenericTracker) +{ + pyMbGenericTracker.def("track", [](vpMbGenericTracker &self, std::map *> &mapOfImages, + std::map> &mapOfPointClouds) { + std::map mapOfWidths, mapOfHeights; + std::map mapOfVectors; + for (const auto &point_cloud_pair: mapOfPointClouds) { + + py::buffer_info buffer = point_cloud_pair.second.request(); + if (buffer.ndim != 3 and buffer.shape[2] != 3) { + std::stringstream ss; + ss << "Pointcloud error: pointcloud at key: " << point_cloud_pair.first << + " should be a 3D numpy array of dimensions H X W x 3"; + throw std::runtime_error(ss.str()); + } + const auto shape = buffer.shape; + mapOfHeights[point_cloud_pair.first] = shape[0]; + mapOfWidths[point_cloud_pair.first] = shape[1]; + vpMatrix pc(shape[0] * shape[1], 3); + const double *data = point_cloud_pair.second.unchecked<3>().data(0, 0, 0); + memcpy(pc.data, data, shape[0] * shape[1] * 3 * sizeof(double)); + mapOfVectors[point_cloud_pair.first] = std::move(pc); + } + std::map mapOfVectorPtrs; + for (const auto &p: mapOfVectors) { + mapOfVectorPtrs[p.first] = &(p.second); + } + self.track(mapOfImages, mapOfVectorPtrs, mapOfWidths, mapOfHeights); + }, R"doc( +Perform tracking, with point clouds being represented as numpy arrays + +:param mapOfImages: Dictionary mapping from a camera name to a grayscale image + +:param: mapOfPointclouds: Dictionary mapping from a camera name to a point cloud. +A point cloud is represented as a H x W x 3 double NumPy array. + +)doc", py::arg("mapOfImages"), py::arg("mapOfPointclouds")); +} + +#endif diff --git a/modules/python/bindings/setup.py.in b/modules/python/bindings/setup.py.in new file mode 100644 index 0000000000..8d72984d6d --- /dev/null +++ b/modules/python/bindings/setup.py.in @@ -0,0 +1,85 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +import os +import re +import subprocess +import sys +from pathlib import Path + +import setuptools +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext +from setuptools.command.install import install +from distutils.command import build as build_module +from pathlib import Path + +package_name = 'visp' +version = '@VISP_PYTHON_VERSION@' + +package_data = {} + +# Inspired by the pyrealsense2 binding +# Include a .so lib that is already compiled into the package +if os.name == 'posix': + package_data[''] = ['*.so', 'py.typed'] +else: + package_data[''] = ['*.pyd', '*.dll', 'py.typed'] + + +# This creates a list which is empty but returns a length of 1. +# Should make the wheel a binary distribution and platlib compliant. +class EmptyListWithLength(list): + def __len__(self): + return 1 + +setup( + name=package_name, + version=version, + author="Samuel Felton", + packages=['', 'visp'], + author_email="samuel.felton@irisa.fr", + description="Python wrapper for the Visual Servoing Platform", + long_description="", + setup_requires=[ + "setuptools" + ], + ext_modules=EmptyListWithLength(), + zip_safe=False, + include_package_data=True, + package_data=package_data, + extras_require={"test": ["pytest>=6.0"]}, + python_requires=">=3.7", +) diff --git a/modules/python/bindings/visp/__init__.py b/modules/python/bindings/visp/__init__.py new file mode 100644 index 0000000000..4807a8bea3 --- /dev/null +++ b/modules/python/bindings/visp/__init__.py @@ -0,0 +1,49 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +import sys +# import os +# sys.path.append(os.path.dirname(__file__)) +# print(sys.path) + + +from .bindings import * +import _visp + +# Fake module names +for k in _visp.__dict__: + from types import ModuleType + if isinstance(_visp.__dict__[k], ModuleType): + sys.modules[f'{__name__}.{k}'] = _visp.__dict__[k] diff --git a/modules/python/bindings/visp/bindings.py b/modules/python/bindings/visp/bindings.py new file mode 100644 index 0000000000..05b61273e7 --- /dev/null +++ b/modules/python/bindings/visp/bindings.py @@ -0,0 +1,36 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +from _visp import * diff --git a/modules/python/bindings/visp/py.typed b/modules/python/bindings/visp/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/python/config/ar.json b/modules/python/config/ar.json new file mode 100644 index 0000000000..9b6b904d82 --- /dev/null +++ b/modules/python/config/ar.json @@ -0,0 +1,7 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": {}, + "enums": {} +} \ No newline at end of file diff --git a/modules/python/config/blob.json b/modules/python/config/blob.json new file mode 100644 index 0000000000..51e9e0a736 --- /dev/null +++ b/modules/python/config/blob.json @@ -0,0 +1,103 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": ["blob.hpp"], + "classes": { + "vpDot": { + "methods": [ + { + "static": true, + "signature": "void display(const vpImage&, const vpImagePoint&, const std::list&, vpColor, unsigned int)", + "custom_name": "displayDot" + }, + { + "static": true, + "signature": "void display(const vpImage&, const vpImagePoint&, const std::list&, vpColor, unsigned int)", + "custom_name": "displayDot" + } + ] + }, + "vpDot2": { + "additional_bindings": "bindings_vpDot2", + "methods": [ + { + "static": true, + "signature": "void display(const vpImage&, const vpImagePoint&, const std::list&, vpColor, unsigned int)", + "custom_name": "displayDot" + }, + { + "static": true, + "signature": "void display(const vpImage&, const vpImagePoint&, const std::list&, vpColor, unsigned int)", + "custom_name": "displayDot" + }, + { + "static": true, + "signature": "vpMatrix defineDots(vpDot2[], const unsigned int&, const std::string&, vpImage&, vpColor, bool)", + "ignore": true + }, + { + "static": true, + "signature": "void trackAndDisplay(vpDot2[], const unsigned int&, vpImage&, std::vector&, vpImagePoint*)", + "ignore": true + }, + { + "static": false, + "signature": "void getFreemanChain(std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + }, + { + "static": false, + "signature": "void searchDotsInArea(const vpImage&, int, int, unsigned int, unsigned int, std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + true, + true, + true, + true, + false + ], + "param_is_output": [ + false, + false, + false, + false, + false, + true + ] + }, + { + "static": false, + "signature": "void searchDotsInArea(const vpImage&, std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false + ], + "param_is_output": [ + false, + true + ] + }, + { + "static": false, + "signature": "void getEdges(std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + } + ] + } + }, + "enums": {} +} diff --git a/modules/python/config/core.json b/modules/python/config/core.json new file mode 100644 index 0000000000..1701b70c7f --- /dev/null +++ b/modules/python/config/core.json @@ -0,0 +1,774 @@ +{ + "ignored_headers": ["vpGEMM.h", "vpDebug.h"], + "ignored_classes": ["vpException", "vpImageException", "vpTrackingException", + "vpFrameGrabberException", "vpIoException", + "vpDisplayException", "vpMatrixException"], + "user_defined_headers": ["core.hpp"], + "enums": { + "vpMunkres::STEP_T": { + "ignore": true + }, + "vpMunkres::ZERO_T": { + "ignore": true + } + }, + "classes": { + "vpIoTools": { + "ignored_attributes": ["separator"] + }, + "vpArray2D": { + "additional_bindings": "bindings_vpArray2D", + "use_buffer_protocol": true, + "specializations": [ + { + "python_name": "ArrayDouble2D", + "arguments": ["double"] + } + ], + "methods": + [ + { + "static": true, + "signature": "void insert(const vpArray2D &, const vpArray2D &, vpArray2D &, unsigned int, unsigned int)", + "custom_name": "insertStatic" + } + ] + }, + "vpMath" :{ + "methods": [ + { + "static": true, + "signature": "double lineFitting(const std::vector&, double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false, + false, + false + ], + "param_is_output": [ + false, + true, + true, + true + ] + } + ] + }, + "vpImage": { + "ignore_repr": true, + "additional_bindings": "bindings_vpImage", + "use_buffer_protocol": true, + "specializations": [ + { + "python_name": "ImageGray", + "arguments": ["unsigned char"] + }, + { + "python_name": "ImageFloat", + "arguments": ["float"] + }, + { + "python_name": "ImageDouble", + "arguments": ["double"] + }, + { + "python_name": "ImageUInt16", + "arguments": ["uint16_t"] + }, + { + "python_name": "ImageRGBa", + "arguments": ["vpRGBa"] + }, + { + "python_name": "ImageRGBf", + "arguments": ["vpRGBf"] + } + ], + "methods": + [ + { + "static": true, + "signature": "void insert(const vpArray2D &, const vpArray2D &, vpArray2D &, unsigned int, unsigned int)", + "custom_name": "insertStatic" + } + ] + }, + "vpTranslationVector": { + "additional_bindings": "bindings_vpTranslationVector", + "methods": + [ + { + "static": true, + "signature": "vpMatrix skew(const vpTranslationVector &)", + "custom_name": "skewOf" + }, + { + "static": true, + "signature": "void skew(const vpTranslationVector &, vpMatrix&)", + "custom_name": "skewOf" + } + ] + }, + "vpColVector": { + "additional_bindings": "bindings_vpColVector", + "use_buffer_protocol": true, + "methods": [ + { + "static": true, + "signature": "vpColVector stack(const vpColVector &, const vpColVector &)", + "custom_name": "stackVectors" + }, + { + "static": true, + "signature": "void stack(const vpColVector &, const vpColVector &, vpColVector &)", + "custom_name": "stackVectors" + } + ] + }, + "vpRowVector": { + "additional_bindings": "bindings_vpRowVector", + "use_buffer_protocol": true, + "methods": [ + { + "static": true, + "signature": "vpRowVector stack(const vpRowVector &, const vpRowVector &)", + "custom_name": "stackVectors" + }, + { + "static": true, + "signature": "void stack(const vpRowVector &, const vpRowVector &, vpRowVector &)", + "custom_name": "stackVectors" + } + ] + }, + "vpMatrix": { + "ignore_repr": true, + "additional_bindings": "bindings_vpMatrix", + "use_buffer_protocol": true, + "methods": + [ + { + + "static": true, + "signature": "vpMatrix insert(const vpMatrix &, const vpMatrix &, unsigned int , unsigned int)", + "custom_name": "insertMatrixInMatrix" + }, + { + + "static": true, + "signature": "void insert(const vpMatrix &, const vpMatrix &, vpMatrix &, unsigned int , unsigned int)", + "custom_name": "insertMatrixInMatrix" + }, + { + + "static": true, + "signature": "void kron(const vpMatrix &, const vpMatrix &, vpMatrix &)", + "custom_name": "kronStatic" + }, + { + + "static": true, + "signature": "vpMatrix kron(const vpMatrix &, const vpMatrix &)", + "custom_name": "kronStatic" + }, + { + + "signature": "vpMatrix stack(const vpMatrix &, const vpMatrix &)", + "static": true, + "custom_name": "stackMatrices" + }, + { + "static": true, + "signature": "vpMatrix stack(const vpMatrix &, const vpRowVector &)", + "custom_name": "stackRow" + }, + { + + "signature": "vpMatrix stack(const vpMatrix &, const vpColVector &)", + "static": true, + "custom_name": "stackColumn" + }, + { + "signature": "void stack(const vpMatrix &, const vpMatrix &, vpMatrix &)", + "static": true, + "custom_name": "stackMatrices" + }, + { + "signature": "void stack(const vpMatrix &, const vpRowVector &, vpMatrix &)", + "static": true, + "custom_name": "stackRow" + }, + { + "signature": "void stack(const vpMatrix &, const vpColVector &, vpMatrix &)", + "static": true, + "custom_name": "stackColumn" + } + ] + }, + "vpRotationMatrix": { + "additional_bindings": "bindings_vpRotationMatrix", + "use_buffer_protocol": true + }, + "vpHomogeneousMatrix": { + "additional_bindings": "bindings_vpHomogeneousMatrix", + "use_buffer_protocol": true, + "methods": [ + { + "static": false, + "signature": "void convert(std::vector&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + }, + { + "static": false, + "signature": "void convert(std::vector&)", + "ignore": true + } + ] + }, + "vpThetaUVector": { + "methods": [ + { + "static": false, + "signature": "void extract(double&, vpColVector&)", + "use_default_param_policy": false, + "param_is_input": [ + false, + false + ], + "param_is_output": [ + true, + true + ] + } + ] + }, + "vpPolygon": { + "methods": + [ + { + "static": true, + "signature": "bool isInside(const std::vector&, const double&, const double&, const vpPolygon::PointInPolygonMethod&)", + "custom_name": "isInsideFromPoints" + } + ] + }, + "vpPolygon3D": { + "methods": [ + { + "static": true, + "signature": "void getClippedPolygon(const std::vector&, std::vector&, const vpHomogeneousMatrix&, const unsigned int&, const vpCameraParameters&, const double&, const double&)", + "use_default_param_policy": false, + "param_is_input": [true, false, true, true, true, true, true], + "param_is_output": [false, true, false, false, false, false, false] + }, + { + "static": false, + "signature": "void getPolygonClipped(std::vector&)", + "use_default_param_policy": false, + "param_is_input": [false], + "param_is_output": [true] + }, + { + "static": false, + "signature": "void getPolygonClipped(std::vector>&)", + "custom_name": "getPolygonClippedWithInfo", + "use_default_param_policy": false, + "param_is_input": [false], + "param_is_output": [true] + }, + { + "static": false, + "signature": "void getRoiClipped(const vpCameraParameters&, std::vector>&, const vpHomogeneousMatrix&)", + "use_default_param_policy": false, + "param_is_input": [true, false, true], + "param_is_output": [false, true, false] + }, + { + "static": false, + "signature": "void getRoiClipped(const vpCameraParameters&, std::vector>&)", + "use_default_param_policy": false, + "param_is_input": [true, false], + "param_is_output": [false, true] + }, + { + "static": false, + "signature": "void getRoiClipped(const vpCameraParameters&, std::vector&, const vpHomogeneousMatrix&)", + "use_default_param_policy": false, + "param_is_input": [true, false, true], + "param_is_output": [false, true, false] + }, + { + "static": false, + "signature": "void getRoiClipped(const vpCameraParameters&, std::vector&)", + "use_default_param_policy": false, + "param_is_input": [true, false], + "param_is_output": [false, true] + } + ] + }, + "vpPoint": { + "methods": + [ + { + "static": false, + "ignore": true, + "signature": "void getWorldCoordinates(std::vector&)" + }, + { + "static": false, + "ignore": true, + "signature": "void getWorldCoordinates(double&, double&, double&)" + } + + ] + }, + "vpBSpline": { + "methods": + [ + { + "static": true, + "signature": "unsigned int findSpan(double, unsigned int, std::vector &)", + "custom_name": "findSpanFromSpline" + }, + { + "static": true, + "signature": "vpImagePoint computeCurvePoint(double, unsigned int, unsigned int, std::vector &, std::vector&)", + "custom_name": "computeCurvePointFromSpline" + } + ] + }, + "vpQuadProg": { + "methods": + [ + { + "static": true, + "signature": "bool solveQPe(const vpMatrix &, const vpColVector &, vpMatrix, vpColVector, vpColVector &, const double &)", + "custom_name": "solveQPeStatic" + } + ] + }, + "vpImageTools": { + "methods": + [ + { + "static": true, + "signature": "void warpImage(const vpImage&, const vpMatrix&, vpImage&, const vpImageTools::vpImageInterpolationType&, bool, bool)", + "specializations": + [ + ["unsigned char"], + ["vpRGBa"] + ] + } + ] + }, + "vpImageConvert": { + "additional_bindings": "bindings_vpImageConvert", + "methods": + [ + { + "static": true, + "signature": "void RGBaToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void RGBToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void RGBToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int, bool)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV444ToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV444ToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV444ToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void GreyToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void GreyToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void GreyToRGB(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void GreyToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUYVToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUYVToRGB(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUYVToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV411ToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV411ToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV411ToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV422ToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV422ToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV422ToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV420ToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV420ToRGB(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YUV420ToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YV12ToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YV12ToRGB(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YVU9ToRGBa(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YVU9ToRGB(unsigned char*, unsigned char*, unsigned int, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YCbCrToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YCbCrToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YCbCrToGrey(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YCrCbToRGB(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + }, + { + "static": true, + "signature": "void YCrCbToRGBa(unsigned char*, unsigned char*, unsigned int)", + "ignore": true + } + ] + }, + "vpConvert": { + "methods": [ + { + "static": true, + "signature": "void convertToOpenCV(const std::vector&, std::vector&, bool)", + "ignore" :true + }, + { + "static": true, + "signature": "void convertToOpenCV(const std::vector&, std::vector&, bool)", + "ignore" :true + }, + { + "static": true, + "signature": "void convertToOpenCV(const std::vector&, std::vector&)", + "ignore" :true + }, + { + "static": true, + "signature": "void convertToOpenCV(const std::vector&, std::vector&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&, bool)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&, bool)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertFromOpenCV(const std::vector&, std::vector&)", + "ignore": true + } + ] + }, + "vpDisplay": { + "methods": + [ + { + "static": true, + "signature": "unsigned int getDownScalingFactor(const vpImage &)", + "custom_name": "getImageDownScalingFactor" + }, + { + "static": true, + "signature": "unsigned int getDownScalingFactor(const vpImage &)", + "custom_name": "getImageDownScalingFactor" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, const vpImageCircle &, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, const vpImagePoint &, unsigned int, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, int, int, unsigned int, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, const vpImageCircle &, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, const vpImagePoint &, unsigned int, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + }, + { + "static": true, + "signature": "void displayCircle(const vpImage &, int, int, unsigned int, const vpColor &, bool, unsigned int)", + "custom_name": "displayCircleStatic" + } + ] + }, + "vpMomentDatabase": { + "methods": [ + { + "static": false, + "signature": "const vpMoment& get(const std::string&, bool&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false + ], + "param_is_output": [ + false, + true + ] + } + ] + }, + "vpPixelMeterConversion": { + "additional_bindings": "bindings_vpPixelMeterConversion", + "methods": [ + { + "static": true, + "signature": "void convertEllipse(const vpCameraParameters&, const vpImagePoint&, double, double, double, double&, double&, double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true, true, true, true, true, false, false, false, false, false], + "param_is_output": [false, false, false, false, false, true, true, true, true, true] + }, + { + "static": true, + "signature": "void convertLine(const vpCameraParameters&, const double&, const double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,true,false,false], + "param_is_output": [false,false,false,true,true] + }, + { + "static": true, + "signature": "void convertPoint(const vpCameraParameters&, const double&, const double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,true,false,false], + "param_is_output": [false,false,false,true,true] + }, + { + "static": true, + "signature": "void convertPoint(const vpCameraParameters&, const vpImagePoint&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,false,false], + "param_is_output": [false,false,true,true] + }, + { + "static": true, + "signature": "void convertEllipse(const cv::Mat&, const cv::Mat&, const vpImagePoint&, double, double, double, double&, double&, double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertLine(const cv::Mat&, const double&, const double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertPoint(const cv::Mat&, const cv::Mat&, const double&, const double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertPoint(const cv::Mat&, const cv::Mat&, const vpImagePoint&, double&, double&)", + "ignore": true + } + ] + + }, + "vpMeterPixelConversion": { + "additional_bindings": "bindings_vpMeterPixelConversion", + "methods": [ + { + "static": true, + "signature": "void convertEllipse(const vpCameraParameters&, const vpImagePoint&, double, double, double, double&, double&, double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true, true, true, true, true, false, false, false, false, false], + "param_is_output": [false, false, false, false, false, true, true, true, true, true] + }, + { + "static": true, + "signature": "void convertLine(const vpCameraParameters&, const double&, const double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,true,false,false], + "param_is_output": [false,false,false,true,true] + }, + { + "static": true, + "signature": "void convertPoint(const vpCameraParameters&, const double&, const double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,true,false,false], + "param_is_output": [false,false,false,true,true] + }, + { + "static": true, + "signature": "void convertPoint(const vpCameraParameters&, const vpImagePoint&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true,true,false,false], + "param_is_output": [false,false,true,true] + }, + { + "static": true, + "signature": "void convertEllipse(const cv::Mat&, const cv::Mat&, const vpImagePoint&, double, double, double, double&, double&, double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertLine(const cv::Mat&, const double&, const double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertPoint(const cv::Mat&, const cv::Mat&, const double&, const double&, double&, double&)", + "ignore": true + }, + { + "static": true, + "signature": "void convertPoint(const cv::Mat&, const cv::Mat&, const vpImagePoint&, double&, double&)", + "ignore": true + } + ] + }, + "vpCircle": { + "methods": [ + { + "static": true, + "signature": "void computeIntersectionPoint(const vpCircle&, const vpCameraParameters&, const double&, const double&, double&, double&)", + "use_default_param_policy": false, + "param_is_input": [true, true, true, true, false, false], + "param_is_output": [false, false, false, false, true, true] + } + ] + } + + } + +} diff --git a/modules/python/config/detection.json b/modules/python/config/detection.json new file mode 100644 index 0000000000..78946149cf --- /dev/null +++ b/modules/python/config/detection.json @@ -0,0 +1,3 @@ +{ + "required_headers": ["visp3/core/vpPoint.h"] +} \ No newline at end of file diff --git a/modules/python/config/dnn_tracker.json b/modules/python/config/dnn_tracker.json new file mode 100644 index 0000000000..294b0d30ac --- /dev/null +++ b/modules/python/config/dnn_tracker.json @@ -0,0 +1,7 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": {}, + "enums": {} +} diff --git a/modules/python/config/gui.json b/modules/python/config/gui.json new file mode 100644 index 0000000000..294b0d30ac --- /dev/null +++ b/modules/python/config/gui.json @@ -0,0 +1,7 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": {}, + "enums": {} +} diff --git a/modules/python/config/imgproc.json b/modules/python/config/imgproc.json new file mode 100644 index 0000000000..307aae2e31 --- /dev/null +++ b/modules/python/config/imgproc.json @@ -0,0 +1,26 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": {}, + "functions": [ + { + "static": false, + "signature": "void findContours(const vpImage&, vp::vpContour&, std::vector>&, const vp::vpContourRetrievalType&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + true, + false, + true + ], + "param_is_output": [ + false, + false, + true, + false + ] + } + ], + "enums": {} +} diff --git a/modules/python/config/io.json b/modules/python/config/io.json new file mode 100644 index 0000000000..838f1963aa --- /dev/null +++ b/modules/python/config/io.json @@ -0,0 +1,22 @@ +{ + "ignored_headers": ["vpParallelPortException.h"], + "ignored_classes": ["vpJsonArgumentParser", "vpParseArgv"], + + "classes": { + "vpParallelPort": { + "methods": [ + { + "static": false, + "signature": "void sendData(unsigned char&)", + "use_default_param_policy": false, + "param_is_input": [ + true + ], + "param_is_output": [ + false + ] + } + ] + } + } +} \ No newline at end of file diff --git a/modules/python/config/klt.json b/modules/python/config/klt.json new file mode 100644 index 0000000000..ade21dbade --- /dev/null +++ b/modules/python/config/klt.json @@ -0,0 +1,34 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": { + "vpKltOpencv": { + "methods": [ + { + "static": false, + "signature": "void display(const vpImage&, const vpColor&, unsigned int)", + "custom_name": "displaySelf" + }, + { + "static": false, + "signature": "void getFeature(const int&, long&, float&, float&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false, + false, + false + ], + "param_is_output": [ + false, + true, + true, + true + ] + } + ] + } + }, + "enums": {} +} diff --git a/modules/python/config/mbt.json b/modules/python/config/mbt.json new file mode 100644 index 0000000000..c189a5cb30 --- /dev/null +++ b/modules/python/config/mbt.json @@ -0,0 +1,13 @@ +{ + + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": ["mbt.hpp"], + "classes": { + + "vpMbGenericTracker": { + "additional_bindings": "bindings_vpMbGenericTracker" + } + }, + "enums": {} +} diff --git a/modules/python/config/me.json b/modules/python/config/me.json new file mode 100644 index 0000000000..f481830444 --- /dev/null +++ b/modules/python/config/me.json @@ -0,0 +1,65 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "enums": {}, + "classes": { + "vpMeSite": { + "methods": [ + { + "static": true, + "signature": "void display(const vpImage &, const double &, const double &, const vpMeSite::vpMeSiteState&)", + "custom_name": "displayMeSite" + }, + { + "static": true, + "signature": "void display(const vpImage &, const double &, const double &, const vpMeSite::vpMeSiteState&)", + "custom_name": "displayMeSite" + } + ] + }, + "vpNurbs": { + "methods": [ + { + "static": true, + "signature": "unsigned int removeCurveKnot(double, unsigned int, unsigned int, double, unsigned int, unsigned int, std::vector &, std::vector &, std::vector &)", + "custom_name": "removeCurveKnotStatic" + }, + { + "static": true, + "signature": "void globalCurveInterp(std::vector &, unsigned int, std::vector &, std::vector &, std::vector &)", + "custom_name": "globalCurveInterpStatic" + }, + { + "static": true, + "signature": "void globalCurveApprox(std::vector &, unsigned int, unsigned int, std::vector &, std::vector &, std::vector &)", + "custom_name": "globalCurveApproxStatic" + }, + { + "static": true, + "signature": "vpImagePoint computeCurvePoint(double, unsigned int, unsigned int, std::vector &, std::vector &, std::vector &)", + "custom_name": "computeCurvePointStatic" + }, + { + "static": true, + "signature": "void curveKnotIns(double, unsigned int, unsigned int, unsigned int, unsigned int, std::vector &, std::vector &, std::vector &)", + "custom_name": "curveKnotInsStatic" + } + ] + }, + "vpMeNurbs": { + "methods": [ + { + "static": true, + "signature": "void display(const vpImage&, vpNurbs&, const vpColor&, unsigned int)", + "custom_name": "displayMeNurbs" + }, + { + "static": true, + "signature": "void display(const vpImage&, vpNurbs&, const vpColor&, unsigned int)", + "custom_name": "displayMeNurbs" + } + ] + } + } +} \ No newline at end of file diff --git a/modules/python/config/robot.json b/modules/python/config/robot.json new file mode 100644 index 0000000000..f79adebd52 --- /dev/null +++ b/modules/python/config/robot.json @@ -0,0 +1,85 @@ +{ + "ignored_headers": ["vpWireFrameSimulatorTypes.h", "vpRobotException.h"], + + + "classes": { + "vpImageSimulator": { + + "methods": + [ + { + "static": true, + "signature": "void getImage(vpImage&, std::list&, const vpCameraParameters&)", + "custom_name": "getImageMultiplePlanes", + "use_default_param_policy": false, + "param_is_input": [ + true, + true, + true + ], + "param_is_output": [ + false, + false, + false + ] + }, + { + "static": true, + "signature": "void getImage(vpImage&, std::list&, const vpCameraParameters&)", + "custom_name": "getImageMultiplePlanes", + "use_default_param_policy": false, + "param_is_input": [ + true, + true, + true + ], + "param_is_output": [ + false, + false, + false + ] + }, + { + "static": false, + "signature": "void init(const vpImage&, vpColVector*)", + "ignore": true + }, + { + "static": false, + "signature": "void init(const vpImage&, vpColVector*)", + "ignore": true + } + ] + }, + "vpRobotSimulator": { + "is_virtual": true + }, + + "vpWireFrameSimulator" : { + "methods": [ + { + "static": false, + "signature": "void get_fMo_History(std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + }, + { + "static": false, + "signature": "void get_cMo_History(std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + } + ] + } + } +} \ No newline at end of file diff --git a/modules/python/config/sensor.json b/modules/python/config/sensor.json new file mode 100644 index 0000000000..adbcbb3663 --- /dev/null +++ b/modules/python/config/sensor.json @@ -0,0 +1,102 @@ +{ + "ignored_headers": [], + "classes": { + "vp1394TwoGrabber": { + "methods": [ + { + "static": false, + "signature": "void acquire(vpImage&, uint64_t&, uint32_t&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false, + false + ], + "param_is_output": [ + false, + true, + true + ] + }, + { + "static": false, + "signature": "void acquire(vpImage&, uint64_t&, uint32_t&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false, + false + ], + "param_is_output": [ + false, + true, + true + ] + }, + { + "static": false, + "signature": "void getNumCameras(unsigned int&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + }, + { + "static": false, + "signature": "void getWidth(unsigned int&)", + "ignore": true + }, + { + "static": false, + "signature": "void getHeight(unsigned int&)", + "ignore": true + }, + { + "static": false, + "signature": "void getCamera(uint64_t&)", + "ignore": true + }, + { + "static": false, + "signature": "uint32_t getVideoModeSupported(std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + false + ], + "param_is_output": [ + true + ] + }, + { + "static": false, + "signature": "uint32_t getFramerateSupported(vp1394TwoGrabber::vp1394TwoVideoModeType, std::list&)", + "use_default_param_policy": false, + "param_is_input": [ + true, + false + ], + "param_is_output": [ + false, + true + ] + }, + { + "static": false, + "signature": "void getAutoGain(unsigned int&, unsigned int&)", + "use_default_param_policy": false, + "param_is_input": [ + false, + false + ], + "param_is_output": [ + true, + true + ] + } + ] + } + } +} \ No newline at end of file diff --git a/modules/python/config/tt.json b/modules/python/config/tt.json new file mode 100644 index 0000000000..294b0d30ac --- /dev/null +++ b/modules/python/config/tt.json @@ -0,0 +1,7 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": {}, + "enums": {} +} diff --git a/modules/python/config/tt_mi.json b/modules/python/config/tt_mi.json new file mode 100644 index 0000000000..689f81f91f --- /dev/null +++ b/modules/python/config/tt_mi.json @@ -0,0 +1,11 @@ +{ + "ignored_headers": [], + "ignored_classes": [], + "user_defined_headers": [], + "classes": { + "vpTemplateTrackerMI": { + "is_virtual": true + } + }, + "enums": {} +} diff --git a/modules/python/config/vision.json b/modules/python/config/vision.json new file mode 100644 index 0000000000..80aaea98bc --- /dev/null +++ b/modules/python/config/vision.json @@ -0,0 +1,25 @@ +{ + "ignored_headers": ["vpPoseException.h", "vpCalibrationException.h", "vpPoseFeatures.h"], + "classes": { + "vpHomography" : { + "methods": + [ + { + "static": true, + "signature": "void computeDisplacement(const vpHomography &, const vpColVector &, vpRotationMatrix &, vpTranslationVector &, vpColVector &)", + "custom_name": "computeHomographyDisplacement" + }, + { + "static": true, + "signature": "void computeDisplacement(const vpHomography &, vpRotationMatrix &, vpTranslationVector &, vpColVector &)", + "custom_name": "computeHomographyDisplacement" + }, + { + "static": true, + "signature": "void computeDisplacement(const vpHomography &, double , double , std::list &, std::list &, std::list &)", + "custom_name": "computeHomographyDisplacement" + } + ] + } + } +} \ No newline at end of file diff --git a/modules/python/config/visual_features.json b/modules/python/config/visual_features.json new file mode 100644 index 0000000000..8e345d95d6 --- /dev/null +++ b/modules/python/config/visual_features.json @@ -0,0 +1,87 @@ +{ + "ignored_headers": [ + "vpFeatureException.h" + ], + "classes": { + "vpGenericFeature": { + "methods": [ + { + "static": false, + "signature": "void get_s(double&)", + "ignore": true + }, + { + "static": false, + "signature": "void get_s(double&, double&)", + "ignore": true + }, + { + "static": false, + "signature": "void get_s(double&, double&, double&)", + "ignore": true + } + ] + }, + "vpFeatureMomentDatabase": {}, + "vpFeatureMomentCommon": { + "methods": [ + { + "static": false, + "signature": "vpFeatureMomentAlpha& getFeatureAlpha()", + "return_policy": "reference", + "keep_alive": [1, 0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentAreaNormalized& getFeatureAn()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentBasic& getFeatureMomentBasic()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentCentered& getFeatureCentered()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentCInvariant& getFeatureCInvariant()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentGravityCenterNormalized& getFeatureGravityNormalized()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentArea& getFeatureArea()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + }, + { + "static": false, + "signature": "vpFeatureMomentGravityCenter& getFeatureGravityCenter()", + "return_policy": "reference", + "keep_alive": [1,0], + "returns_ref_ok": true + } + ] + } + } +} diff --git a/modules/python/config/vs.json b/modules/python/config/vs.json new file mode 100644 index 0000000000..b32bccd656 --- /dev/null +++ b/modules/python/config/vs.json @@ -0,0 +1,3 @@ +{ + "ignored_headers": ["vpServoException.h"] +} \ No newline at end of file diff --git a/modules/python/doc/CMakeLists.txt b/modules/python/doc/CMakeLists.txt new file mode 100644 index 0000000000..1d21183d24 --- /dev/null +++ b/modules/python/doc/CMakeLists.txt @@ -0,0 +1,131 @@ +############################################################################ +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings module +# +############################################################################# + +# configured documentation tools and intermediate build results +set(BINARY_BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}/_build") + +# Sphinx cache with pickled ReST documents +set(SPHINX_CACHE_DIR "${CMAKE_CURRENT_BINARY_DIR}/_doctrees") + +# HTML output directory +set(SPHINX_HTML_DIR "${VISP_DOC_DIR}/python") + +set(SPHINX_SOURCE_DIR "${${CMAKE_CURRENT_BINARY_DIR}/_src}") + +# Sphinx Template directory +set(SPHINX_TEMPLATE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/_templates") + + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/conf.py.in" + "${BINARY_BUILD_DIR}/conf.py" + @ONLY) + + +foreach(module ${python_bound_modules}) + # start string with 2 spaces since its included in autosummary + string(REPLACE "visp_" " visp." python_module_name ${module}) + string(APPEND VISP_PYTHON_MODULES_DOC_INCLUDE ${python_module_name} "\n") +endforeach() + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/api.rst.in" + "${BINARY_BUILD_DIR}/api.rst" + @ONLY +) + +set(SPHINX_INPUT_SOURCE_DIRS + "_static" + "rst" + "_templates" +) + +set(generated_deps "") + +# Copy all the source subdirectories: we're building in the cmake build folder, not in the cmake folder +foreach(source_dir ${SPHINX_INPUT_SOURCE_DIRS}) + set(output_dir "${BINARY_BUILD_DIR}/${source_dir}") + set(input_dir "${CMAKE_CURRENT_SOURCE_DIR}/${source_dir}") + file(GLOB_RECURSE files RELATIVE "${input_dir}" "${input_dir}/*") + foreach(f ${files}) + add_custom_command( + OUTPUT "${output_dir}/${f}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${input_dir}/${f}" "${output_dir}/${f}" + MAIN_DEPENDENCY "${input_dir}/${f}" + ) + list(APPEND generated_deps "${input_dir}/${f}" "${output_dir}/${f}") + endforeach() +endforeach() + +set(output_dir "${BINARY_BUILD_DIR}/examples") +set(input_dir "${CMAKE_CURRENT_SOURCE_DIR}/../examples") +file(GLOB_RECURSE files RELATIVE "${input_dir}" "${input_dir}/*") +foreach(f ${files}) + add_custom_command( + OUTPUT "${output_dir}/${f}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${input_dir}/${f}" "${output_dir}/${f}" + MAIN_DEPENDENCY "${input_dir}/${f}" + ) + list(APPEND generated_deps "${input_dir}/${f}" "${output_dir}/${f}") +endforeach() + + +set(SPHINX_INPUT_SOURCE_FILES + "index.rst" +) +foreach(source_file ${SPHINX_INPUT_SOURCE_FILES}) + set(output_file "${BINARY_BUILD_DIR}/${source_file}") + add_custom_command( + OUTPUT "${output_file}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${source_file}" "${BINARY_BUILD_DIR}" + ) + list(APPEND generated_deps "${output_file}") +endforeach() + +add_custom_target(visp_python_bindings_doc + COMMAND ${PYTHON3_EXECUTABLE} -m pip install -q -r "${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt" + COMMAND ${PYTHON3_EXECUTABLE} -m sphinx + -b html + -c "${BINARY_BUILD_DIR}" + -d "${SPHINX_CACHE_DIR}" + -j auto + -E + "${BINARY_BUILD_DIR}" + "${SPHINX_HTML_DIR}" + DEPENDS ${generated_deps} + COMMENT "Building Sphinx HTML documentation for ViSP's Python bindings" +) + +add_dependencies(visp_python_bindings_doc visp_python_bindings) diff --git a/modules/python/doc/_static/visp_icon.png b/modules/python/doc/_static/visp_icon.png new file mode 100644 index 0000000000..512ab9de57 Binary files /dev/null and b/modules/python/doc/_static/visp_icon.png differ diff --git a/modules/python/doc/_static/visp_icon_white.png b/modules/python/doc/_static/visp_icon_white.png new file mode 100644 index 0000000000..2611203027 Binary files /dev/null and b/modules/python/doc/_static/visp_icon_white.png differ diff --git a/modules/python/doc/_templates/custom-class-template.rst b/modules/python/doc/_templates/custom-class-template.rst new file mode 100644 index 0000000000..96f82162f4 --- /dev/null +++ b/modules/python/doc/_templates/custom-class-template.rst @@ -0,0 +1,63 @@ +{{ objname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + :member-order: groupwise + :inherited-members: pybind11_builtins.pybind11_object + :special-members: + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + :nosignatures: + {% for item in methods %} + {%- if not item.startswith('_') and item not in inherited_members or item.startswith('__init__') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block inheritedmethods %} + {% if inherited_members %} + .. rubric:: {{ _('Inherited Methods') }} + + .. autosummary:: + :nosignatures: + {% for item in inherited_members %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block operators %} + {% if members %} + .. rubric:: {{ _('Operators') }} + + .. autosummary:: + :nosignatures: + {% for item in members %} + {%- if item not in inherited_members and item.startswith('__') and item.endswith('__') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/modules/python/doc/_templates/custom-module-template.rst b/modules/python/doc/_templates/custom-module-template.rst new file mode 100644 index 0000000000..22bd4a50b6 --- /dev/null +++ b/modules/python/doc/_templates/custom-module-template.rst @@ -0,0 +1,65 @@ +{{ objname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/modules/python/doc/api.rst.in b/modules/python/doc/api.rst.in new file mode 100644 index 0000000000..004e63bc57 --- /dev/null +++ b/modules/python/doc/api.rst.in @@ -0,0 +1,18 @@ +.. _API reference: + +API reference +============== + +This API documentation is automatically generated by parsing the C++ documentation. + +.. warning:: + + Some documentation may be missing, and there may incorrect/missing links. If you are having issues, see the C++ documentation. + + +.. autosummary:: + :toctree: _autosummary + :recursive: + :template: custom-module-template.rst + +@VISP_PYTHON_MODULES_DOC_INCLUDE@ diff --git a/modules/python/doc/conf.py.in b/modules/python/doc/conf.py.in new file mode 100644 index 0000000000..cd46bf0496 --- /dev/null +++ b/modules/python/doc/conf.py.in @@ -0,0 +1,417 @@ +# +# python_example documentation build configuration file, created by +# sphinx-quickstart on Fri Feb 26 00:29:33 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +import sys +import os + + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx_immaterial", + "sphinx_design" +] + +# python_apigen_modules = { +# "visp.core": "generated/core.", +# "visp.vs": "generated/vs.", +# } +# python_apigen_default_groups = [ +# (r".*:visp.core.*", "Core Public-members"), +# (r"class:visp.core.*", "Core Classes"), +# (r".*:visp.vs.*", "VS Public-members"), +# (r"class:visp.vs.*", "VS Classes"), +# ] + +# python_apigen_default_order = [ +# (r".*:visp.core.*", -1), +# (r"class:visp.core.*", -2), +# (r".*:visp.vs.*", -1), +# (r"class:visp.vs.*", -2), +# ] +autosummary_generate = True + +autoclass_content = "both" # Add __init__ doc (ie. params) to class summaries +html_show_sourcelink = False # Remove 'view source code' from top of page (for html, not python) +autodoc_inherit_docstrings = True # If no docstring, inherit from base class +set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints +nbsphinx_allow_errors = True # Continue through Jupyter errors + +autodoc_typehints = "both" # Sphinx-native method. Not as good as sphinx_autodoc_typehints +add_module_names = False # Remove namespaces from class/method signatures + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "visp" +copyright = "2023, Inria" +author = "Samuel Felton" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = "@VISP_PYTHON_VERSION@" +# The full version, including alpha/beta/rc tags. +release = "@VISP_PYTHON_VERSION@" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build", "_templates"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'sphinx_immaterial' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} +html_theme_options = { + "toc_title_is_page_title": True, + "repo_url": "https://github.com/lagadic/visp", + "repo_name": "visp", + "features": [ + "toc.follow", + "toc.sticky", + "navigation.instant" + ], + "globaltoc_collapse": False, + "palette": [ + { + "media": "(prefers-color-scheme: light)", + "scheme": "default", + "toggle": { + "icon": "material/toggle-switch-off-outline", + "name": "Switch to dark mode", + }, + "primary": "red", + "accent": "indigo" + }, + { + "media": "(prefers-color-scheme: dark)", + "scheme": "slate", + "toggle": { + "icon": "material/toggle-switch", + "name": "Switch to light mode", + }, + "primary": "red", + "accent": "indigo" + }, + ] +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = 'ViSP documentation' + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "vispdoc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', +} +rst_prolog = """ +.. role:: python(code) + :language: python + :class: highlight +""" +html_logo = '_static/visp_icon_white.png' +html_favicon = '_static/visp_icon.png' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "visp.tex", + "visp Documentation", + "Samuel Felton", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, "visp", "ViSP Documentation", [author], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "visp", + "ViSP Documentation", + author, + "visp", + "Python binding for ViSP", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {"python": ("https://docs.python.org/", None)} + +from sphinx.util.logging import WarningLogRecordTranslator, WarningStreamHandler + +# Filter warning about Parameter names not matching function signature +# This is somethiing that is due to pybind overloads, so we cannot do anything about it +class FilterPybindArgWarnings(WarningLogRecordTranslator): + def filter(self, record): + if 'Parameter name' in record.msg and 'does not match any of the parameters' in record.msg: + return False + return super(FilterPybindArgWarnings, self).filter(record) + +# Filter warning about duplicate objects +class FilterNoIndexWarnings(WarningLogRecordTranslator): + def filter(self, record): + if 'use :no-index: for' in record.msg: + return False + return super(FilterNoIndexWarnings, self).filter(record) + + +object_description_options = [ + ("py:.*", dict(include_fields_in_toc=False)), +] + +python_type_aliases = { + "_visp.": "visp.", +} + + +autodoc_excludes = [ +'__weakref__', '__doc__', '__module__', '__dict__', +'__dir__', '__delattr__', '__format__', '__init_subclass__', '__new__', +'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', +'__sizeof__', '__str__', '__subclasshook__', '__getattribute__', '__entries', +] +def autodoc_skip_member(app, what, name, obj, skip, options): + # Ref: https://stackoverflow.com/a/21449475/ + + exclude = name in autodoc_excludes + # return True if (skip or exclude) else None # Can interfere with subsequent skip functions. + return True if exclude else skip + +def setup(app): + import logging + from sphinx.util.logging import NAMESPACE + logger = logging.getLogger(NAMESPACE) + for handler in logger.handlers: + if isinstance(handler, WarningStreamHandler): + handler.addFilter(FilterPybindArgWarnings(app)) + handler.addFilter(FilterNoIndexWarnings(app)) + + app.connect('autodoc-skip-member', autodoc_skip_member) diff --git a/modules/python/doc/index.rst b/modules/python/doc/index.rst new file mode 100644 index 0000000000..349222616f --- /dev/null +++ b/modules/python/doc/index.rst @@ -0,0 +1,81 @@ +ViSP Python Documentation +============================ + +.. currentmodule:: visp + +.. toctree:: + :maxdepth: 2 + :hidden: + + rst/coming_from_cpp.rst + rst/python_api/python_api.rst + rst/tutorials/tutorials.rst + rst/dev/dev.rst + api.rst + rst/known_issues.rst + + +Welcome to the ViSP Python binding documentation! + + ViSP is a modular C++ library that allows fast development of visual servoing applications. + ViSP is developed and maintained by the `Inria Rainbow (former Lagadic) team located `_ at Inria Rennes. + + + +Introduction +---------------------------- + +This documentation is specifically aimed at developers choosing to use ViSP in Python. + +Other, more general resources, are available: + +* If you are using C++, please see `the dedicated documentation `_ + +* The ViSP wiki can be found `here `_ + +* The ViSP source code available on `GitHub `_ + +* Results and demonstrations can be seen on the `ViSP YouTube channel `_ + + +.. note:: + + This documentation does not cover the full capabilities of ViSP. Please see the C++ documentation, which contains: + + * `Tutorials `_ on: + + * The core concepts: linear algebra, image processing, etc. + * Visual servoing with 3D, 2D or photometric features + * Object pose estimation and tracking + + * With the model-based tracker (MBT) :py:class:`visp.mbt.MbGenericTracker` + * With MegaPose, a deep learning approach to pose estimation :py:class:`visp.dnn_tracker.MegaPose` + + * `Examples `_ + + * Demonstrating basic feature usage + * Servoing on specific robotics platforms + * Tracking + + +.. warning:: + + There are still issues with these generated bindings: see :ref:`Known issues`. + + +Getting started +^^^^^^^^^^^^^^^^^^^^^^^ + +If you are transitioning from C++, please have a look at the :ref:`CPP guide` to understand the differences between the Python and C++ versions. + +For general ViSP + Python guidance, see the :ref:`Python API guide`. + +For tutorials and examples of specific features: see :ref:`Examples`. + +Finally, if you wish to browse the full ViSP class documentation, go to the :ref:`API reference`. + + +Customizing, extending and contributing to the bindings +-------------------------------------------------------- + +If you wish to contribute, extend or modify the bindings for your own needs, please read :ref:`Development guide` diff --git a/modules/python/doc/requirements.txt b/modules/python/doc/requirements.txt new file mode 100644 index 0000000000..fdb58ef4a2 --- /dev/null +++ b/modules/python/doc/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx-immaterial +sphinxcontrib-jsmath +sphinx-design diff --git a/modules/python/doc/rst/coming_from_cpp.rst b/modules/python/doc/rst/coming_from_cpp.rst new file mode 100644 index 0000000000..00f96b2db5 --- /dev/null +++ b/modules/python/doc/rst/coming_from_cpp.rst @@ -0,0 +1,139 @@ +.. _CPP guide: + +Differences with C++ ViSP +============================== + +In this section, we highlight the differences with writing ViSP code in C++. + +Module structure +----------------------------- + +The overall module structure remains the same. +What was, in C++, referred as :code:`visp3/core/*` can now be accessed as :python:`visp.core.*` in Python. +Note that before this works in Python, you need to :python:`import visp` + + +Naming convention +----------------------------- + +In C++, each class has the prefix `vp`. In Python, this prefix has been dropped as imports can be aliased and full names can be used. + +.. testcode:: + + import visp.core + from visp.core import Matrix as vpMatrix # if the name clashes with another lib + + m = vpMatrix() + vec = visp.core.ColVector(10) # Use the full name, explicit visp use + + + +Importing a class +---------------------------- + +The syntax to import a ViSP class into the current scope is different. + +In C++, including a header file pulls everything in it (functions and classes). + + +In the ViSP Python API, no distinction is made between the different headers: everything is at the same level in the package hierarchy. +In Python, you can import a single symbol. + +Thus, if a single header contains two symbols in ViSP, you will need to import both on the Python side. + +Below, the difference in syntax between C++ and Python on imports is illustrated: + +.. tab-set:: + + .. tab-item:: C++ + :sync: cpp + + .. code-block:: cpp + + #include + #include + #include + + + .. tab-item:: Python + :sync: python + + .. testcode:: + + from visp.core import ImageConvert + from visp.core import ColVector, Matrix # Grouping relevant imports + + +You can also import everything from a single submodule: + +.. tab-set:: + + .. tab-item:: C++ + :sync: cpp + + .. code-block:: cpp + + #include + + + .. tab-item:: Python + :sync: python + + .. testcode:: + + from visp.core import * + + +Changes in function parameters +-------------------------------------- + +For some functions, the Python API differs from the C++ one, mainly in the input arguments and return type. + +Due to python considering basic types as immutable, it is no longer possible to modify them passing their reference to a function call. + +Thus, we have made the choice to modify the functions such that these immutable types, if they are modified, are returned along with the original type. + +This encompasses other types, such as lists (std::vector), and dictionaries (maps) + + +Naively translating the use of :code:`convertPoint` from C++: + +.. testcode:: error_args + + from visp.core import PixelMeterConversion, CameraParameters + cam = CameraParameters(600, 600, 320, 240) + u, v = 240, 320 + x, y = 0, 0 + # WRONG: C++-like version, using references to modify x and y + PixelMeterConversion.convertPoint(cam, u, v, x, y) + +Would lead to an error such as: + +.. testoutput:: error_args + :options: -ELLIPSIS, +NORMALIZE_WHITESPACE, +IGNORE_EXCEPTION_DETAIL + + Traceback (most recent call last): + File "", line 1, in + TypeError: convertPoint(): incompatible function arguments. The following argument types are supported: + 1. (cam: _visp.core.CameraParameters, u: float, v: float) -> Tuple[float, float] + 2. (cam: _visp.core.CameraParameters, iP: _visp.core.ImagePoint) -> Tuple[float, float] + + Invoked with: Camera parameters for perspective projection without distortion: + px = 600 py = 600 + u0 = 320 v0 = 240 + , 240, 320, 0, 0 + +Because this function has been modified to return a tuple of :code:`Tuple[float, float]` (the x and y values). +The x and y arguments are no longer accepted, as they are output only. + +Thus, the correct function call is: + +.. testcode:: error_args + + from visp.core import PixelMeterConversion, CameraParameters + cam = CameraParameters(600, 600, 320, 240) + u, v = 240, 320 + x, y = PixelMeterConversion.convertPoint(cam, u, v) + + +If you have such errors, it is recommended that you look at the Python :ref:`API reference` for the function and look at its signature. diff --git a/modules/python/doc/rst/dev/config.rst b/modules/python/doc/rst/dev/config.rst new file mode 100644 index 0000000000..50f9f58736 --- /dev/null +++ b/modules/python/doc/rst/dev/config.rst @@ -0,0 +1,294 @@ +Modifying the bindings through JSON configuration files +======================================================== + +The bindings and the generated code can be customized through JSON configuration files. +These files will be read and interpreted by the generator code. + +They are located in the :code:`modules/python/config` folder of the ViSP source code. +After modifying them, you should retrigger the build of the python bindings. + +When something cannot be resolved through configuration files, you should revert to using a :ref:`Custom binding`. + + +Root configuration file +--------------------------------------- + +The general configuration file is generated by the build system (CMake), it contains the arguments to the generator script. + +It contains: + +* The list of include directories used when compiling ViSP C++. They are used to resolve include of 3rd parties and find the **vpConfig.h** file + +* A set of preprocessor macros that will be used when parsing C++ files with the generator. These are system and compiler dependent. + +* For each module (core, imgproc, etc.), the list of header files that should be parsed. + + +A module can be ignored through the **CMakeLists.txt** configuration of the python module. + +Each module has a dedicated JSON configuration file, located in the :code:`modules/python/config` folder. + + +Module configuration +-------------------------------------------- + +Configuration files have a top-down structure: First come the general module options, +then class/enum options, and for each of those, method/value configurations. + +Module-level options +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: json + + { + "required_headers": ["visp3/core/vpPoint.h"], + "ignored_headers": ["vpGEMM.h", "vpDebug.h"], + "ignored_classes": ["vpException", "vpImageException"], + "user_defined_headers": ["core.hpp"], + "enums": {}, + "classes": {}, + "functions": {} + } + + +.. list-table:: Parameters + :header-rows: 1 + + * - Name + - Type + - Explanation + * - :code:`required_headers` + - List of strings + - List of full header paths. These headers are forcefully included. Should be used if for some reason, the already included ViSP headers are not sufficient. + By default, each parsed header is added to the includes of this module's binding source (.cpp file compiled with PyBind). + * - :code:`ignored_headers` + - List of strings + - List of header file names, not including the module path. Each of these headers will be **completely** skipped. + No preprocessing, no parsing, no binding generation. Useful if a header generates errors on parsing. + * - :code:`ignored_classes` + - List of strings + - List of C++ class names (without any template argument). + These classes will be ignored (but still parsed, unlike with :code:`ignored_headers`) and no binding code generated. + * - :code:`user_defined_headers` + - List of strings + - Paths to user-defined bindings that will be called when generating the overall bindings + (see class options and :ref:`Custom binding`). These paths are relative to the **modules/python/bindings/include** folder. + If a file does not exist, an error will be raised at compile time. + * - :code:`enums` + - Dictionary + - Mapping from C++ enum name to an :ref:`enum configuration `. + * - :code:`classes` + - Dictionary + - Mapping from C++ class name (untemplated) to a :ref:`class configuration `. + * - :code:`functions` + - List of dictionaries + - List of :ref:`function configuration `. These are for free functions, not class methods. + + +.. warning:: + + Exceptions are not handled: they should always be placed in :code:`ignored_classes`. + + When a ViSP exception is thrown to the Python interpreter, it is converted to a RuntimeError + + +.. _Enum options: + +Enum-level options +^^^^^^^^^^^^^^^^^^^ + +If an enum does not appear in the configuration dictionary, it takes on the default values of each option. + +For enums there is only a single option: :code:`"ignore"`, which is a boolean. +If this flag is true, no binding is generated for this enum. The default value is **false**. + + +.. note:: + + By design, all exported ViSP enumerations are of the arithmetic kind. + It is thus possible to do :python:`Enum.value1 | Enum.value2`. + Not all enumerations should actually behave like this, + but it is not trivial to automatically determine which require arithmetic capabalities. + + A possible improvement would be to add an :code:`arithmetic` flag to the configuration options to handle this. + +.. _Class options: + +Class-level options +^^^^^^^^^^^^^^^^^^^ + +If a class does not appear in the configuration dictionary, it takes on the default value of each option. + + +.. code-block:: json + + "ignored_attributes": ["myAttribute"] + "additional_bindings": "bindings_vpArray2D", + "use_buffer_protocol": true, + "specializations": [ + { + "python_name": "ArrayDouble2D", + "arguments": ["double"] + } + ] + "ignore_repr": true, + "is_virtual": true, + "methods": {} + + +.. list-table:: Parameters + :header-rows: 1 + + * - Name + - Type + - Explanation + * - :code:`ignored_attributes` + - List of strings + - List of attribute names. Each of the corresponding attributes will be ignored when generating binding code. + By default, binding code is generated only for public fields that are not pointers or other hard to translate types. + * - :code:`additional_bindings` + - String + - Name of a C++ function, defined in **User-defined binding code**. + Should be visible from the module's .cpp file and have to correct signature. + This means that the header file in which it is defined should be included in :code:`user_defined_headers`. + See :ref:`Custom binding` for more info. + * - :code:`use_buffer_protocol` + - Boolean + - Whether to add the buffer protocol to this object. This is a PyBind specific thing, + and is helpful to automatically interpret an object of this class as an iterable/array (e.g., list) on the python side. + This should be defined by hand in user-defined bindings. See the + `Pybind documentation `_ + for more info. + + * - :code:`specializations` + - List of dictionaries + - Only required for templated classes. Templating does not exist in Python, and Pybind can only generate bindings for + classes that are fully specialized. Thus, it is required to declare the specializations. + A specialization contains: the Python name of the class as well as the C++ types that will replace the generic template typenames. + The C++ types should be in the same order as the template parameters. + * - :code:`ignore_repr` + - Boolean + - In python the :python:`__repr__` method is equivalent to the :code:`operator<<(std::ostream&, Cls& self)` function + allowing to print an object in the terminal. By default, the generator tries to find the C++ defined operator to generate a Python representation. + If this is not desired, set this flag to true. You can define a custom representation through custom bindings. + + .. warning:: + Long to string representations (matrices, images) can flood the terminal. + This is problematic if this happens when Pybind throws an error for an incorrect method call + * - :code:`is_virtual` + - Boolean + - Whether to force this class to be considered as purely virtual (cannot be instanciated in Python) + + .. note:: + While most purely virtual classes are correctly detected, classes that inherit from an abstract one + but do not implement its methods are not correctly detected, which will raise an error at compile time. + It is for these cases that this flag is required. + * - :code:`methods` + - List of dictionaries + - List of :ref:`function configuration `. + + +.. _Function options: + +Function-level options +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: json + + { + "signature": "vpImage& fn(vpImage&, Type, double&)", + "static": false, + "ignore": false, + "use_default_param_policy": false, + "param_is_input": [true, true, false], + "param_is_output": [false, true, true], + "return_policy": "reference", + "keep_alive": [1, 0], + "returns_ref_ok": true, + "specializations": + [ + ["unsigned char"], + ["vpRGBa"] + ], + "custom_name": "function_name" + } + +.. list-table:: Parameters + :header-rows: 1 + + * - Name + - Type + - Explanation + * - :code:`signature` + - String + - Signature of the function for which the functions apply. + + * Signature does not include the name of the parameters + * The templated types should not be replaced with specializations. + * Spaces are stripped when matching with parsed signatures + * Signature does not include the *;* + + * - :code:`static` + - Boolean + - Whether this function is static. In the case of free functions (not related to a class), it should be false. + * - :code:`ignore` + - Boolean + - Whether the binding for this method should be skipped. Defaults to false. + + .. note:: + + If you provide an alternative to this function through custom bindings, + you should set this to true so that the default is ignored or no warning is emitted + + * - :code:`use_default_param_policy` + - Boolean + - Whether to use the default parameter policy. With this policy, + non-const references (**&**) to types that are immutable in Python (including STL containers) + are considered as both inputs and outputs. Defaults to false. + If true, no warning is emitted in the logs about parameter policy + + .. note:: + + This is required since we do not know whether + the references are used as inputs or outputs (or both) of the function. + + When a parameter is an output, it is either returned (it is the only output) or it is aggregated to a result tuple. + + + * - :code:`param_is_input` + - List of booleans + - For a function with n arguments, a list of n booleans. at index i, describes whether the i-eth parameter is an input. + If false, a default value is created. + Requires that the type is default constructible. + + .. warning:: + + The basic types (int, double, etc.) are left uninitialized. + + * - :code:`param_is_output` + - List of booleans + - For a function with n arguments, a list of n booleans. at index i, describes whether the i-eth parameter is an output. + if true it is added to the return tuple. + * - :code:`return_policy` + - String + - How C++ returns the type to Python. If there are issues about unwanted copies or memory freeing, configure this. + See `The Pybind documentation `_ + * - :code:`keep_alive` + - 2-tuple of ints or List of 2-tuples + - Dictates the lifetime of arguments and return types. + Each tuple indicates that the second argument should be kept alive until the first argument is deleted. + 0 indicates the return value, 1 indicates self. + See `The pybind documentation `_ + * - :code:`returns_ref_ok` + - Boolean + - If this function returns a ref, mark it as ok or not. Returning a ref may lead to double frees or copy depending on return policy. + Make sure that :code:`keep_alive` and :code:`return_policy` are correctly set if you get a warning in the log, then set this to true to ignore the warning. + * - :code:`specializations` + - List of list of strings + - Each list of string denotes a specialization, for a templated function. For each specialization, + each string is a typename that is used to instanciate the template. + The typenames should be in the same order ar the template specification of the function. + If there are multiple specializations, the function will be overloaded. + * - :code:`custom_name` + - String + - Rename this function to another name. Especially useful in the case of both static and member functions having the same name, which is forbidden by Pybind11. diff --git a/modules/python/doc/rst/dev/custom_bindings.rst b/modules/python/doc/rst/dev/custom_bindings.rst new file mode 100644 index 0000000000..583404dc52 --- /dev/null +++ b/modules/python/doc/rst/dev/custom_bindings.rst @@ -0,0 +1,4 @@ +.. _Custom binding: + +Adding a custom function binding +================================= diff --git a/modules/python/doc/rst/dev/dev.rst b/modules/python/doc/rst/dev/dev.rst new file mode 100644 index 0000000000..a3478c4bb0 --- /dev/null +++ b/modules/python/doc/rst/dev/dev.rst @@ -0,0 +1,98 @@ +.. _Development guide: + +Modifying and contributing to the bindings +============================================ + +.. toctree:: + :glob: + :maxdepth: 2 + + how.rst + config.rst + custom_bindings.rst + python_side.rst + + + +Remaining work +----------------------- + +In this section, we list some remaining issues or work to be done. + + +Changes to ViSP C++ API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Write initTracking for vpKltOpencv taking a vpImage as input. Ignore setInitialGuess. + +Code generation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* n-ary operators are not generated +* Matrix multiplication should be done through the @ operator (__matmul\__) +* Get operators for vpArray2D and the subclasses should be ignored, as they are reimplemented through custom bindings +* Classes that are not in the top level namespace are ignored. +* Inner classes are also ignored +* The default return policy for references is to copy, which is probably not the expected usage. ViSP sometimes returns references to STL containers, which have to be copied to Python +* Add parameters in config for: + + * GIL scope + +* Add callback for before_module and after_module so that we can define additional bindings by hand in the module. This is already done per class + +Documentation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Generate documentation for: + + * Functions in namespaces etc. + +* Reference python types in Documentation +* Prefer Python examples instead of C++ ones ? + +To be written: +* Documentation for the overall workflow of the bindings generation + + +Python side +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* UI + +* Add python sources to visp package + + * Matplotlib based plotter + + + +Errors when generating bindings +------------------------------------- + +When modifying the bindings, you may encounter errors. + +Here is a very non-exhaustive list of errors. + +If you encounter a compilation error, make sure to first try rebuilding after cleaning the CMake cache +Pybind did generate problems (an error at the pybind include line) that were solved like this. + +Static and member methods have the same name +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If, when importing visp in python, you encounter this message: + + ImportError: overloading a method with both static and instance methods is not supported; error while attempting to bind instance method visp.xxx() -> None + +Then it means that a class has both a static method and a member method with the same name. You should :ref:`rename either one through the config files `. + +Abstract class not detected +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have this error: + + error: invalid new-expression of abstract class type ‘vpTemplateTrackerMI’ + return new Class{std::forward(args)...}; + In file included from /home/visp_ws/visp_build/modules/python/bindings/src/tt_mi.cpp:13:0: + /home/visp_ws/visp/modules/tracker/tt_mi/include/visp3/tt_mi/vpTemplateTrackerMI.h:46:19: note: because the following virtual functions are pure within ‘vpTemplateTrackerMI’: + class VISP_EXPORT vpTemplateTrackerMI : public vpTemplateTracker + +You should define the class (here vpTemplaterMI) as pure virtual in the config file (via the flag is_virtual). +This error occurs because some methods are defined as pure virtual in a parent class and are not defined in the class this class: Pure virtual class detection does not look in the class hierarchy but only at the present class. diff --git a/modules/python/doc/rst/dev/how.rst b/modules/python/doc/rst/dev/how.rst new file mode 100644 index 0000000000..ebc95a77d0 --- /dev/null +++ b/modules/python/doc/rst/dev/how.rst @@ -0,0 +1,2 @@ +How bindings are generated +===================================== diff --git a/modules/python/doc/rst/dev/python_side.rst b/modules/python/doc/rst/dev/python_side.rst new file mode 100644 index 0000000000..0122871cf6 --- /dev/null +++ b/modules/python/doc/rst/dev/python_side.rst @@ -0,0 +1,2 @@ +Adding a Python side improvement +================================= diff --git a/modules/python/doc/rst/known_issues.rst b/modules/python/doc/rst/known_issues.rst new file mode 100644 index 0000000000..a469289ba1 --- /dev/null +++ b/modules/python/doc/rst/known_issues.rst @@ -0,0 +1,30 @@ +.. _Known issues: + +Known issues +====================== + +We are aware of some remaining issues. +If you encounter another problem, please file an issue on Github. + + +Usability +-------------------- + +No implicit conversion from ViSP types to Numpy +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Numpy array cannot be implicitely converted to a ViSP representation when calling a ViSP function. + + +ViSP 3rd party types (such as cv::Mat) cannot be used from Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We do not interface with other bindings (as it is not trivial and may require specific Pybind ABI), and we do not wrap third party types. +Thus, alternatives must be provided by hand into the ViSP API (or wrapped through custom bindings) so that the functionalities can be used from Python + +Cannot inherit from a ViSP class in Python +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Right now, it is not possible to inherit from a ViSP class with a Python class. Virtual methods cannot be overriden. +To remedy this, trampoline classes should be implemented into the generator, either fully automated (but that is a lot of complexity) +or by providing the trampoline by hand and adding a way to reference the trampoline class in the configuration file. diff --git a/modules/python/doc/rst/python_api/conversions.rst b/modules/python/doc/rst/python_api/conversions.rst new file mode 100644 index 0000000000..51747f4925 --- /dev/null +++ b/modules/python/doc/rst/python_api/conversions.rst @@ -0,0 +1,370 @@ +Using ViSP with other libraries +=============================================== + +ViSP provides multiple types to manipulate mathematical objects, such as: + +* Vectors + + * :py:class:`visp.core.ColVector` + * :py:class:`visp.core.RowVector` + * :py:class:`visp.core.ThetaUVector` + +* Matrices + + * :py:class:`visp.core.Matrix` + * :py:class:`visp.core.RotationMatrix` + * :py:class:`visp.core.HomogeneousMatrix` + + +While these representations should allow you to work with all the ViSP functions, +they are foreign to all the other Python libraries. + +For most scientific computing libraries, the standard data representation is based on `NumPy `_. +Since most libraries will accept and manipulate these arrays, ViSP provides conversion functions. + + +NumPy ↔ ViSP +----------------------------------------- + + + +Mapping between NumPy arrays and ViSP types +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here, we give the list of all supported NumPy-convertible types + +.. list-table:: Core math types + :header-rows: 1 + + * - Python type + - NumPy Shape and dtype + * - :py:class:`visp.core.Matrix` + - (N, M), np.float64 + * - :py:class:`visp.core.ColVector` + - (M,), np.float64 + * - :py:class:`visp.core.RowVector` + - (N,), np.float64 + * - :py:class:`visp.core.RotationMatrix` + - (3, 3), np.float64 + * - :py:class:`visp.core.HomogeneousMatrix` + - (4, 4), np.float64 + * - :py:class:`visp.core.ThetaUVector` + - (3,), np.float64 + +.. list-table:: Core image types + :header-rows: 1 + + * - C++ type + - Python type + - NumPy Shape and dtype + * - :code:`vpImage` + - :py:class:`visp.core.ImageGray` + - (H, W), np.uint8 + * - :code:`vpImage` + - :py:class:`visp.core.ImageUInt16` + - (H, W), np.uint16 + * - :code:`vpImage` + - :py:class:`visp.core.ImageRGBa` + - (H, W, 4), np.uint8 + * - :code:`vpImage` + - :py:class:`visp.core.ImageRGBf` + - (H, W, 3), np.float32 + + + +Acquiring a view of a ViSP object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is possible to view a ViSP object as a NumPy array. When using a view, changes made to one representation is reflected in the other. + +See `the NumPy documentation `_ for more information. + +To reinterpret a supported ViSP object as a Numpy array, use either: + +.. testcode:: + + from visp.core import ColVector + import numpy as np + + list_representation = [i for i in range(3)] + vec = ColVector(list_representation) # Initialize a 3 vector from a list + np_vec = vec.numpy() # A 1D numpy array of size 3 + + print(np.all(np_vec == list_representation)) + + + vec *= 2.0 + print(np.all(np_vec == list_representation)) + +.. testoutput:: + + True + False + + +or + +.. testcode:: + + from visp.core import ColVector + import numpy as np + + list_representation = [i for i in range(3)] + vec = ColVector(list_representation) # Initialize a 3 vector from a list + np_vec = np.array(vec, copy=False) # A 1D numpy array of size 3 + + print(np.all(np_vec == list_representation)) + # Modifying the ViSP vector modifies the NumPy view + vec *= 2.0 + print(np.all(np_vec == list_representation)) + + # Modifying the NumPy array modifies the ViSP object + np_vec[:2] = 0.0 + print(vec[0] == 0.0 and vec[1] == 0.0) + +.. testoutput:: + + True + False + True + + +Note that with these methods, some ViSP objects cannot be modified. +That is the case for :py:class:`visp.core.HomogeneousMatrix` and :py:class:`visp.core.RotationMatrix`, where an undesired modification +may lead to an invalid representation (Such as a rotation matrix not conserving its properties) + +Thus, this code will not work: + +.. testcode:: + + from visp.core import RotationMatrix, HomogeneousMatrix + import numpy as np + + R = RotationMatrix() + R.numpy()[0, 1] = 1.0 + + T = HomogeneousMatrix() + T.numpy()[0, 1] = 1.0 + +.. testoutput:: + :options: +IGNORE_EXCEPTION_DETAIL + + Traceback (most recent call last): + File "", line 1, in + ValueError: assignment destination is read-only + Traceback (most recent call last): + File "", line 1, in + ValueError: assignment destination is read-only + + +Copying to a NumPy array +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To obtain a copy of the ViSP representation you can simply use: + +.. testcode:: + + from visp.core import ColVector + import numpy as np + + vec = ColVector(3, 0) + np_vec = vec.numpy().copy() # or np.array(vec, copy=True) + np_vec[0] = 1 + + print(np_vec[0] == vec[0]) + +.. testoutput:: + + False + + +Keep in mind that it may be preferable to use a copy of the data, especially if you are using both numpy and ViSP representations for different tasks at the same time + +For instance, the following code will lead to an undesired behaviour: + +.. testcode:: + + from visp.core import ColVector + import numpy as np + + def compute_velocity(velocity: ColVector) -> None: + # Dummy function to illustrate in place ops + velocity *= 2.0 # This code modifies the content of velocity + + velocity = ColVector(6, 1.0) + iteration = 0 + # Store the velocities in a list + log_data = [] + + # Servoing loop + while iteration < 10: + compute_velocity(velocity) + log_data.append(velocity.numpy()) + iteration += 1 + + # Do some logging... + print(log_data[0]) + print(log_data[-1]) + +.. testoutput:: + + [1024. 1024. 1024. 1024. 1024. 1024.] + [1024. 1024. 1024. 1024. 1024. 1024.] + + +Although we're multiplying the velocity by 2 at each iteration, +we can see that we have the same values for the first and last iterations. + + +.. warning:: + + In essence, this is because while we store 10 different NumPy arrays, they all share the same underlying storage. + This storage is, at each iteration, modified by the :python:`compute_velocity` function. + +.. note:: + + To remedy this, you can either: + + * Make a copy of the NumPy array at every iteration before storing it in the list :python:`log_data.append(velocity.numpy().copy())` + * Change the :python:`compute_velocity` to return a new :py:class:`visp.core.ColVector` + + +Building a ViSP object from a NumPy array +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In the above section, we have shown how to convert a ViSP representation to a NumPy array. + +To perform the inverse operation, a custom constructor is defined for each class that allows the Numpy → ViSP conversion. + +This constructor performs a **copy** of the NumPy data into the newly created ViSP object. + +For instance, to build a new matrix + +.. testcode:: + + from visp.core import Matrix + import numpy as np + + random_mat = np.random.rand(5, 10) # 10 x 10 random matrix + + mat = Matrix(random_mat) + print(mat.getRows(), mat.getCols()) + print(np.all(random_mat == mat)) + + # We built a matrix by copying the numpy array: modifying one does not impact the other + random_mat[:, 0] = 0 + print(np.all(random_mat == mat)) + +.. testoutput:: + + 5 10 + True + False + + +.. warning:: + + A way to build a ViSP object as a view of a NumPy array is still lacking + +Numpy-like indexing of ViSP arrays +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +ViSP data types now support numpy-like indexing, and methods like slicing and iterating on values. + +To read values, rows and columns of a Matrix, you can use: + +.. testcode:: + + from visp.core import Matrix + + m = Matrix(2, 3, 1.0) + print(m[0, 0]) + print(m[0]) # First row + print(m[:, 0]) # First column + + +.. testoutput:: + + 1.0 + [1. 1. 1.] + [1. 1.] + + +Using RealSense cameras with ViSP +--------------------------------------- + +In the C++ version of ViSP, a class is provided to work with Intel cameras such as the D435. +This class, that acts as a thin wrapper around the Realsense libraries, cannot be used in Python. + +Instead we recommend to use the Python wrapper provided by Intel, :code:`pyrealsense2`. + +You can install it with: + + python -m pip install pyrealsense2 + +This library allows us to acquire frames from the camera. +It is possible to convert them to a numpy representation, +and they can thus be used with ViSP. + +The code below demonstrates how to use the Realsense package with ViSP: + +.. code-block:: python + + import pyrealsense2 as rs + import numpy as np + from visp.core import CameraParameters + from visp.core import ImageRGBa, ImageUInt16, ImageGray + from visp.core import ImageConvert, Display + from visp.gui import DisplayX + + def cam_from_rs_profile(profile) -> CameraParameters: + '''Get camera intrinsics from the realsense framework''' + # Downcast to video_stream_profile and fetch intrinsics + intr = profile.as_video_stream_profile().get_intrinsics() + return CameraParameters(intr.fx, intr.fy, intr.ppx, intr.ppy) + + if __name__ == '__main__': + + # Initialize realsense2 + pipe = rs.pipeline() + config = rs.config() + fps = 60 + h, w = 480, 640 + config.enable_stream(rs.stream.depth, w, h, rs.format.z16, fps) + config.enable_stream(rs.stream.color, w, h, rs.format.rgba8, fps) + + cfg = pipe.start(config) + + I_gray = ImageGray(h, w) + display_gray = DisplayX() + display_gray.init(I_gray, 0, 0, 'Color') + I_depth_hist = ImageGray(h, w) + display_depth = DisplayX() + display_depth.init(I_depth_hist, 640, 0, 'Color') + + + # Retrieve intrinsics + cam_color = cam_from_rs_profile(cfg.get_stream(rs.stream.color)) + cam_depth = cam_from_rs_profile(cfg.get_stream(rs.stream.depth)) + + point_cloud_computer = rs.pointcloud() + while True: + frames = pipe.wait_for_frames() + color_frame = frames.get_color_frame() + depth_frame = frames.get_depth_frame() + # NumPy Representations of realsense frames + I_color_np = np.asanyarray(color_frame.as_frame().get_data()) + I_depth_np = np.asanyarray(depth_frame.as_frame().get_data()) + # ViSP representations + I_color = ImageRGBa(I_color_np) # This works because format is rs.format.rgba8, otherwise concat or conversion needed + I_depth = ImageUInt16(I_depth_np) + # Transform depth frame as point cloud and view it as an N x 3 numpy array + point_cloud = np.asanyarray(point_cloud_computer.calculate(depth_frame).get_vertices()).view((np.float32, 3)) + + ImageConvert.convert(I_color, I_gray) + ImageConvert.createDepthHistogram(I_depth, I_depth_hist) + + Display.display(I_gray) + Display.display(I_depth_hist) + Display.flush(I_gray) + Display.flush(I_depth_hist) diff --git a/modules/python/doc/rst/python_api/python_api.rst b/modules/python/doc/rst/python_api/python_api.rst new file mode 100644 index 0000000000..d24e43f007 --- /dev/null +++ b/modules/python/doc/rst/python_api/python_api.rst @@ -0,0 +1,11 @@ +.. _Python API guide: + +Python specific features and help +================================== + +.. toctree:: + :glob: + :maxdepth: 2 + + conversions.rst + python_specific_fns.rst diff --git a/modules/python/doc/rst/python_api/python_specific_fns.rst b/modules/python/doc/rst/python_api/python_specific_fns.rst new file mode 100644 index 0000000000..9ddba3a843 --- /dev/null +++ b/modules/python/doc/rst/python_api/python_specific_fns.rst @@ -0,0 +1,20 @@ +Python specific functions +============================== + +To either make code more pythonic or help improve performance, some functions and helpers have been defined. + +To add other custom functionalities :ref:`Custom binding`. + + +Core module +---------------------- + +* :py:class:`~visp.core.PixelMeterConversion` and :py:class:`~visp.core.MeterPixelConversion` both +have a vectorised implementation of :code:`convertPoint`, called :code:`convertPoints`, accepting NumPy arrays + + +MBT module +----------------------- + +* :py:class:`~visp.mbt.MbGenericTracker` as a reworked version of :py:meth:`visp.mbt.MbGenericTracker.track`, taking as inputs +maps of color images and of numpy representations (of shape H x W x 3) of the point clouds. diff --git a/modules/python/doc/rst/tutorials/tracking/realsense-mbt.rst b/modules/python/doc/rst/tutorials/tracking/realsense-mbt.rst new file mode 100644 index 0000000000..29a05095ef --- /dev/null +++ b/modules/python/doc/rst/tutorials/tracking/realsense-mbt.rst @@ -0,0 +1,11 @@ +Tracking an object with the model-based tracker and a Realsense camera +======================================================================= + +This example shows how to track a cube object with the model-based tracker, with a realsense camera. + +This tutorial requires the data from the corresponding `C++ tutorial `_. + + + +.. literalinclude:: /examples/realsense-mbt.py + :language: python diff --git a/modules/python/doc/rst/tutorials/tracking/synthetic-mbt.rst b/modules/python/doc/rst/tutorials/tracking/synthetic-mbt.rst new file mode 100644 index 0000000000..eb156f38c9 --- /dev/null +++ b/modules/python/doc/rst/tutorials/tracking/synthetic-mbt.rst @@ -0,0 +1,12 @@ +Tracking an object in a synthetic sequence with the model-based tracker +======================================================================= + +This example shows how to track a box object with the model-based tracker. + +The data is synthetic and acquired with Blender. + +It is based on the `Blender simulation tutorial `_ + + +.. literalinclude:: /examples/synthetic-data-mbt.py + :language: python diff --git a/modules/python/doc/rst/tutorials/tutorials.rst b/modules/python/doc/rst/tutorials/tutorials.rst new file mode 100644 index 0000000000..b166c9d656 --- /dev/null +++ b/modules/python/doc/rst/tutorials/tutorials.rst @@ -0,0 +1,25 @@ +.. _Examples: + +Examples +==================== + + +Visual servoing +----------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + vs/* + + + +Tracking +----------------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + tracking/* diff --git a/modules/python/doc/rst/tutorials/vs/ibvs.rst b/modules/python/doc/rst/tutorials/vs/ibvs.rst new file mode 100644 index 0000000000..07e573259d --- /dev/null +++ b/modules/python/doc/rst/tutorials/vs/ibvs.rst @@ -0,0 +1,5 @@ +Simulated Image-Based visual servoing +======================================= + +.. literalinclude:: /examples/ibvs-four-points.py + :language: python diff --git a/modules/python/doc/rst/tutorials/vs/pbvs.rst b/modules/python/doc/rst/tutorials/vs/pbvs.rst new file mode 100644 index 0000000000..eab6fae1f4 --- /dev/null +++ b/modules/python/doc/rst/tutorials/vs/pbvs.rst @@ -0,0 +1,5 @@ +Simulated Pose-Based visual servoing +====================================== + +.. literalinclude:: /examples/pbvs-four-points.py + :language: python diff --git a/modules/python/examples/ibvs-four-points.py b/modules/python/examples/ibvs-four-points.py new file mode 100644 index 0000000000..aa73828db7 --- /dev/null +++ b/modules/python/examples/ibvs-four-points.py @@ -0,0 +1,310 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python example to simulate an image-based visual servo on four 2D points +# +############################################################################# + +import numpy as np +import argparse +import sys + +# For plots +import matplotlib.pyplot as plt +import os + +# Use latex in plot legends and labels +plt.rc('text', usetex=True) +plt.rc('text.latex', preamble=r'\usepackage{amsmath}') + +# ViSp Python bindings +from visp.core import ExponentialMap +from visp.core import HomogeneousMatrix +from visp.core import Math +from visp.core import Point +from visp.core import RotationMatrix +from visp.core import ThetaUVector +from visp.core import TranslationVector + +from visp.visual_features import FeatureBuilder +from visp.visual_features import FeaturePoint + +from visp.vs import Servo + +class PlotIbvs: + def __init__(self, e, norm_e, v, x, xd, c_T_w, plot_log_scale): + self.vector_e = e + self.vector_ne = norm_e + self.vector_v = v + self.vector_x = x + self.vector_xd = xd + self.vector_w_t_c = c_T_w.inverse().getTranslationVector() + self.plot_log_scale = plot_log_scale + + def stack(self, e, norm_e, v, x, xd, c_T_w): + self.vector_e = np.vstack((self.vector_e, e)) + self.vector_ne = np.vstack((self.vector_ne, norm_e)) + self.vector_v = np.vstack((self.vector_v, v)) + self.vector_x = np.vstack((self.vector_x, x)) + self.vector_w_t_c = np.vstack((self.vector_w_t_c, c_T_w.inverse().getTranslationVector())) + + def display(self, fig_filename): + plt.figure(figsize=(10,10)) + + plot_e = plt.subplot(2, 2, 1) + plot_v = plt.subplot(2, 2, 2) + plot_ne = plt.subplot(2, 2, 3) + plot_x = plt.subplot(2, 2, 4) + + plot_e.set_title('error') + plot_v.set_title('camera velocity') + plot_x.set_title('point trajectory in the image plane') + + if self.plot_log_scale: + plot_ne.set_title('log(norm error)') + plot_ne.plot(np.log(self.vector_ne)) + else: + plot_ne.set_title('norm error') + plot_ne.plot(self.vector_ne) + + plot_ne.grid(True) + plot_e.grid(True) + plot_v.grid(True) + plot_x.grid(True) + + plot_e.plot(self.vector_e) + plot_e.legend(['$x_1$','$y_1$','$x_2$','$y_2$','$x_3$','$y_3$','$x_4$','$y_4$',]) + + for i in range(self.vector_v.shape[1]): # Should be 6 + plot_v.plot(self.vector_v[:,i]) + plot_v.legend(['$v_x$','$v_y$','$v_z$','$\omega_x$','$\omega_y$','$\omega_z$']) + + for i in range(self.vector_x.shape[1]): + pts = np.asarray([[p.get_x(), p.get_y()] for p in self.vector_x[:, i]]) + plot_x.plot(pts[:, 0], pts[:, 1]) + plot_x.legend(['$x_1$','$x_2$','$x_3$','$x_4$']) + + for i in range(self.vector_x.shape[1] ): + plot_x.plot(self.vector_xd[i].get_x(), self.vector_xd[i].get_y(),'o') + + # Create output folder it it doesn't exist + output_folder = os.path.dirname(fig_filename) + if not os.path.exists(output_folder): + os.makedirs(output_folder) + print("Create output folder: ", output_folder) + + print(f"Figure is saved in {fig_filename}") + plt.savefig(fig_filename) + + # Plot 3D camera trajectory + plot_traj = plt.figure().add_subplot(projection='3d') + plot_traj.scatter(self.vector_w_t_c[0][0], self.vector_w_t_c[0][1], self.vector_w_t_c[0][2], marker='x', c='r', label='Initial position') + # Hack to ensure that the scale is at minimum between -0.5 and 0.5 along X and Y axis + min_s = np.min(self.vector_w_t_c, axis=0) + max_s = np.max(self.vector_w_t_c, axis=0) + for i in range(len(min_s)): + if (max_s[i] - min_s[i]) < 1.: + max_s[i] += 0.5 + min_s[i] -= 0.5 + plot_traj.axis(xmin=min_s[0], xmax=max_s[0]) + plot_traj.axis(ymin=min_s[1], ymax=max_s[1]) + + plot_traj.plot(self.vector_w_t_c[:, 0], self.vector_w_t_c[:, 1], zs=self.vector_w_t_c[:, 2], label='Camera trajectory') + plot_traj.set_title('Camera trajectory w_t_c in world space') + plot_traj.legend() + filename = os.path.splitext(fig_filename)[0] + "-traj-w_t_c.png" + print(f"Figure is saved in {filename}") + plt.savefig(filename) + + plt.show() + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='The script corresponding to TP 4, IBVS on 4 points.') + parser.add_argument('--initial-position', type=int, default=2, dest='initial_position', help='Initial position selection (value 1, 2, or 3)') + parser.add_argument('--interaction', type=str, default="current", dest='interaction_matrix_type', help='Interaction matrix type (value \"current\" or \"desired\")') + parser.add_argument('--plot-log-scale', action='store_true', help='Plot norm of the error using a logarithmic scale') + parser.add_argument('--no-plot', action='store_true', help='Disable plots') + + args, unknown_args = parser.parse_known_args() + if unknown_args: + print("The following args are not recognized and will not be used: %s" % unknown_args) + sys.exit() + + print(f"Use initial position {args.initial_position}") + + # Position of the reference in the camera frame + c_T_w = HomogeneousMatrix() + + if args.initial_position == 1: + # - CASE 1 + thetau = ThetaUVector(0.0, 0.0, 0.0) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(0.0, 0.0, 1.3) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + elif args.initial_position == 2: + # - CASE 2 + thetau = ThetaUVector(Math.rad(10), Math.rad(20), Math.rad(30)) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(-0.2, -0.1, 1.3) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + elif args.initial_position == 3: + # - CASE 3 : 90 rotation along Z axis + thetau = ThetaUVector(0.0, 0.0, Math.rad(90)) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(0.0, 0.0, 1.0) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + else: + raise ValueError(f"Wrong initial position value. Values are 1, 2 or 3") + + # Position of the desired camera in the world reference frame + cd_T_w = HomogeneousMatrix() + thetau = ThetaUVector(0, 0, 0) + cd_R_w = RotationMatrix(thetau) + cd_t_w = TranslationVector(0.0, 0.0, 1.0) + cd_T_w.insert(cd_R_w) + cd_T_w.insert(cd_t_w) + + # 3D point in the reference frame in homogeneous coordinates + wX = [] + wX.append(Point(-0.1, 0.1, 0.0)) + wX.append(Point( 0.1, 0.1, 0.0)) + wX.append(Point( 0.1, -0.1, 0.0)) + wX.append(Point(-0.1, -0.1, 0.0)) + + # Creation of the current and desired features vectors respectively in x and xd + x = [FeaturePoint(), FeaturePoint(), FeaturePoint(),FeaturePoint()] # Initialize current visual feature + xd = [FeaturePoint(), FeaturePoint(), FeaturePoint(), FeaturePoint()] # Initialize desired visual feature + + # Create the visual servo task + task = Servo() + task.setServo(Servo.EYEINHAND_CAMERA) + task.setLambda(0.1) # Set the constant gain + + # For each point + for i in range(len(wX)): + # Create current visual feature + wX[i].track(c_T_w) + FeatureBuilder.create(x[i], wX[i]) + + print(f"Visual features at initial position for point {i}:") + print(f" wX[{i}]: {wX[i].get_oX()} {wX[i].get_oY()} {wX[i].get_oZ()}") + print(f" cX[{i}]: {wX[i].get_X()} {wX[i].get_Y()} {wX[i].get_Z()}") + print(f" x[{i}]: {x[i].get_x()} {x[i].get_y()} {x[i].get_Z()}") + + # Create desired visual feature + wX[i].track(cd_T_w) + FeatureBuilder.create(xd[i], wX[i]) + + print(f"Visual features at desired position for point {i}:") + print(f"cdX[{i}]: {wX[i].get_X()} {wX[i].get_Y()} {wX[i].get_Z()}") + print(f"xd[{i}]: {xd[i].get_x()} {xd[i].get_y()} {xd[i].get_Z()}") + + # Add current and desired features to the visual servo task + task.addFeature(x[i], xd[i]) + + iter = 0 + + # Control loop + while (iter == 0 or norm_e > 0.0001): + print(f"---- Visual servoing iteration {iter} ----") + # Considered vars: + # e: error vector + # norm_e: norm of the error vector + # v: velocity to apply to the camera + # x: current visual feature vector + # xd: desired visual feature vector + # c_T_w: current position of the camera in the world frame + if args.interaction_matrix_type == "current": + # Set interaction matrix type + task.setInteractionMatrixType(Servo.CURRENT, Servo.PSEUDO_INVERSE) + # Update visual features in x for each point + for i in range(len(wX)): + # Update current visual feature + wX[i].track(c_T_w); + FeatureBuilder.create(x[i], wX[i]) + elif args.interaction_matrix_type == "desired": + # Set interaction matrix type + task.setInteractionMatrixType(Servo.DESIRED, Servo.PSEUDO_INVERSE) + # Update visual features in xd + for i in range(len(wX)): + # Update current visual feature + wX[i].track(c_T_w); + FeatureBuilder.create(x[i], wX[i]) + # Update desired visual feature + wX[i].track(cd_T_w); + FeatureBuilder.create(xd[i], wX[i]) + else: + raise ValueError(f"Wrong interaction matrix type. Values are \"current\" or \"desired\"") + + # Compute the control law + v = task.computeControlLaw() + e = task.getError() + norm_e = e.frobeniusNorm() + Lx = task.getInteractionMatrix() + + xplot = [] + + if not args.no_plot: + for f in x: + p = FeaturePoint() + p.buildFrom(f.get_x(), f.get_y(), f.get_Z()) + xplot.append(p) + + if iter == 0: + plot = PlotIbvs(e, norm_e, v, xplot, xd, c_T_w, args.plot_log_scale) + else: + plot.stack(e, norm_e, v, xplot, xd, c_T_w) + + # Compute camera displacement after applying the velocity for delta_t seconds. + c_T_c_delta_t = ExponentialMap.direct(v, 0.040) + + # Compute the new position of the camera + c_T_w = c_T_c_delta_t.inverse() * c_T_w + + print(f"e: \n{e}") + print(f"norm e: \n{norm_e}") + print(f"Lx: \n{Lx}") + print(f"v: \n{v}") + print(f"c_T_w: \n{c_T_w}") + + # Increment iteration counter + iter += 1 + + print(f"\nConvergence achieved in {iter} iterations") + + if not args.no_plot: + # Display the servo behavior + plot.display("results/fig-ibvs-four-points-initial-position-" + str(args.initial_position) + ".png") + + print("Kill the figure to quit...") diff --git a/modules/python/examples/pbvs-four-points.py b/modules/python/examples/pbvs-four-points.py new file mode 100644 index 0000000000..0a06bb979d --- /dev/null +++ b/modules/python/examples/pbvs-four-points.py @@ -0,0 +1,298 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python example to simulate a position-based visual servo on four 2D points +# +############################################################################# + +import numpy as np +import argparse +import sys + +# For plots +import matplotlib.pyplot as plt +import os + +# Use latex in plot legends and labels +plt.rc('text', usetex=True) +plt.rc('text.latex', preamble=r'\usepackage{amsmath}') + +# ViSp Python bindings +from visp.core import ExponentialMap +from visp.core import HomogeneousMatrix +from visp.core import Math +from visp.core import Point +from visp.core import RotationMatrix +from visp.core import ThetaUVector +from visp.core import TranslationVector + +from visp.visual_features import FeatureTranslation +from visp.visual_features import FeatureThetaU + +from visp.vs import Servo + +class PlotPbvs: + def __init__(self, e, norm_e, v, x, xd, c_T_w, plot_log_scale): + self.vector_e = e + self.vector_ne = norm_e + self.vector_v = v + self.vector_x = x + self.vector_xd = xd + self.vector_w_t_c = c_T_w.inverse().getTranslationVector() + self.plot_log_scale = plot_log_scale + + def stack(self, e, norm_e, v, x, xd, c_T_w): + self.vector_e = np.vstack((self.vector_e, e)) + self.vector_ne = np.vstack((self.vector_ne, norm_e)) + self.vector_v = np.vstack((self.vector_v, v)) + self.vector_x = np.vstack((self.vector_x, x)) + self.vector_w_t_c = np.vstack((self.vector_w_t_c, c_T_w.inverse().getTranslationVector())) + + def display(self, fig_filename): + plt.figure(figsize=(10,10)) + + plot_e = plt.subplot(2, 2, 1) + plot_v = plt.subplot(2, 2, 2) + plot_ne = plt.subplot(2, 2, 3) + plot_x = plt.subplot(2, 2, 4) + + plot_e.set_title('error') + plot_v.set_title('camera velocity') + plot_x.set_title('point trajectory in the image plane') + + if self.plot_log_scale: + plot_ne.set_title('log(norm error)') + plot_ne.plot(np.log(self.vector_ne)) + else: + plot_ne.set_title('norm error') + plot_ne.plot(self.vector_ne) + + plot_ne.grid(True) + plot_e.grid(True) + plot_v.grid(True) + plot_x.grid(True) + + plot_e.plot(self.vector_e) + plot_e.legend(['$x_1$','$y_1$','$x_2$','$y_2$','$x_3$','$y_3$','$x_4$','$y_4$',]) + + for i in range(self.vector_v.shape[1]): # Should be 6 + plot_v.plot(self.vector_v[:,i]) + plot_v.legend(['$v_x$','$v_y$','$v_z$','$\omega_x$','$\omega_y$','$\omega_z$']) + + for i in range(self.vector_x.shape[1] // 2): # Note: Use // to divide an int and return an int + plot_x.plot(self.vector_x[:,2*i], self.vector_x[:,2*i+1]) + plot_x.legend(['$x_1$','$x_2$','$x_3$','$x_4$']) + for i in range(self.vector_x.shape[1] // 2): # Note: Use // to divide an int and return an int + plot_x.plot(self.vector_xd[2*i], self.vector_xd[2*i+1],'o') + + # Create output folder it it doesn't exist + output_folder = os.path.dirname(fig_filename) + if not os.path.exists(output_folder): + os.makedirs(output_folder) + print("Create output folder: ", output_folder) + + print(f"Figure is saved in {fig_filename}") + plt.savefig(fig_filename) + + # Plot 3D camera trajectory + plot_traj = plt.figure().add_subplot(projection='3d') + plot_traj.scatter(self.vector_w_t_c[0][0], self.vector_w_t_c[0][1], self.vector_w_t_c[0][2], marker='x', c='r', label='Initial position') + # Hack to ensure that the scale is at minimum between -0.5 and 0.5 along X and Y axis + min_s = np.min(self.vector_w_t_c, axis=0) + max_s = np.max(self.vector_w_t_c, axis=0) + for i in range(len(min_s)): + if (max_s[i] - min_s[i]) < 1.: + max_s[i] += 0.5 + min_s[i] -= 0.5 + plot_traj.axis(xmin=min_s[0], xmax=max_s[0]) + plot_traj.axis(ymin=min_s[1], ymax=max_s[1]) + + plot_traj.plot(self.vector_w_t_c[:, 0], self.vector_w_t_c[:, 1], zs=self.vector_w_t_c[:, 2], label='Camera trajectory') + plot_traj.set_title('Camera trajectory w_t_c in world space') + plot_traj.legend() + filename = os.path.splitext(fig_filename)[0] + "-traj-w_t_c.png" + print(f"Figure is saved in {filename}") + plt.savefig(filename) + + plt.show() + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='The script corresponding to TP 4, IBVS on 4 points.') + parser.add_argument('--initial-position', type=int, default=2, dest='initial_position', help='Initial position selection (value 1, 2, 3 or 4)') + parser.add_argument('--interaction', type=str, default="current", dest='interaction_matrix_type', help='Interaction matrix type (value \"current\" or \"desired\")') + parser.add_argument('--plot-log-scale', action='store_true', help='Plot norm of the error using a logarithmic scale') + parser.add_argument('--no-plot', action='store_true', help='Disable plots') + + args, unknown_args = parser.parse_known_args() + if unknown_args: + print("The following args are not recognized and will not be used: %s" % unknown_args) + sys.exit() + + print(f"Use initial position {args.initial_position}") + + # Position of the reference in the camera frame + c_T_w = HomogeneousMatrix() + + if args.initial_position == 1: + # - CASE 1 + thetau = ThetaUVector(0.0, 0.0, 0.0) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(0.0, 0.0, 1.3) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + elif args.initial_position == 2: + # - CASE 2 + thetau = ThetaUVector(Math.rad(10), Math.rad(20), Math.rad(30)) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(-0.2, -0.1, 1.3) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + elif args.initial_position == 3: + # - CASE 3 : 90 rotation along Z axis + thetau = ThetaUVector(0.0, 0.0, Math.rad(90)) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(0.0, 0.0, 1.0) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + elif args.initial_position == 4: + # - CASE 4 : 180 rotation along Z axis + thetau = ThetaUVector(0.0, 0.0, Math.rad(180)) + c_R_w = RotationMatrix(thetau) + c_t_w = TranslationVector(0.0, 0.0, 1.0) + c_T_w.insert(c_R_w) + c_T_w.insert(c_t_w) + else: + raise ValueError(f"Wrong initial position value. Values are 1, 2, 3 or 4") + + # Position of the desired camera in the world reference frame + cd_T_w = HomogeneousMatrix() + thetau = ThetaUVector(0, 0, 0) + cd_R_w = RotationMatrix(thetau) + cd_t_w = TranslationVector(0.0, 0.0, 1.0) + cd_T_w.insert(cd_R_w) + cd_T_w.insert(cd_t_w) + + # 3D point in the reference frame in homogeneous coordinates + wX = [] + wX.append(Point(-0.1, 0.1, 0.0)) + wX.append(Point( 0.1, 0.1, 0.0)) + wX.append(Point( 0.1, -0.1, 0.0)) + wX.append(Point(-0.1, -0.1, 0.0)) + + # Begin just for point trajectory display, compute the coordinates of the points in the image plane + x = np.zeros(8) # Current coordinates of the points [x1,y1,x2,y2,x3,y3,x4,y4] + xd = np.zeros(8) # desired coordinates of the points [xd1,yd1,xd2,yd2,xd3,yd3,xd4,yd4] + # Update the coordinates of the 4 desired points + for i in range(len(wX)): + wX[i].track(cd_T_w) + xd[2*i:2*i+2] = [wX[i].get_x(), wX[i].get_y()] + # End just for point trajectory display + + # Creation of the current (t and tu) and desired (td and tud) features vectors + t = FeatureTranslation(FeatureTranslation.cdMc) + tu = FeatureThetaU(FeatureThetaU.cdRc) + td = FeatureTranslation(FeatureTranslation.cdMc) + tud = FeatureThetaU(FeatureThetaU.cdRc) + + # Create the visual servo task + task = Servo() + task.setServo(Servo.EYEINHAND_CAMERA) + task.setLambda(0.1) # Set the constant gain + task.addFeature(t, td) + task.addFeature(tu, tud) + + iter = 0 + + # Control loop + while (iter == 0 or norm_e > 0.0001): + print(f"---- Visual servoing iteration {iter} ----") + # Considered vars: + # e: error vector + # norm_e: norm of the error vector + # v: velocity to apply to the camera + # x: current visual feature vector + # xd: desired visual feature vector + # c_T_w: current position of the camera in the world frame + + # Compute current features + cd_T_c = cd_T_w * c_T_w.inverse() + t.buildFrom(cd_T_c) + tu.buildFrom(cd_T_c) + + # Begin just for point trajectory display, compute the coordinates of the points in the image plane + for i in range(len(wX)): + wX[i].track(c_T_w) + x[2*i:2*i+2] = [wX[i].get_x(), wX[i].get_y()] + # End just for point trajectory display + + if args.interaction_matrix_type == "current": + # Set interaction matrix type + task.setInteractionMatrixType(Servo.CURRENT, Servo.PSEUDO_INVERSE) + elif args.interaction_matrix_type == "desired": + # Set interaction matrix type + task.setInteractionMatrixType(Servo.DESIRED, Servo.PSEUDO_INVERSE) + else: + raise ValueError(f"Wrong interaction matrix type. Values are \"current\" or \"desired\"") + + # Compute the control law + v = task.computeControlLaw() + e = task.getError() + norm_e = e.frobeniusNorm() + Lx = task.getInteractionMatrix() + + if not args.no_plot: + if iter == 0: + plot = PlotPbvs(e, norm_e, v, x, xd, c_T_w, args.plot_log_scale) + else: + plot.stack(e, norm_e, v, x, xd, c_T_w) + + # Compute camera displacement after applying the velocity for delta_t seconds. + c_T_c_delta_t = ExponentialMap.direct(v, 0.040) + + # Compute the new position of the camera + c_T_w = c_T_c_delta_t.inverse() * c_T_w + + print(f"e: \n{e}") + print(f"norm e: \n{norm_e}") + print(f"Lx: \n{Lx}") + print(f"v: \n{v}") + print(f"c_T_w: \n{c_T_w}") + + # Increment iteration counter + iter += 1 + + print(f"\nConvergence achieved in {iter} iterations") + + if not args.no_plot: + # Display the servo behavior + plot.display("results/fig-pbvs-four-points-initial-position-" + str(args.initial_position) + ".png") + + print("Kill the figure to quit...") diff --git a/modules/python/examples/realsense-mbt.py b/modules/python/examples/realsense-mbt.py new file mode 100644 index 0000000000..7c58c0ed47 --- /dev/null +++ b/modules/python/examples/realsense-mbt.py @@ -0,0 +1,230 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings example +# +############################################################################# + +import argparse +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, Tuple +import numpy as np +import time +import faulthandler +faulthandler.enable() + + +from visp.core import CameraParameters, HomogeneousMatrix +from visp.core import Color, Display, ImageConvert +from visp.core import ImageGray, ImageUInt16, ImageRGBa +from visp.io import ImageIo +from visp.mbt import MbGenericTracker +from visp.gui import DisplayOpenCV +import pyrealsense2 as rs + + +try: + import cv2 +except: + print('Could not import opencv-python! make sure that it is installed as it is required') + import sys + sys.exit(1) + +import matplotlib.pyplot as plt + + +class MBTModelData: + def __init__(self, data_root: Path, object_name: str): + model_path = data_root / 'model' / object_name + assert model_path.exists() + self.config_color = model_path / f'{object_name}.xml' + self.config_depth = model_path / f'{object_name}_depth.xml' + self.cad_file = model_path / f'{object_name}.cao' + self.init_file = model_path / f'{object_name}.init' + + +class MBTConfig: + def __init__(self, data_root: Path): + data_path = data_root / 'data' + assert data_path.exists() + self.extrinsic_file = str(data_path / 'depth_M_color.txt') + + +@dataclass +class FrameData: + I: ImageGray + I_depth: Optional[ImageUInt16] + point_cloud: Optional[np.ndarray] + + + + +def read_data(cam_depth: CameraParameters | None, I: ImageGray, pipe: rs.pipeline): + use_depth = cam_depth is not None + iteration = 1 + point_cloud_computer = rs.pointcloud() + while True: + frames = pipe.wait_for_frames() + I_np = np.asanyarray(frames.get_color_frame().as_frame().get_data()) + I_np = np.concatenate((I_np, np.ones_like(I_np[..., 0:1], dtype=np.uint8)), axis=-1) + I_rgba = ImageRGBa(I_np) + ImageConvert.convert(I_rgba, I, 0) + I_depth_raw = None + point_cloud = None + if use_depth: + I_depth_raw = np.asanyarray(frames.get_depth_frame().as_frame().get_data()) + point_cloud = np.asanyarray(point_cloud_computer.calculate(frames.get_depth_frame()).get_vertices()).view((np.float32, 3)) + iteration += 1 + yield FrameData(I, ImageUInt16(I_depth_raw), point_cloud) + + + +def cam_from_rs_profile(profile) -> Tuple[CameraParameters, int, int]: + intr = profile.as_video_stream_profile().get_intrinsics() # Downcast to video_stream_profile and fetch intrinsics + return CameraParameters(intr.fx, intr.fy, intr.ppx, intr.ppy), intr.height, intr.width + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--data-root', type=str, required=True, + help='Path to the folder containing all the data for the MBT example.') + parser.add_argument('--object-name', type=str, required=True, + help='Name of the object to track.') + parser.add_argument('--disable-klt', action='store_true', help='Disable KLT features for tracking.') + parser.add_argument('--disable-depth', action='store_true', help='Do not use depth to perform tracking.') + parser.add_argument('--step-by-step', action='store_true', help='Perform tracking frame by frame. Go to the next frame by clicking.') + + + args = parser.parse_args() + data_root = Path(args.data_root) + + + # Initialize realsense2 + pipe = rs.pipeline() + config = rs.config() + config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 60) + config.enable_stream(rs.stream.color, 640, 480, rs.format.rgb8, 60) + + cfg = pipe.start(config) + + assert data_root.exists() and data_root.is_dir() + + mbt_model = MBTModelData(data_root, args.object_name) + exp_config = MBTConfig(data_root) + + cam_color, color_height, color_width = cam_from_rs_profile(cfg.get_stream(rs.stream.color)) + cam_depth, depth_height, depth_width = cam_from_rs_profile(cfg.get_stream(rs.stream.depth)) + + + rgb_tracker: int = MbGenericTracker.EDGE_TRACKER | (MbGenericTracker.KLT_TRACKER if not args.disable_klt else 0) + tracker_types: List[int] = [rgb_tracker] + if not args.disable_depth: + depth_tracker = MbGenericTracker.DEPTH_DENSE_TRACKER + tracker_types.append(depth_tracker) + + tracker = MbGenericTracker(tracker_types) + + if args.disable_depth: + tracker.loadConfigFile(str(mbt_model.config_color)) + else: + tracker.loadConfigFile(str(mbt_model.config_color), str(mbt_model.config_depth)) + tracker.loadModel(str(mbt_model.cad_file)) + + # Camera intrinsics + + tracker.setCameraParameters(*((cam_color,) if args.disable_depth else (cam_color, cam_depth))) + tracker.setDisplayFeatures(True) + + print('Color intrinsics:', cam_color) + print('Depth intrinsics:', cam_depth) + I = ImageGray() + data_generator = read_data(cam_depth, I, pipe) + frame_data = next(data_generator) # Get first frame for init + + depth_M_color = HomogeneousMatrix() + if not args.disable_depth: + depth_M_color.load(exp_config.extrinsic_file) + tracker.setCameraTransformationMatrix('Camera2', depth_M_color) + + # Initialize displays + dI = DisplayOpenCV() + dI.init(I, 0, 0, 'Color image') + + I_depth = None if args.disable_depth else ImageGray() + dDepth = DisplayOpenCV() + if not args.disable_depth: + ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) + dDepth.init(I_depth, I.getWidth(), 0, 'Depth') + + for frame in data_generator: + Display.display(I) + Display.displayText(I, 0, 0, 'Click to initialize tracking', Color.red) + Display.flush(I) + event = Display.getClick(I, blocking=False) + if event: + break + + tracker.initClick(I, str(mbt_model.init_file)) + start_time = time.time() + for frame_data in data_generator: + if frame_data.I_depth is not None: + ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) + + Display.display(I) + if not args.disable_depth: + Display.display(I_depth) + + if args.disable_depth: + tracker.track(I=I) + else: + pc = frame_data.point_cloud + image_dict = { + 'Camera1': I + } + t = time.time() + tracker.track(image_dict, {'Camera2': pc.reshape(depth_height, depth_width, 3)}) + cMo = HomogeneousMatrix() + tracker.getPose(cMo) + + Display.displayFrame(I, cMo, cam_color, 0.05, Color.none, 2) + tracker.display(I, cMo, cam_color, Color.red, 2) + Display.flush(I) + if not args.disable_depth: + Display.flush(I_depth) + + if args.step_by_step: + Display.getKeyboardEvent(I, blocking=True) + else: + event = Display.getClick(I, blocking=False) + if event: + break + end_time = time.time() + print(f'total time = {end_time - start_time}s') diff --git a/modules/python/examples/synthetic-data-mbt.py b/modules/python/examples/synthetic-data-mbt.py new file mode 100644 index 0000000000..4929afa721 --- /dev/null +++ b/modules/python/examples/synthetic-data-mbt.py @@ -0,0 +1,255 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings example +# +############################################################################# + +import argparse +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional +import numpy as np +import time +import faulthandler +faulthandler.enable() + +from visp.core import XmlParserCamera, CameraParameters, ColVector, HomogeneousMatrix, Display, ImageConvert +from visp.core import ImageGray, ImageUInt16 +from visp.io import ImageIo +from visp.mbt import MbGenericTracker, MbTracker +from visp.gui import DisplayOpenCV +from visp.core import Color +from visp.core import PixelMeterConversion + +try: + import cv2 +except: + print('Could not import opencv python! make sure that it is installed as it is required') + import sys + sys.exit(1) + +import matplotlib.pyplot as plt + + +class MBTModelData: + def __init__(self, data_root: Path): + model_path = data_root / 'model' / 'teabox' + assert model_path.exists() + self.config_color = model_path / 'teabox_color.xml' + self.config_depth = model_path / 'teabox_depth.xml' + self.cad_file = model_path / 'teabox.cao' + self.init_file = model_path / 'teabox.init' + + +class MBTConfig: + def __init__(self, data_root: Path): + data_path = data_root / 'data' / 'teabox' + assert data_path.exists() + self.color_camera_name = 'Camera_L' + self.depth_camera_name = 'Camera_R' + + self.color_intrinsics_file = data_path / f'{self.color_camera_name}.xml' + self.depth_intrinsics_file = data_path / f'{self.depth_camera_name}.xml' + + self.color_images_dir = data_path / 'color' + self.depth_images_dir = data_path / 'depth' + self.ground_truth_dir = data_path / 'ground-truth' + + + self.depth_intrinsics_file = data_path / f'{self.depth_camera_name}.xml' + + self.extrinsic_file = str(data_path / 'depth_M_color.txt') + # self.ground_truth = str(data_root / 'data' / 'depth_M_color.txt') + +@dataclass +class FrameData: + I: ImageGray + I_depth: Optional[ImageUInt16] + point_cloud: Optional[np.ndarray] + cMo_ground_truth: HomogeneousMatrix + +def read_data(exp_config: MBTConfig, cam_depth: CameraParameters | None, I: ImageGray): + color_format = '{:04d}_L.jpg' + depth_format = 'Image{:04d}_R.exr' + use_depth = cam_depth is not None + iteration = 1 + while True: + start_parse_time = time.time() + color_filepath = exp_config.color_images_dir / color_format.format(iteration) + if not color_filepath.exists(): + print(f'Could not find image {color_filepath}, is the sequence finished?') + return + ImageIo.read(I, str(color_filepath), ImageIo.IO_DEFAULT_BACKEND) + + + I_depth_raw = None + point_cloud = None + if use_depth: + t = time.time() + depth_filepath = exp_config.depth_images_dir / depth_format.format(iteration) + if not depth_filepath.exists(): + print(f'Could not find image {depth_filepath}') + return + I_depth_np = cv2.imread(str(depth_filepath), cv2.IMREAD_ANYCOLOR | cv2.IMREAD_ANYDEPTH) + I_depth_np = I_depth_np[..., 0] + print(f'\tDepth load took {(time.time() - t) * 1000}ms') + I_depth_raw = ImageUInt16(I_depth_np * 32767.5) + if I_depth_np.size == 0: + print('Could not successfully read the depth image') + return + t = time.time() + # point_cloud = np.empty((*I_depth_np.shape, 3), dtype=np.float64) + Z = I_depth_np.copy() + Z[Z > 2] = 0.0 # Clamping values that are too high + + vs, us = np.meshgrid(range(I_depth_np.shape[0]), range(I_depth_np.shape[1]), indexing='ij') + xs, ys = PixelMeterConversion.convertPoints(cam_depth, us, vs) + point_cloud = np.stack((xs * Z, ys * Z, Z), axis=-1) + + print(f'\tPoint_cloud took {(time.time() - t) * 1000}ms') + + + cMo_ground_truth = HomogeneousMatrix() + ground_truth_file = exp_config.ground_truth_dir / (exp_config.color_camera_name + '_{:04d}.txt'.format(iteration)) + cMo_ground_truth.load(str(ground_truth_file)) + iteration += 1 + end_parse_time = time.time() + print(f'Data parsing took: {(end_parse_time - start_parse_time) * 1000}ms') + yield FrameData(I, I_depth_raw, point_cloud, cMo_ground_truth) + +def parse_camera_file(exp_config: MBTConfig, is_color: bool) -> CameraParameters: + cam = CameraParameters() + xml_parser = XmlParserCamera() + if is_color: + camera_name, file_path = exp_config.color_camera_name, exp_config.color_intrinsics_file + else: + camera_name, file_path = exp_config.depth_camera_name, exp_config.depth_intrinsics_file + parse_res = xml_parser.parse(cam, str(file_path), camera_name, + CameraParameters.perspectiveProjWithoutDistortion, 0, 0, True) + assert parse_res == XmlParserCamera.SEQUENCE_OK # Check result + return cam + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--data-root', type=str, required=True, + help='Path to the folder containing all the data for the synthetic MBT example') + parser.add_argument('--disable-klt', action='store_true', help='Disable KLT features for tracking.') + parser.add_argument('--disable-depth', action='store_true', help='Do not use depth to perform tracking.') + parser.add_argument('--step-by-step', action='store_true', help='Perform tracking frame by frame. Go to the next frame by clicking.') + parser.add_argument('--init-ground-truth', action='store_true') + + args = parser.parse_args() + data_root = Path(args.data_root) + + mbt_model = MBTModelData(data_root) + exp_config = MBTConfig(data_root) + + assert data_root.exists() and data_root.is_dir() + + rgb_tracker: int = MbGenericTracker.EDGE_TRACKER | (MbGenericTracker.KLT_TRACKER if not args.disable_klt else 0) + tracker_types: List[int] = [rgb_tracker] + if not args.disable_depth: + depth_tracker = MbGenericTracker.DEPTH_DENSE_TRACKER + tracker_types.append(depth_tracker) + + tracker = MbGenericTracker(tracker_types) + + if args.disable_depth: + tracker.loadConfigFile(str(mbt_model.config_color)) + else: + tracker.loadConfigFile(str(mbt_model.config_color), str(mbt_model.config_depth)) + tracker.loadModel(str(mbt_model.cad_file)) + + # Camera intrinsics + cam_color = parse_camera_file(exp_config, True) + cam_depth = parse_camera_file(exp_config, False) if not args.disable_depth else None + + tracker.setCameraParameters(*((cam_color,) if args.disable_depth else (cam_color, cam_depth))) + tracker.setDisplayFeatures(True) + + print('Color intrinsics:', cam_color) + print('Depth intrinsics:', cam_depth) + I = ImageGray() + data_generator = read_data(exp_config, cam_depth, I) + frame_data = next(data_generator) # Get first frame for init + + depth_M_color = HomogeneousMatrix() + if not args.disable_depth: + depth_M_color.load(exp_config.extrinsic_file) + tracker.setCameraTransformationMatrix('Camera2', depth_M_color) + + # Initialize displays + dI = DisplayOpenCV() + dI.init(I, 0, 0, 'Color image') + + I_depth = None if args.disable_depth else ImageGray() + dDepth = DisplayOpenCV() + if not args.disable_depth: + ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) + dDepth.init(I_depth, I.getWidth(), 0, 'Depth') + + if args.init_ground_truth: + tracker.initFromPose(I, frame_data.cMo_ground_truth) + else: + tracker.initClick(I, str(mbt_model.init_file)) + + start_time = time.time() + for frame_data in data_generator: + if frame_data.I_depth is not None: + ImageConvert.createDepthHistogram(frame_data.I_depth, I_depth) + + Display.display(I) + if not args.disable_depth: + Display.display(I_depth) + + if args.disable_depth: + tracker.track(I=I) + else: + pc = frame_data.point_cloud + image_dict = { + 'Camera1': I + } + t = time.time() + tracker.track(image_dict, {'Camera2': pc}) + print(f'Tracking took {(time.time() - t) * 1000}ms') + cMo = HomogeneousMatrix() + tracker.getPose(cMo) + + Display.displayFrame(I, cMo, cam_color, 0.05, Color.none, 2) + tracker.display(I, cMo, cam_color, Color.red, 2) + Display.flush(I) + if not args.disable_depth: + Display.flush(I_depth) + if args.step_by_step: + Display.getKeyboardEvent(I, blocking=True) + end_time = time.time() + print(f'total time = {end_time - start_time}s') diff --git a/modules/python/generator/pyproject.toml b/modules/python/generator/pyproject.toml new file mode 100644 index 0000000000..ccd2ef01da --- /dev/null +++ b/modules/python/generator/pyproject.toml @@ -0,0 +1,152 @@ +[project] +# This is the name of your project. The first time you publish this +# package, this name will be registered for you. It will determine how +# users can install this project, e.g.: +# +# $ pip install sampleproject +# +# And where it will live on PyPI: https://pypi.org/project/sampleproject/ +# +# There are some restrictions on what makes a valid project name +# specification here: +# https://packaging.python.org/specifications/core-metadata/#name +name = "visp_python_bindgen" # Required + +# Versions should comply with PEP 440: +# https://www.python.org/dev/peps/pep-0440/ +# +# For a discussion on single-sourcing the version, see +# https://packaging.python.org/guides/single-sourcing-package-version/ +version = "0.0.1" # Required + +# This is a one-line description or tagline of what your project does. This +# corresponds to the "Summary" metadata field: +# https://packaging.python.org/specifications/core-metadata/#summary +description = "Bindings generator for the ViSP library. Generates C++ code base on Pybind that allows using ViSP in Python." # Optional + +# This is an optional longer description of your project that represents +# the body of text which users will see when they visit PyPI. +# +# Often, this is the same as your README, so you can just read it in from +# that file directly (as we have already done above) +# +# This field corresponds to the "Description" metadata field: +# https://packaging.python.org/specifications/core-metadata/#description-optional +readme = "README.md" # Optional + +# Specify which Python versions you support. In contrast to the +# 'Programming Language' classifiers above, 'pip install' will check this +# and refuse to install the project if the version does not match. See +# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires +requires-python = ">=3.7" + +# This is either text indicating the license for the distribution, or a file +# that contains the license +# https://packaging.python.org/en/latest/specifications/core-metadata/#license +license = {file = "LICENSE.txt"} + +# This field adds keywords for your project which will appear on the +# project page. What does your project relate to? +# +# Note that this is a list of additional keywords, separated +# by commas, to be used to assist searching for the distribution in a +# larger catalog. +keywords = ["ViSP", "Visual Servoing", "Bindings generator", "Pybind"] # Optional + +# This should be your name or the name of the organization who originally +# authored the project, and a valid email address corresponding to the name +# listed. +authors = [ + {name = "Samuel Felton", email = "samuel.felton@irisa.fr" } # Optional +] + +# This should be your name or the names of the organization who currently +# maintains the project, and a valid email address corresponding to the name +# listed. +maintainers = [ + {name = "Samuel Felton", email = "samuel.felton@irisa.fr" } # Optional +] + +# Classifiers help users find your project by categorizing it. +# +# For a list of valid classifiers, see https://pypi.org/classifiers/ +classifiers = [ # Optional + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 3 - Beta", + + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "Topic :: Text Processing :: General", + + # Specify the Python versions you support here. In particular, ensure + # that you indicate you support Python 3. These classifiers are *not* + # checked by "pip install". See instead "python_requires" below. + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", +] + +# This field lists other packages that your project depends on to run. +# Any package you put here will be installed by pip when your project is +# installed, so they must be valid existing projects. +# +# For an analysis of this field vs pip's requirements files see: +# https://packaging.python.org/discussions/install-requires-vs-requirements/ +dependencies = [ + "tqdm", + "pcpp", + "cxxheaderparser@git+https://github.com/robotpy/cxxheaderparser#egg=master", + "lxml", + "doxmlparser@git+https://github.com/doxygen/doxygen#subdirectory=addon/doxmlparser" +] + +# List additional groups of dependencies here (e.g. development +# dependencies). Users will be able to install these using the "extras" +# syntax, for example: +# +# $ pip install sampleproject[dev] +# +# Similar to `dependencies` above, these must be valid existing +# projects. +# [project.optional-dependencies] # Optional + + +# List URLs that are relevant to your project +# +# This field corresponds to the "Project-URL" and "Home-Page" metadata fields: +# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use +# https://packaging.python.org/specifications/core-metadata/#home-page-optional +# +# Examples listed include a pattern for specifying where the package tracks +# issues, where the source is hosted, where to say thanks to the package +# maintainers, and where to support the project financially. The key is +# what's used to render the link text on PyPI. +[project.urls] # Optional +"Homepage" = "https://github.com/lagadic/visp" +"Bug Reports" = "https://github.com/lagadic/visp/issues" + +# The following would provide a command line executable called `sample` +# which executes the function `main` from this package when invoked. +[project.scripts] # Optional +visp_pybindgen = "visp_python_bindgen.generator:main" + +# This is configuration specific to the `setuptools` build backend. +# If you are using a different build backend, you will need to change this. +[tool.setuptools] +# If there are data files included in your packages that need to be +# installed, specify them here. +# package-data = {"sample" = ["*.dat"]} + +[build-system] +# These are the assumed default build requirements from pip: +# https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support +requires = ["setuptools>=43.0.0", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/modules/python/generator/visp_python_bindgen/__init__.py b/modules/python/generator/visp_python_bindgen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/python/generator/visp_python_bindgen/doc_parser.py b/modules/python/generator/visp_python_bindgen/doc_parser.py new file mode 100644 index 0000000000..4dd47d8325 --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/doc_parser.py @@ -0,0 +1,449 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +import_failed = False +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +import re +from cxxheaderparser.simple import parse_string +try: + import doxmlparser + from doxmlparser.compound import DoxCompoundKind, DoxygenType, compounddefType, descriptionType, docParaType, MixedContainer +except ImportError: + print('Cannot import xml parser') + import_failed = True + +from visp_python_bindgen.utils import * +from visp_python_bindgen.generator_config import GeneratorConfig + +class DocumentationObjectKind(Enum): + ''' + Kind of the object for which we seek the documentation + ''' + Class = 'class' + Struct = 'struct' + Method = 'method' + +class DocumentationData(object): + + @staticmethod + def get_xml_path_if_exists(name: str, kind: DocumentationObjectKind) -> Optional[Path]: + if import_failed: + return None + + xml_root = GeneratorConfig.xml_doc_path + if xml_root is None or not xml_root.exists(): + return None + p = None + if kind == DocumentationObjectKind.Class: + p = xml_root / f'class{name}.xml' + else: + assert False, 'Seeking documentation for type other than class not handled for now' + + if not p.exists(): + return None + return p + + +def to_cstring(s: str) -> str: + s = s.replace('\t', ' ') + s = re.sub('\n\n\n+', '\n\n', s) + s = re.sub('\\\\ +', '\\\\', s) + + return f'''R"doc( +{s} +)doc"''' + +@dataclass +class MethodDocSignature: + name: str + ret: str + params: List[str] + is_const: bool + is_static: bool + + def __init__(self, name, ret, params, is_const, is_static): + self.name = name.replace(' ', '') + self.ret = ret.replace(' ', '') + self.params = [p.replace(' ', '') for p in params] + self.is_const = is_const + self.is_static = is_static + + def __hash__(self) -> int: + s = f'{self.is_static} {self.ret} {self.name}({",".join(self.params)}) {self.is_const}' + return s.__hash__() + +@dataclass +class MethodDocumentation(object): + documentation: str + +@dataclass +class ClassDocumentation(object): + documentation: str +@dataclass +class EnumDocumentation(object): + general_documentation: str + value_documentation: Dict[str, str] + + def get_overall_doc(self) -> str: + full_doc = '' + full_doc += self.general_documentation.strip('\n') + full_doc += '\n\nValues: \n\n' + for k,v in self.value_documentation.items(): + full_doc += '* ' + full_doc += '**' + k + '**' + if len(v.strip('\n').strip()) > 0: + full_doc += ': ' + v.strip('\n') + full_doc += '\n\n' + + return to_cstring(full_doc) + + def get_value_doc(self, k: str) -> Optional[str]: + return to_cstring(self.value_documentation.get(k) or '') + +@dataclass +class DocElements(object): + compounddefs: Dict[str, compounddefType] + enums: Dict[str, doxmlparser.memberdefType] + methods: Dict[Tuple[str, MethodDocSignature], List[doxmlparser.memberdefType]] + +IGNORED_MIXED_CONTAINERS = [ + + 'parameterlist' +] + +IGNORED_SIMPLE_SECTS = [ + 'author', + 'date', +] + +def escape_for_rst(text: str) -> str: + forbidden_chars = ['_', '*'] + res = text + for c in forbidden_chars: + res = res.replace(c, '\\' + c) + return res + + + +def process_mixed_container(container: MixedContainer, level: int, level_string='', escape_rst=False) -> str: + ''' + :param level_string: the string being built for a single level (e.g. a line/paragraph of text) + This is equivalent to a left fold operation, + so don't forget to aggregate the results in level_string if you add another component + ''' + if container.name in IGNORED_MIXED_CONTAINERS: + return level_string + one_indent = ' ' * 2 + indent_str = one_indent * level + requires_space = not level_string.endswith(('\n', '\t', ' ')) and len(level_string) > 0 + + def process_markup(symbol: str) -> str: + text = symbol if not requires_space or len(level_string) == 0 else ' ' + symbol + for c in container.value.content_: + text = process_mixed_container(c, level, text, escape_rst=escape_rst) + text += symbol + ' ' + return text + + # Inline blocks + if isinstance(container.value, str) and container.name != 'verbatim': + content = container.value.replace('\n', '\n' + indent_str).strip() + if escape_rst: + content = escape_for_rst(content) + return level_string + content + if container.name == 'text': + content = container.value.replace('\n', '\n' + indent_str).strip() + if escape_rst: + content = escape_for_rst(content) + return level_string + content + if container.name == 'bold': + return level_string + process_markup('**') + if container.name == 'computeroutput': + return level_string + process_markup('`') + if container.name == 'emphasis': + markup_start = '*' if not requires_space else ' *' + return level_string + process_markup('*') + if container.name == 'sp': + return level_string + ' ' + if container.name == 'linebreak': + return level_string + '\n' + if container.name == 'heading': + text = container.value.valueOf_ + new_line_with_sep = '\n' + indent_str + '=' * len(text) + return level_string + new_line_with_sep + '\n' + indent_str + text + new_line_with_sep + if container.name == 'ulink': + url: doxmlparser.docURLLink = container.value + text = url.valueOf_ + url_value = url.url + return level_string + (' ' if requires_space else '') + f'`{text} <{url_value}>`_ ' + + if container.name == 'formula': + v: str = container.value.valueOf_.strip() + if v.startswith(('\\[', '$$')): + pure_math = indent_str + one_indent + v[2:-2].replace('\n', '') + return level_string + ('\n' + indent_str) * 2 + '.. math::' + '\n' + pure_math + '\n' + '\n' + else: + pure_math = v[1:-1].replace('\n', indent_str + one_indent) + return level_string + (' ' if requires_space else '') + ':math:`' + pure_math.strip() + '` ' + + if container.name == 'ref': # TODO: replace with Python refs if possible + return level_string + (' ' if requires_space else '') + container.value.valueOf_ + ' ' + + if container.name == 'verbatim': + text = container.value + text = escape_for_rst(text) + text = '\n' * 2 + indent_str + '::\n\n' + indent_str + one_indent + text.strip().replace('\n', '\n' + indent_str + one_indent).strip() + '\n' * 2 + return level_string + text + + # Block types + if container.name == 'simplesect': + process_fn = lambda item: process_paragraph(item, level + 1) + res = '\n' + kind = container.value.kind + if kind in IGNORED_SIMPLE_SECTS: + return level_string + item_content = '\n'.join(map(process_fn, container.value.para)) + item_content = re.sub('\n\n\n+', '\n\n', item_content) + if kind == 'note': + res += '\n' + indent_str + '.. note:: ' + item_content + '\n' + indent_str + elif kind == 'warning' or kind == 'attention': + res += '\n' + indent_str + '.. warning:: ' + item_content + '\n' + indent_str + elif kind == 'see': + res += '\n' + indent_str + '.. note:: See ' + item_content + '\n' + indent_str + elif kind == 'return': # Don't do anything, we will parse them separately when we get method documentation + return '' + else: + res += '\n' + f'' + return level_string + res + '\n' + + if container.name == 'blockquote': + blockquote: doxmlparser.docBlockQuoteType = container.value + process_fn = lambda item: process_paragraph(item, level + 1) + res = '\n' + item_content = '\n'.join(map(process_fn, blockquote.para)) + return level_string + item_content + '\n' + + if container.name == 'programlisting': + program: doxmlparser.listingType = container.value + codelines: List[doxmlparser.codelineType] = program.codeline + res = '\n\n' + indent_str + '.. code-block:: cpp' + '\n\n' + indent_str + one_indent + lines = [] + for line in codelines: + cs = [] + for h in line.highlight: + c = '' + for hh in h.content_: + c = process_mixed_container(hh, level, c, escape_rst=False) + cs.append(c) + s = ''.join(cs) + lines.append(s) + + code = ('\n' + indent_str + one_indent).join(lines) + res += code + '\n\n' + return level_string + res + + if container.name == 'itemizedlist': + items: List[doxmlparser.docListItemType] = container.value.listitem + res = '\n' + process_fn = lambda item: process_paragraph(item, level + 1) + for item in items: + item_content = '\n'.join(map(process_fn, item.para)) + item_content = re.sub('\n\n\n+', '\n\n', item_content) + #item_content = item_content.replace('\n' + indent_str, '\n' + indent_str + ' ') + res += '\n' + indent_str + '* ' + item_content + '\n' + indent_str + return level_string + res + '\n' + + return f'' + + +def process_paragraph(para: docParaType, level: int) -> str: + res = '' + contents: List[MixedContainer] = para.content_ + for content_item in contents: + res = process_mixed_container(content_item, level, res, escape_rst=True) + return res + +def process_description(brief: Optional[descriptionType]) -> str: + if brief is None: + return '' + para: List[docParaType] = brief.para + return '\n\n'.join([process_paragraph(par, 0) for par in para]) + +class DocumentationHolder(object): + def __init__(self, path: Optional[Path], env_mapping: Dict[str, str]): + self.xml_path = path + self.elements = None + if not import_failed and GeneratorConfig.xml_doc_path is not None: + if not self.xml_path.exists(): + print(f'Could not find documentation when looking in {str(path)}') + else: + self.xml_doc = doxmlparser.compound.parse(str(path), True, False) + compounddefs_res = {} + enums_res = {} + methods_res = {} + for compounddef in self.xml_doc.get_compounddef(): + compounddef: compounddefType = compounddef + if compounddef.kind == DoxCompoundKind.CLASS: + cls_name = compounddef.get_compoundname() + compounddefs_res[cls_name] = compounddef + section_defs: List[doxmlparser.sectiondefType] = compounddef.sectiondef + for section_def in section_defs: + member_defs: List[doxmlparser.memberdefType] = section_def.memberdef + enum_defs = [d for d in member_defs if d.kind == doxmlparser.compound.DoxMemberKind.ENUM and d.prot == 'public'] + method_defs = [d for d in member_defs if d.kind == doxmlparser.compound.DoxMemberKind.FUNCTION and d.prot == 'public'] + for method_def in method_defs: + is_const = False if method_def.const == 'no' else True + is_static = False if method_def.static == 'no' else True + ret_type = ''.join(process_mixed_container(c, 0, escape_rst=False) for c in method_def.type_.content_).replace('vp_deprecated', '').replace('VISP_EXPORT', '') + param_types = [] + for param in method_def.get_param(): + t = ''.join(process_mixed_container(c, 0, escape_rst=False) for c in param.type_.content_) + param_types.append(t) + if method_def.name == cls_name or ret_type != '': + signature_str = f'{ret_type} {cls_name}::{method_def.name}({",".join(param_types)}) {{}}' + method = parse_string(signature_str).namespace.method_impls[0] + method.static = is_static + method.const = is_const + + signature = MethodDocSignature(method_def.name, + get_type(method.return_type, {}, env_mapping) or '', # Don't use specializations so that we can match with doc + [get_type(param.type, {}, env_mapping) or '' for param in method.parameters], + method.const, method.static) + key = (compounddef.get_compoundname(), signature) + if key in methods_res: + num_paras = len(method_def.detaileddescription.para) + len(method_def.briefdescription.para) + if num_paras > 0: # Doxygen adds some empty memberdefs in addition to the ones where the doc is defined... + methods_res[key] = method_def + else: + methods_res[key] = method_def + + for enum_def in enum_defs: + enums_res[compounddef.get_compoundname() + '::' + enum_def.name] = enum_def + + self.elements = DocElements(compounddefs_res, enums_res, methods_res) + + def get_documentation_for_class(self, name: str, cpp_ref_to_python: Dict[str, str], specs: Dict[str, str]) -> Optional[ClassDocumentation]: + compounddef = self.elements.compounddefs.get(name) + if compounddef is None: + return None + cls_str = to_cstring(self.generate_class_description_string(compounddef)) + return ClassDocumentation(cls_str) + + def get_documentation_for_enum(self, enum_name: str) -> Optional[EnumDocumentation]: + member_def = self.elements.enums.get(enum_name) + if member_def is None: + return None + general_doc = self.generate_method_description_string(member_def) + value_doc = {} + for enum_val in member_def.enumvalue: + enum_value: doxmlparser.enumvalueType = enum_val + brief = process_description(enum_value.briefdescription) + detailed = process_description(enum_value.detaileddescription) + value_doc[enum_value.name] = brief + '\n\n' + detailed + return EnumDocumentation(general_doc, value_doc) + + def generate_class_description_string(self, compounddef: compounddefType) -> str: + brief = process_description(compounddef.get_briefdescription()) + detailed = process_description(compounddef.get_detaileddescription()) + return brief + '\n\n' + detailed + + def get_documentation_for_method(self, cls_name: str, signature: MethodDocSignature, cpp_ref_to_python: Dict[str, str], + specs: Dict[str, str], input_param_names: List[str], output_param_names: List[str]) -> Optional[MethodDocumentation]: + method_def = self.elements.methods.get((cls_name, signature)) + if method_def is None: + return None + + descr = self.generate_method_description_string(method_def) + + params_dict = self.get_method_params(method_def) + cpp_return_str = self.get_method_return_str(method_def) + + param_strs = [] + for param_name in input_param_names: + if param_name in params_dict: + param_strs.append(f':param {escape_for_rst(param_name)}: {params_dict[param_name]}') + param_str = '\n'.join(param_strs) + + if len(output_param_names) > 0: + return_str = ':return: A tuple containing:\n' # TODO: if we only return a single element, we should modify this + if signature.ret != 'void' and signature.ret is not None: + return_str += f'\n\t * {cpp_return_str}' + for param_name in output_param_names: + if param_name in params_dict: + return_str += f'\n\t * {escape_for_rst(param_name)}: {params_dict[param_name]}' + else: + return_str += f'\n\t * {escape_for_rst(param_name)}' + else: + return_str = f':return: {cpp_return_str}' if len(cpp_return_str) > 0 else '' + + res = to_cstring('\n\n'.join([descr, param_str, return_str])) + return MethodDocumentation(res) + + def generate_method_description_string(self, method_def: doxmlparser.memberdefType) -> str: + brief = process_description(method_def.get_briefdescription()) + detailed = process_description(method_def.get_detaileddescription()) + return brief + '\n\n' + detailed + + def get_method_params(self, method_def: doxmlparser.memberdefType) -> Dict[str, str]: + parameter_list_full: List[doxmlparser.docParamListItem] = [] + paras: List[doxmlparser.docParaType] = method_def.detaileddescription.para + method_def.inbodydescription.para + method_def.briefdescription.para + for paragraph in paras: + if paragraph.parameterlist is not None: + parameter_lists: List[doxmlparser.docParamListType] = paragraph.parameterlist + assert isinstance(parameter_lists, list) + for param_list in parameter_lists: + parameter_list_full.extend(param_list.parameteritem) + + params_dict = {} + for param_info in parameter_list_full: + from functools import reduce + name_list: List[doxmlparser.compound.docParamName] = reduce(lambda all, pnl: all + pnl.parametername, param_info.parameternamelist, []) + param_descr: doxmlparser.compound.descriptionType = param_info.parameterdescription + param_descr_str = ' '.join(map(lambda para: process_paragraph(para, 0), param_descr.para)).lstrip(': ').strip().replace('\n', '\n\t') + for param_name in name_list: + params_dict[param_name.valueOf_] = param_descr_str + return params_dict + + def get_method_return_str(self, method_def: doxmlparser.memberdefType) -> Dict[str, str]: + paras: List[doxmlparser.docParaType] = method_def.detaileddescription.para + method_def.inbodydescription.para + method_def.briefdescription.para + return_str = '' + for paragraph in paras: + sections: List[doxmlparser.docSimpleSectType] = paragraph.simplesect + for sect in sections: + if sect.kind == 'return': + return_str += ' '.join(map(lambda para: process_paragraph(para, 0), sect.para)) + return return_str diff --git a/modules/python/generator/visp_python_bindgen/enum_binding.py b/modules/python/generator/visp_python_bindgen/enum_binding.py new file mode 100644 index 0000000000..23f417ee1e --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/enum_binding.py @@ -0,0 +1,239 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List, Optional, Tuple, Dict, Union +from dataclasses import dataclass +import logging + +from cxxheaderparser import types +from cxxheaderparser.simple import NamespaceScope, ClassScope + +from visp_python_bindgen.utils import * +from visp_python_bindgen.submodule import Submodule + + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from visp_python_bindgen.header import SingleObjectBindings, HeaderFile + +@dataclass +class EnumRepr: + ''' + Intermediate representation of an enumeration + ''' + id: Optional[int] # Integer Id for an enumeration. Used when an enumeration is anonymous (hidden behind a typedef). Id is unique per header file + name: Optional[str] # Name of the enumeration + values: Optional[List[types.Enumerator]] # The values of the enumeration + public_access: bool = True # Whether this enum is visible from outside the header file + +def is_typedef_to_enum(typedef: types.Typedef): + ''' + Check whether a typedef refers to an enum + ''' + if not isinstance(typedef.type, types.Type): + return False + if not typedef.type.typename.classkey == 'enum': + return False + return True + +def is_anonymous_name(typename: types.PQName) -> Tuple[bool, int]: + ''' + Check whether the name is anonymous. If it is, then the actual name is defined in a typedef. + ''' + for segment in typename.segments: + if isinstance(segment, types.AnonymousName): + return True, segment.id + return False, None + +def get_owner_py_ident(owner_name: str, root_scope: NamespaceScope) -> Optional[str]: + ''' + Get the the owner's identifier (variable in generated code). + If the owner is not an exported symbol (exported symbols are classes, enums, structs) then None is returned + ''' + scope = get_cpp_identifier_scope(owner_name, root_scope) + + if isinstance(scope, NamespaceScope): + return None + + return f'py{get_name(scope.class_decl.typename).replace("vp", "")}' #TODO: fix for custom names + +def get_cpp_identifier_scope(fully_qualified_name: str, root_scope: Union[NamespaceScope, ClassScope]) -> Union[NamespaceScope, ClassScope]: + if fully_qualified_name == '': + return root_scope + segments = fully_qualified_name.split('::') + first_segment, remainder = segments[0], segments[1:] + for cls in root_scope.classes: + if get_name(cls.class_decl.typename) == first_segment: + return get_cpp_identifier_scope('::'.join(remainder), cls) + if isinstance(root_scope, NamespaceScope): + for ns in root_scope.namespaces: + if ns == first_segment: + return get_cpp_identifier_scope('::'.join(remainder), root_scope.namespaces[ns]) + return root_scope + +def resolve_enums_and_typedefs(root_scope: NamespaceScope, mapping: Dict) -> Tuple[List[EnumRepr], List[EnumRepr]]: + final_data: List[EnumRepr] = [] + temp_data: List[EnumRepr] = [] # Data that is incomplete for preprocessing + match_id = lambda repr, enum_id: repr.id is not None and enum_id is not None and repr.id == enum_id + match_name = lambda repr, full_name: repr.name is not None and full_name is not None and repr.name == full_name + enum_repr_is_ready = lambda repr: repr.name is not None and repr.values is not None + + def accumulate_data(scope: Union[NamespaceScope, ClassScope]): + if isinstance(scope, ClassScope): + if scope.class_decl.access is not None and scope.class_decl.access != 'public': + return + for cls in scope.classes: + accumulate_data(cls) + if isinstance(scope, NamespaceScope): + for namespace in scope.namespaces: + accumulate_data(scope.namespaces[namespace]) + for enum in scope.enums: + public_access = True + anonymous_enum, enum_id = is_anonymous_name(enum.typename) + + full_name = get_typename(enum.typename, {}, mapping) if not anonymous_enum else None + # If in a namespace or parent class, this symbol can be referenced from outside and the visibility will be incorrect + if enum.access is not None and enum.access != 'public': + public_access = False + + if full_name is not None and '::' in full_name: + base_ref = fetch_fully_qualified_id(root_scope, full_name.split('::')) + if base_ref is not None and isinstance(base_ref, types.EnumDecl) and base_ref.access is not None and base_ref.access != 'public': + public_access = False + + matches = lambda repr: match_id(repr, enum_id) or match_name(repr, full_name) + matching = list(filter(matches, temp_data)) + assert len(matching) <= 1, f"There cannot be more than one repr found. Matches = {matching}" + if len(matching) == 0: + temp_data.append(EnumRepr(enum_id, full_name, enum.values, public_access)) + else: + if full_name is not None: + matching[0].name = full_name + if enum.values is not None: + matching[0].values = enum.values + matching[0].public_access = matching[0].public_access and public_access # If we found a private def somewhere, mark enum as private + + for typedef in scope.typedefs: + if not is_typedef_to_enum(typedef): + continue + public_access = True + if typedef.access is not None and typedef.access != 'public': + public_access = False + + anonymous_enum, enum_id = is_anonymous_name(typedef.type.typename) + full_name = mapping[typedef.name] + + matches = lambda repr: match_id(repr, enum_id) or match_name(repr, full_name) + matching = list(filter(matches, temp_data)) + assert len(matching) <= 1, f"There cannot be more than one repr found. Matches = {matching}" + if len(matching) == 0: + temp_data.append(EnumRepr(enum_id, full_name, None, public_access)) + else: + if full_name is not None: + matching[0].name = full_name + matching[0].public_access = matching[0].public_access and public_access + + ready_enums = list(filter(enum_repr_is_ready, temp_data)) + for repr in ready_enums: + final_data.append(repr) + temp_data.remove(repr) + + accumulate_data(root_scope) + return final_data, temp_data + +def get_enum_bindings(root_scope: NamespaceScope, mapping: Dict, submodule: Submodule, header: 'HeaderFile') -> List[SingleObjectBindings]: + + final_data, filtered_reprs = resolve_enums_and_typedefs(root_scope, mapping) + + for repr in filtered_reprs: + logging.info(f'Enum {repr} was ignored, because it is incomplete (missing values or name)') + + result: List['SingleObjectBindings'] = [] + final_reprs = [] + for repr in final_data: + + enum_config = submodule.get_enum_config(repr.name) + if enum_config['ignore']: + filtered_reprs.append(repr) + logging.info(f'Enum {repr.name} is ignored by user') + elif repr.public_access: + final_reprs.append(repr) + else: + filtered_reprs.append(repr) + doc_holder = header.documentation_holder + for enum_repr in final_reprs: + name_segments = enum_repr.name.split('::') + py_name = name_segments[-1].replace('vp', '') + # If an owner class is ignored, don't export this enum + parent_ignored = False + ignored_parent_name = None + enum_doc = None + if doc_holder is not None: + enum_doc = header.documentation_holder.get_documentation_for_enum(repr.name) + + for segment in name_segments[:-1]: + full_segment_name = mapping.get(segment) + if full_segment_name is not None and submodule.class_should_be_ignored(full_segment_name): + parent_ignored = True + ignored_parent_name = full_segment_name + break + + if parent_ignored: + logging.info(f'Ignoring enum {py_name} because {ignored_parent_name} is ignored') + continue + + owner_full_name = '::'.join(name_segments[:-1]) + owner_py_ident = get_owner_py_ident(owner_full_name, root_scope) or 'submodule' + py_ident = f'py{owner_py_ident}{py_name}' + py_args = ['py::arithmetic()'] + if enum_doc is not None: + py_args = [enum_doc.get_overall_doc()] + py_args + + py_args_str = ','.join(py_args) + declaration = f'py::enum_<{enum_repr.name}> {py_ident}({owner_py_ident}, "{py_name}", {py_args_str});' + values = [] + for enumerator in enum_repr.values: + maybe_value_doc = None + # if enum_doc is not None: + # maybe_value_doc = enum_doc.get_value_doc(enumerator.name) + maybe_value_doc_str = f', {maybe_value_doc}' if maybe_value_doc else '' + + values.append(f'{py_ident}.value("{enumerator.name}", {enum_repr.name}::{enumerator.name}{maybe_value_doc_str});') + + values.append(f'{py_ident}.export_values();') + enum_names = BoundObjectNames(py_ident, py_name, enum_repr.name, enum_repr.name) + enum_binding = SingleObjectBindings(enum_names, declaration, values, GenerationObjectType.Enum) + result.append(enum_binding) + return result diff --git a/modules/python/generator/visp_python_bindgen/gen_report.py b/modules/python/generator/visp_python_bindgen/gen_report.py new file mode 100644 index 0000000000..1eb606606f --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/gen_report.py @@ -0,0 +1,156 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List, Dict +from pathlib import Path +import json + +from cxxheaderparser import types + +from visp_python_bindgen.utils import * +from visp_python_bindgen.methods import NotGeneratedReason, RejectedMethod + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from visp_python_bindgen.submodule import Submodule + +class Report(object): + def __init__(self, submodule: 'Submodule'): + self.submodule_name = submodule.name + self.result = { + 'ignored_headers': [], + 'returns_ref': [], + 'holds_pointer_or_ref': [], + 'classes': {}, + 'methods': {}, + 'default_param_policy_methods': [], + } + + def add_ignored_header(self, path: Path) -> None: + self.result['ignored_headers'].append(str(path)) + + def add_non_generated_class(self, cls_name: str, config: Dict, reason: str) -> None: + self.result['classes'][cls_name] = { + 'reason': reason + } + + def add_non_generated_method(self, method: RejectedMethod) -> None: + if NotGeneratedReason.is_non_trivial_reason(method.rejection_reason): + proposed_help = { + 'static': method.method.static, + 'signature': method.signature, + 'ignore': True + } + report_dict = { + 'reason': method.rejection_reason.value, + 'fix': proposed_help + } + if method.cls_name is not None: + report_dict['class'] = method.cls_name + self.result['methods'][method.signature] = report_dict + + def add_default_policy_method(self, cls_name: str, method: types.Method, signature: str, input_params: List[bool], output_params: List[bool]) -> None: + proposed_help = [ + { + 'static': method.static, + 'signature': signature, + 'use_default_param_policy': True + }, + { + 'static': method.static, + 'signature': signature, + 'use_default_param_policy': False, + 'param_is_input': input_params, + 'param_is_output': output_params + } + ] + report_dict = { + 'reason': 'Method uses default parameter policy, make sure that this is correct! If it is use "use_default_param_policy": true. Otherwise, set the custom inputness/outputness of params', + 'signature': signature, + 'static': method.static, + 'class': cls_name, + 'possible_fixes': proposed_help + } + self.result['default_param_policy_methods'].append(report_dict) + def add_method_returning_ref(self, cls_name: str, method: types.Method, signature: str) -> None: + proposed_help = [ + { + 'static': method.static, + 'signature': signature, + 'return_policy': 'reference', + 'keep_alive': [[1, 0]], + 'returns_ref_ok': True + }, + ] + report_dict = { + 'reason': 'Method returns a reference: this can lead to memory leaks or unwarranted copies. Ensure that keep_alive and return_policy are correctly set. If ok, set return_ref_ok to True', + 'signature': signature, + 'static': method.static, + 'class': cls_name, + 'possible_fixes': proposed_help + } + self.result['returns_ref'].append(report_dict) + + def add_pointer_or_ref_holder(self, cls_name: str, fieldnames: List[str]) -> None: + proposed_help = [ + { + 'acknowledge_pointer_or_ref_fields': fieldnames + }, + ] + report_dict = { + 'reason': 'This class stores a pointer or a raw reference, when interfaced with python, methods that return this reference or pointer lead to double frees or memory leaks', + 'class': cls_name, + 'possible_fixes': proposed_help + } + self.result['holds_pointer_or_ref'].append(report_dict) + + def write(self, path: Path) -> None: + print('=' * 50) + print(f'Statistics for module {self.submodule_name}:') + stats = [ + f'Ignored headers: {len(self.result["ignored_headers"])}', + f'Ignored classes: {len(self.result["classes"].keys())}', + f'Unacknowledged pointer/ref holders: {len(self.result["holds_pointer_or_ref"])}', + + f'Ignored methods: {len(self.result["methods"].keys())}', + f'Methods with default parameter policy: {len(self.result["default_param_policy_methods"])}', + f'Methods returning a reference: {len(self.result["returns_ref"])}', + ] + print('\n\t', '\n\t'.join(stats), '\n', sep='') + with open(path, 'w') as report_file: + json.dump(self.result, report_file, indent=2) + + print(f'A JSON report has been written to {path.absolute()}') + print('=' * 50) diff --git a/modules/python/generator/visp_python_bindgen/generator.py b/modules/python/generator/visp_python_bindgen/generator.py new file mode 100644 index 0000000000..51216b27eb --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/generator.py @@ -0,0 +1,177 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List +import sys +from pathlib import Path +from multiprocessing import Pool +import argparse +import logging + +from cxxheaderparser.errors import CxxParseError + +from visp_python_bindgen.header import * +from visp_python_bindgen.submodule import * +from visp_python_bindgen.generator_config import GeneratorConfig + +def header_preprocess(header: HeaderFile): + ''' + Preprocess a single header. Supposed to be called from a subprocess. + ''' + try: + header.preprocess() + return header + except Exception as e: + error_msg = 'There was an error when processing header' + str(header.path) + logging.error(error_msg) + import traceback + logging.error(traceback.format_exc()) + logging.error(type(e)) + if isinstance(e, CxxParseError): + logging.error(header.preprocessed_header_str) + + print(error_msg, 'See the text log in the build folder for more information.') + + return None + +def main_str(submodule_fn_declarations, submodule_fn_calls): + ''' + Main binding generation content + :param submodule_fn_declarations: declaration of the functions that bind the submodules + :param submodule_fn_calls: Calls to the functions declared above + + ''' + return f''' +//#define PYBIND11_DETAILED_ERROR_MESSAGES +#include +namespace py = pybind11; +{submodule_fn_declarations} + +PYBIND11_MODULE(_visp, m) +{{ + m.doc() = "ViSP Python binding"; + + {submodule_fn_calls} +}} +''' + +def generate_module(generate_path: Path, config_path: Path) -> None: + + submodules: List[Submodule] = get_submodules(config_path, generate_path) + + # Step 1: Preprocess all headers + all_headers: List[HeaderFile] = [] + for submodule in submodules: + all_headers.extend(submodule.headers) + + from tqdm import tqdm + # Parallel processing of headers to speedup this step + # This parallel implementation is disabled, + # since the behaviour on Mac is different and leads to preprocessing not finding vpConfig.h and others + # Reverting to a single process version fixes the issue + # with Pool() as pool: + # new_all_headers = [] + # for result in list(tqdm(pool.imap(header_preprocess, all_headers), total=len(all_headers), file=sys.stderr)): + # if result is None: + # raise RuntimeError('There was an exception when processing headers: You should either ignore the faulty header/class, or fix the generator code!') + # new_all_headers.append(result) + + new_all_headers = [] + for result in list(tqdm(map(header_preprocess, all_headers), total=len(all_headers), file=sys.stderr, unit="hdr")): + if result is None: + raise RuntimeError('There was an exception when processing headers: You should either ignore the faulty header/class, or fix the generator code!') + new_all_headers.append(result) + + # Sort headers according to the dependencies. This is done across all modules. + # TODO: sort module generation order. For now this works but it's fairly brittle + new_all_headers = sort_headers(new_all_headers) + for header in new_all_headers: + header.compute_environment() + + headers_with_deps = list(map(lambda header: (header, header.get_header_dependencies(new_all_headers)), new_all_headers)) + + for header, header_deps in headers_with_deps: + other_mappings = list(map(lambda h: h.environment.mapping, header_deps)) + header.environment.update_with_dependencies(other_mappings) + + for submodule in submodules: + submodule.set_headers_from_common_list(new_all_headers) + + # Step 2: generate the binding code for each submodule + # Each submodule write to its own file(s) its binding code + for submodule in submodules: + submodule.generate() + + # Step 3: write to main.cpp the call to the submodule binding implementations. + main_path = generate_path / 'main.cpp' + with open(main_path, 'w') as main_file: + submodule_fn_declarations = [] + submodule_fn_calls = [] + for submodule in submodules: + name = submodule.generation_function_name() + submodule_fn_declarations.append(f'void {name}(py::module_&);') + submodule_fn_calls.append(f'{name}(m);') + + submodule_fn_declarations = '\n'.join(submodule_fn_declarations) + submodule_fn_calls = '\n'.join(submodule_fn_calls) + + format_str = main_str(submodule_fn_declarations, submodule_fn_calls) + main_file.write(format_str) + +def main(): + parser = argparse.ArgumentParser(description='Python Bindings generator for ViSP') + parser.add_argument('--config', type=str, required=True, help='Path to the folder containing the module configurations (one .json file per module)') + parser.add_argument('--build-folder', type=str, required=True, help='Where to save the generated binding code') + parser.add_argument('--main-config', type=str, required=True, help='Path to the .json file detailing which modules to build, include directories etc.') + + args = parser.parse_args() + + generation_path = Path(args.build_folder) + logging.basicConfig(filename=str(generation_path / 'generation.log'), filemode='w', level=logging.DEBUG) + assert generation_path.exists(), f'Path to where to generate bindings does not exist! Path: {generation_path}' + + generation_path_src = generation_path / 'src' + generation_path_src.mkdir(exist_ok=True) + + config_path = Path(args.config) + assert config_path.exists(), f'Path to the folder containing the configuration files does not exist! Path: {config_path}' + + main_config_path = Path(args.main_config) + assert main_config_path.exists(), f'Path to the main JSON configuration is invalid! Path: {main_config_path}' + GeneratorConfig.update_from_main_config_file(main_config_path) + generate_module(generation_path_src, config_path) + +if __name__ == '__main__': + main() diff --git a/modules/python/generator/visp_python_bindgen/generator_config.py b/modules/python/generator/visp_python_bindgen/generator_config.py new file mode 100644 index 0000000000..67b15576dc --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/generator_config.py @@ -0,0 +1,184 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# +import logging +from typing import Dict, Final, List, Optional +import re +from pathlib import Path +from dataclasses import dataclass +import json + +@dataclass +class ModuleInputData(object): + name: str + headers: List[Path] + dependencies: List[str] + +@dataclass +class PreprocessorConfig(object): + ''' + Preprocessor config. Contains the arguments that are passed to pcpp to preprocess a header. + This does not include the header search path (-I) or the input and output paths + ''' + + defines: Dict[str, str] # Mapping from a #define to its value (#define A 1 is equal to a pair "A": "1") + never_defined: List[str] # List of macros that should never be defined, even if they are defined in other included headers + include_directories: List[str] + passthrough_includes_regex: str # Regex to see which header should be included (expanded and put in the resulting pcpp output) or ignored + line_directive: Optional[str] # prefix for Warning/logging emitted by pcpp. If none, no warning + other_args: List[str] + + def to_pcpp_args_list(self) -> List[str]: + args = [] + for k,v in self.defines.items(): + args += ['-D', f'{k}={v}'] if v is not None else ['-D', k] + for v in self.never_defined: + args += ['-N', v] + for v in self.include_directories: + args += ['-I', v] + args += self.other_args + args.extend(['--passthru-includes', self.passthrough_includes_regex]) + if self.line_directive is not None: + args.extend(['--line-directive', self.line_directive]) + else: + args.extend(['--line-directive', '']) + return args + +''' +Regular expressions that should match with types that are considered as immutable on the Python side +This only encompasses raw types +''' +IMMUTABLE_TYPES_REGEXS = [ + '^(float|double|u?int\d+_t|unsigned|unsigned int|size_t|ssize_t|char|long|long\wlong|bool)$', + '^std::string$' +] + +''' +Regular expressions that should match with types that are considered as immutable on the Python side +This only encompasses raw types +''' +IMMUTABLE_CONTAINERS_REGEXS = [ + '^std::vector', '^std::list' +] + + +''' +Specific argument regexs for which having default arguments is specifically forbidden +''' +FORBIDDEN_DEFAULT_ARGUMENT_TYPES_REGEXS = [ + '^std::ostream', + '^std::initializer_list', + '^rs2::', + '^cv::' +] + +''' +Regexes for names of functions that should be ignored +''' +FORBIDDEN_FUNCTION_NAMES_REGEXS = [ + '^(from|to)_.*json', + '^operator.*', + '^ignored$' +] +class GeneratorConfig(object): + pcpp_config: Final[PreprocessorConfig] = PreprocessorConfig( + defines={ + 'VISP_EXPORT': '', # remove symbol as it messes up the cxxheaderparsing + 'vp_deprecated': '', # remove symbol as it messes up the cxxheaderparsing + 'DOXYGEN_SHOULD_SKIP_THIS': None, # Do not generate methods that do not appear in public api doc + 'NLOHMANN_JSON_SERIALIZE_ENUM(a,...)': 'void ignored() {}', # Remove json enum serialization as it cnanot correctly be parsed + 'CV_OUT': '', # In vpKeyPoint, this appears and breaks parsing + }, + never_defined=[ + 'VISP_BUILD_DEPRECATED_FUNCTIONS', # Do not bind deprecated functions + 'VISP_RUBIK_REGULAR_FONT_RESOURCES' + ], + include_directories=[], # Populated through the main configuration file + passthrough_includes_regex="^.*$", # Never output the result of other includes. + line_directive=None, + other_args=['--passthru-unfound-includes'] #"--passthru-comments" + ) + + xml_doc_path: Optional[Path] = None + + module_data: List[ModuleInputData] = [] + + @staticmethod + def _matches_regex_in_list(s: str, regexes: List[str]) -> bool: + return any(map(lambda regex: re.match(regex, s) is not None, regexes)) + + @staticmethod + def is_immutable_type(type: str) -> bool: + return GeneratorConfig._matches_regex_in_list(type, IMMUTABLE_TYPES_REGEXS) + + @staticmethod + def is_immutable_container(type: str) -> bool: + return GeneratorConfig._matches_regex_in_list(type, IMMUTABLE_CONTAINERS_REGEXS) + + @staticmethod + def is_forbidden_default_argument_type(type: str) -> bool: + return GeneratorConfig._matches_regex_in_list(type, FORBIDDEN_DEFAULT_ARGUMENT_TYPES_REGEXS) + + @staticmethod + def is_forbidden_function_name(name: str) -> bool: + return GeneratorConfig._matches_regex_in_list(name, FORBIDDEN_FUNCTION_NAMES_REGEXS) + + @staticmethod + def update_from_main_config_file(path: Path) -> None: + assert path.exists() + with open(path, 'r') as main_config_file: + main_config = json.load(main_config_file) + logging.info('Updating the generator config from dict: ', main_config) + GeneratorConfig.pcpp_config.include_directories = main_config['include_dirs'] + + defines = main_config.get('defines') + if defines is not None: + for define_key in defines: + GeneratorConfig.pcpp_config.defines[define_key] = defines[define_key] + + xml_doc_path = main_config.get('xml_doc_path') + if xml_doc_path is not None: + GeneratorConfig.xml_doc_path = Path(xml_doc_path) + + modules_dict = main_config.get('modules') + source_dir = Path(main_config.get('source_dir')) + for module_name in modules_dict: + headers = map(lambda s: Path(s), modules_dict[module_name].get('headers')) + deps = modules_dict[module_name].get('dependencies') + + # Include only headers that are in the VISP source directory + headers = list(filter(lambda h: source_dir in h.parents, headers)) + headers_log_str = '\n\t'.join([str(header) for header in headers]) + logging.info(f'Module {module_name} headers: \n\t{headers_log_str}') + GeneratorConfig.module_data.append(ModuleInputData(module_name, headers, deps)) diff --git a/modules/python/generator/visp_python_bindgen/header.py b/modules/python/generator/visp_python_bindgen/header.py new file mode 100644 index 0000000000..faa19986aa --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/header.py @@ -0,0 +1,546 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + + +from typing import List, Optional, Dict +from pathlib import Path +from dataclasses import dataclass +from collections import OrderedDict +import logging + +import pcpp +from cxxheaderparser import types +from cxxheaderparser.simple import parse_string, ParsedData, NamespaceScope, ClassScope + +from visp_python_bindgen.utils import * +from visp_python_bindgen.methods import * +from visp_python_bindgen.doc_parser import * +from visp_python_bindgen.header_utils import * +from visp_python_bindgen.generator_config import GeneratorConfig + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from submodule import Submodule + +class HeaderFile(): + def __init__(self, path: Path, submodule: 'Submodule'): + self.path = path + self.submodule = submodule + self.includes = [f''] + self.binding_code = None + self.header_repr = None + self.contains = [] + self.depends = [] + self.documentation_holder_path: Path = None + self.documentation_holder = None + self.environment: HeaderEnvironment = None + + def __getstate__(self): + return self.__dict__ + def __setstate__(self, d): + self.__dict__ = d + + def get_header_dependencies(self, headers: List['HeaderFile']) -> List['HeaderFile']: + if len(self.depends) == 0: + return [] + header_deps = [] + for header in headers: + if header == self: + continue + is_dependency = False + for d in self.depends: + if d in header.contains: + is_dependency = True + break + if is_dependency: + header_deps.append(header) + upper_dependencies = header.get_header_dependencies(headers) + header_deps.extend(upper_dependencies) + return header_deps + + def preprocess(self) -> None: + ''' + Preprocess the header to obtain the abstract representation of the cpp classes available. + Additionally get the path to the xml documentation file generated by doxygen + ''' + from cxxheaderparser.options import ParserOptions + self.preprocessed_header_str = self.run_preprocessor() # Run preprocessor, get only code that can be compiled with current visp + + self.header_repr: ParsedData = parse_string(self.preprocessed_header_str, options=ParserOptions(verbose=False, convert_void_to_zero_params=True)) # Get the cxxheaderparser representation of the header + + # Get dependencies of this header. This is important for the code generation order + for cls in self.header_repr.namespace.classes: + name_cpp_no_template = '::'.join([seg.name for seg in cls.class_decl.typename.segments]) + self.contains.append(name_cpp_no_template) + + # Add parent classes as dependencies + for base_class in cls.class_decl.bases: + base_class_str_no_template = '::'.join([segment.name for segment in base_class.typename.segments]) + if base_class_str_no_template.startswith('vp'): + self.depends.append(base_class_str_no_template) + + # Get documentation if available, only one document supported for now + if self.documentation_holder_path is None: + self.documentation_holder_path = DocumentationData.get_xml_path_if_exists(name_cpp_no_template, DocumentationObjectKind.Class) + + def run_preprocessor(self): + logging.info(f'Preprocessing header {self.path.name}') + tmp_dir = self.submodule.submodule_file_path.parent / "tmp" + tmp_dir.mkdir(exist_ok=True) + tmp_file_path = tmp_dir / (self.path.name + '.in') + preprocessor_output_path = tmp_dir / (self.path.name) + tmp_file_content = [] + + # Includes that should be appended at the start of every file + forced_includes = [ + 'visp3/core/vpConfig.h', # Always include vpConfig: ensure that VISP macros are correctly defined + 'opencv2/opencv_modules.hpp' + ] + for include in forced_includes: + tmp_file_content.append(f'#include <{include}>\n') + + # Remove all includes: we only include configuration headers, defined above + with open(self.path.absolute(), 'r') as input_header_file: + include_regex = '#include\s*<(.*)>' + for line in input_header_file.readlines(): + matches = re.search(include_regex, line) + if matches is None: # Include line if its not an include + tmp_file_content.append(line) + # else: + # if 'visp3' in matches.group() or 'opencv' in matches.group(): + # tmp_file_content.append(line) + + with open(tmp_file_path.absolute(), 'w') as tmp_file: + tmp_file.write(''.join(tmp_file_content)) + tmp_file.flush() + + + argv = [''] + GeneratorConfig.pcpp_config.to_pcpp_args_list() + argv += ['-o', f'{preprocessor_output_path}', str(tmp_file_path.absolute())] + argv_str = ", ".join(argv) + logging.info(f'Preprocessor arguments:\n{argv_str}') + + pcpp.CmdPreprocessor(argv) + preprocessed_header_content = None + + # Remove all #defines that could have been left by the preprocessor + with open(preprocessor_output_path, 'r') as header_file: + preprocessed_header_lines = [] + for line in header_file.readlines(): + if not line.startswith('#define'): + preprocessed_header_lines.append(line) + preprocessed_header_content = ''.join(preprocessed_header_lines) + preprocessed_header_content = preprocessed_header_content.replace('#include<', '#include <') # Bug in cpp header parser + + return preprocessed_header_content + + def generate_binding_code(self, bindings_container: BindingsContainer) -> None: + assert self.header_repr is not None, 'The header was not preprocessed before calling the generation step!' + self.parse_data(bindings_container) + + def compute_environment(self): + ''' + Compute the header environment + This environment contains: + - The mapping from partially qualified names to fully qualified names + If a class inherits from another, the environment should be updated with what is contained in the base class environment. + This should be done in another step + ''' + self.environment = HeaderEnvironment(self.header_repr) + + def parse_data(self, bindings_container: BindingsContainer) -> None: + ''' + Update the bindings container passed in parameter with the bindings linked to this header file + ''' + from visp_python_bindgen.enum_binding import get_enum_bindings + # Fetch documentation if available + if self.documentation_holder_path is not None: + self.documentation_holder = DocumentationHolder(self.documentation_holder_path, self.environment.mapping) + else: + logging.warning(f'No documentation found for header {self.path}') + + for cls in self.header_repr.namespace.classes: + self.generate_class(bindings_container, cls, self.environment) + enum_bindings = get_enum_bindings(self.header_repr.namespace, self.environment.mapping, self.submodule, self) + for enum_binding in enum_bindings: + bindings_container.add_bindings(enum_binding) + + # Parse functions that are not linked to a class + self.parse_sub_namespace(bindings_container, self.header_repr.namespace) + + def parse_sub_namespace(self, bindings_container: BindingsContainer, ns: NamespaceScope, namespace_prefix = '', is_root=True) -> None: + ''' + Parse a subnamespace and all its subnamespaces. + In a namespace, only the functions are exported. + ''' + + if not is_root and ns.name == '': # Anonymous namespace, only visible in header, so we ignore it + return + + functions_with_configs, rejected_functions = get_bindable_functions_with_config(self.submodule, ns.functions, self.environment.mapping) + + # Log rejected functions + rejection_strs = [] + for rejected_function in rejected_functions: + self.submodule.report.add_non_generated_method(rejected_function) + if NotGeneratedReason.is_non_trivial_reason(rejected_function.rejection_reason): + rejection_strs.append(f'\t{rejected_function.signature} was rejected! Reason: {rejected_function.rejection_reason}') + if len(rejection_strs) > 0: + logging.warning(f'Rejected function in namespace: {ns.name}') + logging.warning('\n' + '\n'.join(rejection_strs)) + + bound_object = BoundObjectNames('submodule', self.submodule.name, namespace_prefix, namespace_prefix) + defs = [] + for function, function_config in functions_with_configs: + defs.append(define_method(function, function_config, False, {}, self, self.environment, bound_object)[0]) + + bindings_container.add_bindings(SingleObjectBindings(bound_object, None, defs, GenerationObjectType.Namespace)) + for sub_ns in ns.namespaces: + logging.info(f'Parsing subnamespace {namespace_prefix + sub_ns}') + self.parse_sub_namespace(bindings_container, ns.namespaces[sub_ns], namespace_prefix + sub_ns + '::', False) + + def generate_class(self, bindings_container: BindingsContainer, cls: ClassScope, header_env: HeaderEnvironment) -> SingleObjectBindings: + ''' + Generate the bindings for a single class: + This method will generate one Python class per template instanciation. + If the class has no template argument, then a single python class is generated + + If it is templated, the mapping (template argument types => Python class name) must be provided in the JSON config file + ''' + def generate_class_with_potiental_specialization(name_python: str, owner_specs: OrderedDict[str, str], cls_config: Dict) -> str: + ''' + Generate the bindings of a single class, handling a potential template specialization. + The handled information is: + - The inheritance of this class + - Its public fields that are not pointers + - Its constructors + - Most of its operators + - Its public methods + ''' + python_ident = f'py{name_python}' + name_cpp = get_typename(cls.class_decl.typename, owner_specs, header_env.mapping) + class_doc = None + + methods_dict: Dict[str, List[MethodBinding]] = {} + def add_to_method_dict(key: str, value: MethodBinding): + ''' + Add a method binding to the dictionary containing all the methods bindings of the class. + This dict is a mapping str => List[MethodBinding] + ''' + if key not in methods_dict: + methods_dict[key] = [value] + else: + methods_dict[key].append(value) + + def add_method_doc_to_pyargs(method: types.Method, py_arg_strs: List[str]) -> List[str]: + if self.documentation_holder is not None: + method_name = get_name(method.name) + method_doc_signature = MethodDocSignature(method_name, + get_type(method.return_type, {}, header_env.mapping) or '', # Don't use specializations so that we can match with doc + [get_type(param.type, {}, header_env.mapping) for param in method.parameters], + method.const, method.static) + method_doc = self.documentation_holder.get_documentation_for_method(name_cpp_no_template, method_doc_signature, {}, owner_specs, param_names, []) + if method_doc is None: + logging.warning(f'Could not find documentation for {name_cpp}::{method_name}!') + return py_arg_strs + else: + return [method_doc.documentation] + py_arg_strs + else: + return py_arg_strs + + if self.documentation_holder is not None: + class_doc = self.documentation_holder.get_documentation_for_class(name_cpp_no_template, {}, owner_specs) + else: + logging.warning(f'Documentation not found when looking up {name_cpp_no_template}') + + # Declaration + # Add template specializations to cpp class name. e.g., vpArray2D becomes vpArray2D if the template T is double + template_decl: Optional[types.TemplateDecl] = cls.class_decl.template + if template_decl is not None: + template_strs = [] + template_strs = map(lambda t: owner_specs[t.name], template_decl.params) + template_str = f'<{", ".join(template_strs)}>' + name_cpp += template_str + + # Reference public base classes when creating pybind class binding + base_class_strs = list(map(lambda base_class: get_typename(base_class.typename, owner_specs, header_env.mapping), + filter(lambda b: b.access == 'public', cls.class_decl.bases))) + class_template_str = ', '.join([name_cpp] + base_class_strs) + doc_param = [] if class_doc is None else [class_doc.documentation] + buffer_protocol_arg = ['py::buffer_protocol()'] if cls_config['use_buffer_protocol'] else [] + cls_argument_strs = ['submodule', f'"{name_python}"'] + doc_param + buffer_protocol_arg + + class_decl = f'\tpy::class_ {python_ident} = py::class_<{class_template_str}>({", ".join(cls_argument_strs)});' + + # Definitions + # Skip constructors for classes that have pure virtual methods since they cannot be instantiated + contains_pure_virtual_methods = False + for method in cls.methods: + if method.pure_virtual: + contains_pure_virtual_methods = True + break + + # User marked this class as virtual. + # This is required if no virtual method is declared in this class, + # but it does not implement pure virtual methods of a base class + contains_pure_virtual_methods = contains_pure_virtual_methods or cls_config['is_virtual'] + + # Find bindable methods + generated_methods: List[MethodData] = [] + bindable_methods_and_config, rejected_methods = get_bindable_methods_with_config(self.submodule, cls.methods, + name_cpp_no_template, owner_specs, header_env.mapping) + # Display rejected methods + rejection_strs = [] + for rejected_method in rejected_methods: + self.submodule.report.add_non_generated_method(rejected_method) + if NotGeneratedReason.is_non_trivial_reason(rejected_method.rejection_reason): + rejection_strs.append(f'\t{rejected_method.signature} was rejected! Reason: {rejected_method.rejection_reason}') + if len(rejection_strs) > 0: + logging.warning(f'Rejected method in class: {name_cpp}') + logging.warning('\n'.join(rejection_strs)) + + # Split between constructors and other methods + constructors, non_constructors = split_methods_with_config(bindable_methods_and_config, lambda m: m.constructor) + + # Split between "normal" methods and operators, which require a specific definition + cpp_operator_names = cpp_operator_list() + operators, basic_methods = split_methods_with_config(non_constructors, lambda m: get_name(m.name) in cpp_operator_names) + + # Constructors definitions + if not contains_pure_virtual_methods: + for method, method_config in constructors: + method_name = get_name(method.name) + params_strs = [get_type(param.type, owner_specs, header_env.mapping) for param in method.parameters] + py_arg_strs = get_py_args(method.parameters, owner_specs, header_env.mapping) + param_names = [param.name or 'arg' + str(i) for i, param in enumerate(method.parameters)] + + py_arg_strs = add_method_doc_to_pyargs(method, py_arg_strs) + + ctor_str = f'''{python_ident}.{define_constructor(params_strs, py_arg_strs)};''' + add_to_method_dict('__init__', MethodBinding(ctor_str, is_static=False, is_lambda=False, + is_operator=False, is_constructor=True)) + + # Operator definitions + binary_return_ops = supported_const_return_binary_op_map() + binary_in_place_ops = supported_in_place_binary_op_map() + unary_return_ops = supported_const_return_unary_op_map() + + for method, method_config in operators: + method_name = get_name(method.name) + method_is_const = method.const + params_strs = [get_type(param.type, owner_specs, header_env.mapping) for param in method.parameters] + return_type_str = get_type(method.return_type, owner_specs, header_env.mapping) + py_args = get_py_args(method.parameters, owner_specs, header_env.mapping) + py_args = py_args + ['py::is_operator()'] + param_names = [param.name or 'arg' + str(i) for i, param in enumerate(method.parameters)] + + py_args = add_method_doc_to_pyargs(method, py_args) + + if len(params_strs) > 1: + logging.info(f'Found operator {name_cpp}{method_name} with more than one parameter, skipping') + rejection = RejectedMethod(name_cpp, method, method_config, get_method_signature(method_name, return_type_str, params_strs), NotGeneratedReason.NotHandled) + self.submodule.report.add_non_generated_method(rejection) + continue + elif len(params_strs) < 1: + for cpp_op, python_op_name in unary_return_ops.items(): + if method_name == f'operator{cpp_op}': + operator_str = f''' +{python_ident}.def("__{python_op_name}__", []({"const" if method_is_const else ""} {name_cpp}& self) -> {return_type_str} {{ + return {cpp_op}self; +}}, {", ".join(py_args)});''' + add_to_method_dict(f'__{python_op_name}__', MethodBinding(operator_str, is_static=False, is_lambda=True, + is_operator=True, is_constructor=False)) + break + + logging.info(f'Found unary operator {name_cpp}::{method_name}, skipping') + continue + for cpp_op, python_op_name in binary_return_ops.items(): + if method_name == f'operator{cpp_op}': + operator_str = f''' +{python_ident}.def("__{python_op_name}__", []({"const" if method_is_const else ""} {name_cpp}& self, {params_strs[0]} o) -> {return_type_str} {{ + return (self {cpp_op} o); +}}, {", ".join(py_args)});''' + add_to_method_dict(f'__{python_op_name}__', MethodBinding(operator_str, is_static=False, is_lambda=True, + is_operator=True, is_constructor=False)) + break + for cpp_op, python_op_name in binary_in_place_ops.items(): + if method_name == f'operator{cpp_op}': + operator_str = f''' +{python_ident}.def("__{python_op_name}__", []({"const" if method_is_const else ""} {name_cpp}& self, {params_strs[0]} o) -> {return_type_str} {{ + self {cpp_op} o; + return self; +}}, {", ".join(py_args)});''' + add_to_method_dict(f'__{python_op_name}__', MethodBinding(operator_str, is_static=False, is_lambda=True, + is_operator=True, is_constructor=False)) + break + + # Define classical methods + class_def_names = BoundObjectNames(python_ident, name_python, name_cpp_no_template, name_cpp) + for method, method_config in basic_methods: + if method.template is not None and method_config.get('specializations') is not None: + method_template_names = [t.name for t in method.template.params] + specializations = method_config.get('specializations') + for method_spec in specializations: + new_specs = owner_specs.copy() + assert len(method_template_names) == len(method_spec) + method_spec_dict = OrderedDict(k for k in zip(method_template_names, method_spec)) + new_specs.update(method_spec_dict) + method_str, method_data = define_method(method, method_config, True, + new_specs, self, header_env, class_def_names) + add_to_method_dict(method_data.py_name, MethodBinding(method_str, is_static=method.static, + is_lambda=f'{name_cpp}::*' not in method_str, + is_operator=False, is_constructor=False, + method_data=method_data)) + generated_methods.append(method_data) + else: + method_str, method_data = define_method(method, method_config, True, + owner_specs, self, header_env, class_def_names) + add_to_method_dict(method_data.py_name, MethodBinding(method_str, is_static=method.static, + is_lambda=f'{name_cpp}::*' not in method_str, + is_operator=False, is_constructor=False, + method_data=method_data)) + generated_methods.append(method_data) + + # See https://github.com/pybind/pybind11/issues/974 + # Update with overloads that are shadowed by new overloads defined in this class + # For instance, declaring: + # class A { void foo(int); }; + # class B: public A { void foo(std::string& s); } + # Will result in the following code generating an error: + # from visp.core import B + # b = B() + # b.foo(0) # no overload known with int + base_bindings = list(filter(lambda b: b is not None, map(lambda s: bindings_container.find_bindings(s), base_class_strs))) + + # assert not any(map(lambda b: b is None, base_bindings)), f'Could not retrieve the bindings for a base class of {name_cpp}' + for base_binding_container in base_bindings: + base_defs = base_binding_container.definitions + if not isinstance(base_defs, ClassBindingDefinitions): + raise RuntimeError + base_methods_dict = base_defs.methods + for method_name in methods_dict.keys(): + if method_name == '__init__': # Do not bring constructors of the base class in this class defs as it makes no sense + continue + if method_name in base_methods_dict: + for parent_method_binding in base_methods_dict[method_name]: + methods_dict[method_name].append(parent_method_binding.get_definition_in_child_class(python_ident)) + + # Add to string representation + if not cls_config['ignore_repr']: + to_string_str = find_and_define_repr_str(cls, name_cpp, python_ident) + if len(to_string_str) > 0: + add_to_method_dict('__repr__', MethodBinding(to_string_str, is_static=False, is_lambda=True, is_operator=True, + is_constructor=False)) + + # Add call to user defined bindings function + # Binding function should be defined in the static part of the generator + # It should have the signature void fn(py::class_& cls); + # If it is for a templated class, it should also be templated in the same way (same order of parameters etc.) + if cls_config['additional_bindings'] is not None: + template_str = '' + if len(owner_specs.keys()) > 0: + template_types = owner_specs.values() + template_str = f'<{", ".join([template_type for template_type in template_types])}>' + add_to_method_dict('__additional_bindings', MethodBinding(f'{cls_config["additional_bindings"]}({python_ident});', + is_static=False, is_lambda=False, + is_operator=False, is_constructor=False)) + + # Check for potential error-generating definitions + error_generating_overloads = get_static_and_instance_overloads(generated_methods) + if len(error_generating_overloads) > 0: + logging.error(f'Overloads defined for instance and class, this will generate a pybind error') + logging.error(error_generating_overloads) + raise RuntimeError + + field_dict = {} + for field in cls.fields: + if field.name in cls_config['ignored_attributes']: + logging.info(f'Ignoring field in class/struct {name_cpp}: {field.name}') + continue + if field.access == 'public': + if is_unsupported_argument_type(field.type): + continue + + field_type = get_type(field.type, owner_specs, header_env.mapping) + logging.info(f'Found field in class/struct {name_cpp}: {field_type} {field.name}') + + field_name_python = field.name.lstrip('m_') + + def_str = 'def_' + def_str += 'readonly' if field.type.const else 'readwrite' + if field.static: + def_str += '_static' + + field_str = f'{python_ident}.{def_str}("{field_name_python}", &{name_cpp}::{field.name});' + field_dict[field_name_python] = field_str + + classs_binding_defs = ClassBindingDefinitions(field_dict, methods_dict) + bindings_container.add_bindings(SingleObjectBindings(class_def_names, class_decl, classs_binding_defs, GenerationObjectType.Class)) + + name_cpp_no_template = '::'.join([seg.name for seg in cls.class_decl.typename.segments]) + logging.info(f'Parsing class "{name_cpp_no_template}"') + + if self.submodule.class_should_be_ignored(name_cpp_no_template): + return '' + + cls_config = self.submodule.get_class_config(name_cpp_no_template) + + # Warning for potential double frees + acknowledged_pointer_fields = cls_config.get('acknowledge_pointer_or_ref_fields') or [] + refs_or_ptr_fields = list(filter(lambda tn: '&' in tn or '*' in tn, + map(lambda field: get_type(field.type, {}, header_env.mapping), + cls.fields))) + + # If some pointer or refs are not acknowledged as existing by user, emit a warning + if len(set(refs_or_ptr_fields).difference(set(acknowledged_pointer_fields))) > 0: + self.submodule.report.add_pointer_or_ref_holder(name_cpp_no_template, refs_or_ptr_fields) + + + if cls.class_decl.template is None: + name_python = name_cpp_no_template.replace('vp', '') + return generate_class_with_potiental_specialization(name_python, {}, cls_config) + else: + if cls_config is None or 'specializations' not in cls_config or len(cls_config['specializations']) == 0: + logging.warning(f'Could not find template specialization for class {name_cpp_no_template}: skipping!') + self.submodule.report.add_non_generated_class(name_cpp_no_template, cls_config, 'Skipped because there was no declared specializations') + else: + specs = cls_config['specializations'] + template_names = [t.name for t in cls.class_decl.template.params] + for spec in specs: + name_python = spec['python_name'] + args = spec['arguments'] + assert len(template_names) == len(args), f'Specializing {name_cpp_no_template}: Template arguments are {template_names} but found specialization {args} which has the wrong number of arguments' + spec_dict = OrderedDict(k for k in zip(template_names, args)) + generate_class_with_potiental_specialization(name_python, spec_dict, cls_config) diff --git a/modules/python/generator/visp_python_bindgen/header_utils.py b/modules/python/generator/visp_python_bindgen/header_utils.py new file mode 100644 index 0000000000..51ce4778d5 --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/header_utils.py @@ -0,0 +1,144 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List, Set, Dict, Union +import sys + +import logging + +from cxxheaderparser import types +from cxxheaderparser.simple import ParsedData, NamespaceScope, ClassScope + +from visp_python_bindgen.utils import * +from visp_python_bindgen.methods import * +from visp_python_bindgen.doc_parser import * + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from visp_python_bindgen.header import HeaderFile + +def sort_headers(headers: List['HeaderFile']) -> List['HeaderFile']: + ''' + Sort headers based on their dependencies on other classes. + If a class does not inherit from any other, then it will be placed at the start of the list. + If it has a dependency, then it will be placed after this dependency in the list. + This step is important to ensure that the code generation is performed in the correct order. + It is not possible to declare an inheriting class to pybind without first exposing the base class. + ''' + def add_level(result: List['HeaderFile'], remainder: List['HeaderFile'], dependencies: Set[str]): + if len(remainder) == 0: + return + + include_in_result_fn = None + if len(result) == 0: # First iteration, query all headers that have no dependencies + include_in_result_fn = lambda h: len(h.depends) == 0 + else: + # Some header define multiple classes, where one may rely on another. So we filter h.depends + include_in_result_fn = lambda h: all(map(lambda x: x in dependencies, filter(lambda s: s not in h.contains, h.depends))) + new_remainder = [] + new_dependencies = dependencies.copy() + for header_file in remainder: + has_dependency = include_in_result_fn(header_file) + if has_dependency: + new_dependencies = new_dependencies | set(header_file.contains) + result.append(header_file) + else: + new_remainder.append(header_file) + if new_remainder == remainder: + warning_msg = f''' + Warning: Could not completely solve dependencies, generating but might have some errors + Faulty headers: {[h.path.name for h in remainder]}''' + logging.warning(warning_msg) + print(warning_msg, file=sys.stderr) + result.extend(remainder) + else: + add_level(result, new_remainder, set(new_dependencies)) + result = [] + add_level(result, headers, set()) + return result + +class HeaderEnvironment(): + def __init__(self, data: ParsedData): + self.mapping: Dict[str, str] = self.build_naive_mapping(data.namespace, {}) + + # Step 2: resolve enumeration names that are possibly hidden behind typedefs + from visp_python_bindgen.enum_binding import resolve_enums_and_typedefs + enum_reprs, _ = resolve_enums_and_typedefs(data.namespace, self.mapping) + for enum_repr in enum_reprs: + for value in enum_repr.values: + self.mapping[value.name] = enum_repr.name + '::' + value.name + + def build_naive_mapping(self, data: Union[NamespaceScope, ClassScope], mapping, scope: str = ''): + if isinstance(data, NamespaceScope): + for alias in data.using_alias: + mapping[alias.alias] = get_type(alias.type, {}, mapping) + + for typedef in data.typedefs: + mapping[typedef.name] = scope + typedef.name + + for enum in data.enums: + if not name_is_anonymous(enum.typename): + enum_name = '::'.join([seg.name for seg in enum.typename.segments]) + mapping[enum_name] = scope + enum_name + + for cls in data.classes: + cls_name = '::'.join([seg.name for seg in cls.class_decl.typename.segments]) + mapping[cls_name] = scope + cls_name + mapping.update(self.build_naive_mapping(cls, mapping=mapping, scope=f'{scope}{cls_name}::')) + + for namespace in data.namespaces: + mapping.update(self.build_naive_mapping(data.namespaces[namespace], mapping=mapping, scope=f'{scope}{namespace}::')) + + elif isinstance(data, ClassScope): + for alias in data.using_alias: + mapping[alias.alias] = get_type(alias.type, {}, mapping) + + for typedef in data.typedefs: + mapping[typedef.name] = scope + typedef.name + + for enum in data.enums: + if not name_is_anonymous(enum.typename): + enum_name = '::'.join([seg.name for seg in enum.typename.segments]) + mapping[enum_name] = scope + enum_name + + for cls in data.classes: + cls_name = '::'.join([seg.name for seg in cls.class_decl.typename.segments if not isinstance(seg, types.AnonymousName)]) + mapping[cls_name] = scope + cls_name + mapping.update(self.build_naive_mapping(cls, mapping=mapping, scope=f'{scope}{cls_name}::')) + return mapping + + def update_with_dependencies(self, other_envs: List['HeaderEnvironment']) -> None: + for env in other_envs: + self.mapping.update(env) diff --git a/modules/python/generator/visp_python_bindgen/methods.py b/modules/python/generator/visp_python_bindgen/methods.py new file mode 100644 index 0000000000..a688e8766e --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/methods.py @@ -0,0 +1,468 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +import logging +from typing import Any, Callable, List, Optional, Tuple, Dict +from enum import Enum +from dataclasses import dataclass + +from cxxheaderparser import types +from cxxheaderparser.simple import ClassScope + +from visp_python_bindgen.doc_parser import MethodDocSignature +from visp_python_bindgen.utils import * +from visp_python_bindgen.generator_config import GeneratorConfig + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from visp_python_bindgen.submodule import Submodule + from visp_python_bindgen.header import HeaderFile, HeaderEnvironment, BoundObjectNames + +def cpp_operator_list(): + ''' + List of cpp methods that are considered operators. + Not all of them have a direct mapping to Python + ''' + symbols = [ + '-', '+', '*', '/', + '++', '--', '%', + '==', '=', '!=', + '>', '>=', '<', '<=', '<=>', + '>>', '<<', '^', + '~', '&', '|', + '&&', '||', '!', + '=', '+=', '-=', '*=', '/=', + '%=', '&=', '|=', '^=', + '<<=', '>>=', + '[]', '->', '->*', + '()', ',' + ] + return [f'operator{s}' for s in symbols] + ['operator'] + +def supported_const_return_binary_op_map(): + ''' + Mapping between CPP operator symbols and their python equivalence, for binary operators that return a value and do not modify self. + a binary operator takes as argument self and an another parameter. + ''' + return { + '==': 'eq', + '!=': 'ne', + '<': 'lt', + '>': 'gt', + '<=': 'le', + '>=': 'ge', + '+': 'add', + '-': 'sub', + '*': 'mul', + '/': 'truediv', + '%': 'mod', + '&': 'and', + '|': 'or', + '^': 'xor', + } + +def supported_in_place_binary_op_map(): + return { + '+=': 'iadd', + '*=': 'imul', + '-=': 'isub', + '/=': 'itruediv', + } + +def supported_const_return_unary_op_map(): + return { + '-': 'neg', + '~': 'invert', + } +def find_and_define_repr_str(cls: ClassScope, cls_name: str, python_ident: str) -> str: + for friend in cls.friends: + if friend.fn is not None: + is_ostream_operator = True + fn = friend.fn + if fn.return_type is None: + is_ostream_operator = False + else: + return_str = get_type(fn.return_type, {}, {}) + if return_str != 'std::ostream&': + is_ostream_operator = False + if not is_ostream_operator: + continue + if is_ostream_operator: + return f''' +{python_ident}.def("__repr__", []({cls_name} &a) {{ + std::stringstream s; + s << a; + return s.str(); +}});''' + return '' + +def ref_to_class_method(method: types.Method, cls_name: str, method_name: str, return_type: str, params: List[str]) -> str: + maybe_const = '' + if method.const: + maybe_const = 'const' + pointer_to_type = f'({cls_name}::*)' if not method.static else '(*)' + cast_str = f'{return_type} {pointer_to_type}({", ".join(params)}) {maybe_const}' + return f'static_cast<{cast_str}>(&{cls_name}::{method_name})' + +def ref_to_function(method_name: str, return_type: str, params: List[str]) -> str: + pointer_to_type = '(*)' + cast_str = f'{return_type} {pointer_to_type}({", ".join(params)})' + return f'static_cast<{cast_str}>(&{method_name})' + +def method_def(py_name: str, method: str, additional_args: List[str], static: bool) -> str: + def_type = 'def' if not static else 'def_static' + additional_args_str = ', '.join(additional_args) + if len(additional_args) > 0: + additional_args_str = ', ' + additional_args_str + return f'{def_type}("{py_name}", {method}{additional_args_str})' + +def tokens_to_str(tokens: List[types.Token]) -> str: + return ''.join([token.value for token in tokens]) + +def parameter_can_have_default_value(parameter: types.Parameter, specs, env_mapping) -> bool: + ''' + Return whether an argument can have a default value. + This is important! In python, default argument are instanciated only once (when the package is imported), while this is not the case in C++ + We need to be careful in what we allow + ''' + t = parameter.type + gt = lambda typename: get_typename(typename, specs, env_mapping) + is_const = False + if isinstance(t, types.Type): + type_name = gt(t.typename) + is_const = t.const + elif isinstance(t, types.Reference): + type_name = gt(t.ref_to.typename) + is_const = t.ref_to.const + elif isinstance(t, types.Pointer): + type_name = gt(t.ptr_to.typename) + is_const = t.ptr_to.const + else: + type_name = '' + if GeneratorConfig.is_forbidden_default_argument_type(type_name): + return False + + if is_const: # Parameter is const, so we can safely give a default value knowing it won't be modified + return True + if GeneratorConfig.is_immutable_type(type_name): # Immutable type on python side + return True + + return False + +def get_py_args(parameters: List[types.Parameter], specs, env_mapping) -> List[str]: + ''' + Get the py::arg parameters of a function binding definition. + They are used to give the argument their names in the doc and the api. + They can also have default values (optional arguments). + ''' + def make_arg(name: str) -> str: + return f'py::arg("{name}")' + py_args = [] + for parameter in parameters: + if parameter.default is None or not parameter_can_have_default_value(parameter, specs, env_mapping): + py_args.append(make_arg(parameter.name)) + else: + t = parameter.type + gt = lambda typename: get_typename(typename, specs, env_mapping) + if isinstance(t, types.Type): + type_name = gt(t.typename) + elif isinstance(t, types.Reference): + type_name = gt(t.ref_to.typename) + else: + type_name = '' + + if type_name.startswith('std::vector'): + default_value = type_name + '()' + default_value_rep = '[]' + elif type_name.startswith('std::optional'): + default_value = type_name + '{}' + default_value_rep = 'None' + else: + default_value = tokens_to_str(parameter.default.tokens) + if default_value in ['nullptr', 'NULL']: + full_typename = get_type(t, specs, env_mapping) + default_value = f'static_cast<{full_typename}>(nullptr)' + default_value_rep = 'None' + else: + default_value_rep = default_value.strip('"') # To handle default char* and raw std::string args + default_value_rep = default_value_rep.replace('"', '\"') # Escape inner quotes in std::string args like std::string("hello"). This would break parsing at compile time + default_value = env_mapping.get(default_value) or default_value + + py_args.append(f'py::arg_v("{parameter.name}", {default_value}, "{default_value_rep}")') + + return py_args + +def define_method(method: types.Method, method_config: Dict, is_class_method, specs: Dict, header: 'HeaderFile', header_env: 'HeaderEnvironment', bound_object: 'BoundObjectNames'): + params_strs = [get_type(param.type, specs, header_env.mapping) for param in method.parameters] + py_arg_strs = get_py_args(method.parameters, specs, header_env.mapping) + method_name = get_name(method.name) + py_method_name = method_config.get('custom_name') or method_name + return_type = get_type(method.return_type, specs, header_env.mapping) + + method_signature = get_method_signature(method_name, + get_type(method.return_type, {}, header_env.mapping), + [get_type(param.type, {}, header_env.mapping) for param in method.parameters]) + + # Detect input and output parameters for a method + use_default_param_policy = method_config['use_default_param_policy'] + param_is_input, param_is_output = method_config['param_is_input'], method_config['param_is_output'] + if use_default_param_policy or param_is_input is None and param_is_output is None: + param_is_input = [True for _ in range(len(method.parameters))] + param_is_output = list(map(lambda param: is_non_const_ref_to_immutable_type(param.type), method.parameters)) + if any(param_is_output): # Emit a warning when using default policy + header.submodule.report.add_default_policy_method(bound_object.cpp_no_template_name, method, method_signature, param_is_input, param_is_output) + + # Pybind arguments + # Only use py::arg for input values (error otherwise) + py_arg_strs = [py_arg_strs[i] for i in range(len(params_strs)) if param_is_input[i]] + + pybind_options = py_arg_strs + # Custom return policy + return_policy = method_config.get('return_policy') + if return_policy is not None: + pybind_options.append(f'py::return_value_policy::{return_policy}') + + # Keep alive values: avoid memory leaks + keep_alives = method_config.get('keep_alive') + keep_alive_strs = [] + if keep_alives is not None: + def make_keep_alive_str(values) -> str: + assert len(values) == 2 and isinstance(values[0], int) and isinstance(values[1], int), 'Tried to make keep alive with incorrect values' + return f'py::keep_alive<{values[0]}, {values[1]}>()' + if not isinstance(keep_alives, list) or len(keep_alives) == 0: + raise RuntimeError(f'Keep alive value should be a list of int or a list of list of ints (multiple args kept alive), but got {keep_alives} for method {method_signature}') + if isinstance(keep_alives[0], int): + keep_alive_strs.append(make_keep_alive_str(keep_alives)) + else: + for keep_alive in keep_alives: + keep_alive_strs.append(make_keep_alive_str(keep_alive)) + pybind_options.extend(keep_alive_strs) + + # Get parameter names + param_names = [param.name or 'arg' + str(i) for i, param in enumerate(method.parameters)] + input_param_names = [param_names[i] for i in range(len(param_is_input)) if param_is_input[i]] + output_param_names = [param_names[i] for i in range(len(param_is_output)) if param_is_output[i]] + output_param_is_ref = [isinstance(method.parameters[i], types.Reference) for i in range(len(params_strs)) if param_is_output[i]] + + # Fetch documentation if available + if header.documentation_holder is not None: + if is_class_method: + method_doc_signature = MethodDocSignature(method_name, + get_type(method.return_type, {}, header_env.mapping), # Don't use specializations so that we can match with doc + [get_type(param.type, {}, header_env.mapping) for param in method.parameters], + method.const, method.static) + else: + method_doc_signature = MethodDocSignature(method_name, + get_type(method.return_type, {}, header_env.mapping), # Don't use specializations so that we can match with doc + [get_type(param.type, {}, header_env.mapping) for param in method.parameters], + True, True) + method_doc = header.documentation_holder.get_documentation_for_method(bound_object.cpp_no_template_name, method_doc_signature, {}, specs, input_param_names, output_param_names) + if method_doc is None: + logging.warning(f'Could not find documentation for {bound_object.cpp_name}::{method_name}!') + else: + pybind_options = [method_doc.documentation] + pybind_options + + + + # If a function has refs to immutable params, we need to return them. + # Also true if user has specified input cpp params as output python params + should_wrap_for_tuple_return = param_is_output is not None and any(param_is_output) + + # Emit a warning when returnin a ref to an object: + # this can probably lead to memory leaks or unwanted object copies (and thus undesired behaviour down the line) + if not should_wrap_for_tuple_return and '&' in return_type and not (method_config.get('returns_ref_ok') or False): + header.submodule.report.add_method_returning_ref(bound_object.cpp_no_template_name, method, method_signature) + + # Arguments that are inputs to the lambda function that wraps the ViSP function + input_param_types = [params_strs[i] for i in range(len(param_is_input)) if param_is_input[i]] + params_with_names = [t + ' ' + name for t, name in zip(input_param_types, input_param_names)] + + # Params that are only outputs: they should be declared in function. Assume that they are default constructible + param_is_only_output = [not is_input and is_output for is_input, is_output in zip(param_is_input, param_is_output)] + param_declarations = [f'{get_type_for_declaration(method.parameters[i].type, specs, header_env.mapping)} {param_names[i]};' for i in range(len(param_is_only_output)) if param_is_only_output[i]] + param_declarations = '\n'.join(param_declarations) + + if is_class_method and not method.static: + self_param_with_name = bound_object.cpp_name + '& self' + method_caller = 'self.' + else: + self_param_with_name = None + method_caller = bound_object.cpp_name + '::' if is_class_method else bound_object.cpp_name + output_param_symbols = [] + if return_type is None or return_type == 'void': + maybe_get_return = '' + else: + maybe_get_return = f'{return_type} res = ' + if '&' in return_type: + output_param_symbols.append('&res') + else: + output_param_symbols.append('res') + + if len(output_param_names) == 0 and (return_type is None or return_type == 'void'): + return_str = '' + elif len(output_param_names) == 0: + return_str = 'res' + elif len(output_param_names) == 1 and (return_type is None or return_type == 'void'): + return_str = output_param_names[0] + else: + # When returning a tuple we need to explicitly convert references to pointer. + # This is required since std::tuple will upcast the ref to its base class and try to store a copy of the object + # If a class is pure virtual, this is not possible and will raise a compilation error! + output_param_symbols.extend(['&' + name if is_ref else name for is_ref, name in zip(output_param_is_ref, output_param_names)]) + return_str = f'std::make_tuple({", ".join(output_param_symbols)})' + + lambda_body = f''' + {param_declarations} + {maybe_get_return}{method_caller}{method_name}({", ".join(param_names)}); + return {return_str}; + ''' + final_lambda_params = [self_param_with_name] + params_with_names if self_param_with_name is not None else params_with_names + lambda_variant = define_lambda('', final_lambda_params, None, lambda_body) + + if should_wrap_for_tuple_return: + method_body_str = lambda_variant + elif is_class_method: + method_body_str = ref_to_class_method(method, bound_object.cpp_name, method_name, return_type, params_strs) + else: + method_body_str = ref_to_function(bound_object.cpp_name + method_name, return_type, params_strs) + + method_str = method_def(py_method_name, method_body_str, pybind_options, method.static if is_class_method else False) + method_str = f'{bound_object.python_ident}.{method_str};' + return method_str, MethodData(py_method_name, method, lambda_variant, pybind_options) + +def define_constructor(params: List[str], additional_args: List[str]) -> str: + additional_args_str = ', '.join(additional_args) + if len(additional_args) > 0: + additional_args_str = ', ' + additional_args_str + return f'def(py::init<{", ".join(params)}>(){additional_args_str})' + +def define_lambda(capture: str, params: List[str], return_type: Optional[str], body: str) -> str: + return_str = f'-> {return_type}' if return_type else '' + return f''' +[{capture}]({", ".join(params)}) {return_str} {{ + {body} +}} + +''' +class NotGeneratedReason(Enum): + UserIgnored = 'user_ignored', + Access = 'access', + Destructor = 'destructor', + ReturnType = 'return_type' + ArgumentType = 'argument_type' + PureVirtual = 'pure_virtual' + UnspecifiedTemplateSpecialization = 'missing_template' + NotHandled = 'not_handled' + + @staticmethod + def is_non_trivial_reason(reason: 'NotGeneratedReason') -> bool: + return reason in [NotGeneratedReason.ArgumentType, + NotGeneratedReason.ReturnType, + NotGeneratedReason.UnspecifiedTemplateSpecialization, + NotGeneratedReason.NotHandled] + +@dataclass +class RejectedMethod: + cls_name: Optional[str] + method: types.Method + method_config: Dict[str, Any] + signature: str + rejection_reason: NotGeneratedReason + +def get_bindable_methods_with_config(submodule: 'Submodule', methods: List[types.Method], cls_name: str, specializations, mapping) -> Tuple[List[Tuple[types.Method, Dict]], List[RejectedMethod]]: + bindable_methods = [] + rejected_methods = [] + # Order of predicates is important: The first predicate that matches will be the one shown in the log, and they do not all have the same importance + filtering_predicates_and_motives = [ + (lambda _, conf: conf['ignore'], NotGeneratedReason.UserIgnored), + (lambda m, _: m.pure_virtual, NotGeneratedReason.PureVirtual), + (lambda m, _: m.access is None or m.access != 'public', NotGeneratedReason.Access), + (lambda m, _: m.destructor, NotGeneratedReason.Destructor), + (lambda m, conf: m.template is not None and (conf.get('specializations') is None or len(conf['specializations']) == 0), NotGeneratedReason.UnspecifiedTemplateSpecialization), + (lambda m, _: any(is_unsupported_argument_type(param.type) for param in m.parameters), NotGeneratedReason.ArgumentType), + (lambda m, _: not m.constructor and is_unsupported_return_type(m.return_type), NotGeneratedReason.ReturnType) + ] + for method in methods: + method_config = submodule.get_method_config(cls_name, method, specializations, mapping) + method_can_be_bound = True + for predicate, motive in filtering_predicates_and_motives: + if predicate(method, method_config): + return_str = '' if method.return_type is None else (get_type(method.return_type, specializations, mapping) or '') + method_name = '::'.join(seg.name for seg in method.name.segments) + param_strs = [get_type(param.type, specializations, mapping) or '' for param in method.parameters] + rejected_methods.append(RejectedMethod(cls_name, method, method_config, get_method_signature(method_name, return_str, param_strs), motive)) + method_can_be_bound = False + break + if method_can_be_bound: + bindable_methods.append((method, method_config)) + + return bindable_methods, rejected_methods + +def get_bindable_functions_with_config(submodule: 'Submodule', functions: List[types.Function], mapping) -> Tuple[List[Tuple[types.Function, Dict]], List[RejectedMethod]]: + bindable_functions = [] + rejected_functions = [] + # Order of predicates is important: The first predicate that matches will be the one shown in the log, and they do not all have the same importance + filtering_predicates_and_motives = [ + (lambda _, conf: conf['ignore'], NotGeneratedReason.UserIgnored), + (lambda m, _: GeneratorConfig.is_forbidden_function_name(get_name(m.name)), NotGeneratedReason.UserIgnored), + (lambda m, conf: m.template is not None and (conf.get('specializations') is None or len(conf['specializations']) == 0), NotGeneratedReason.UnspecifiedTemplateSpecialization), + (lambda m, _: any(is_unsupported_argument_type(param.type) for param in m.parameters), NotGeneratedReason.ArgumentType), + (lambda m, _: is_unsupported_return_type(m.return_type), NotGeneratedReason.ReturnType) + ] + for function in functions: + function_config = submodule.get_method_config(None, function, {}, mapping) + method_can_be_bound = True + for predicate, motive in filtering_predicates_and_motives: + if predicate(function, function_config): # Function should be rejected + return_str = '' if function.return_type is None else (get_type(function.return_type, {}, mapping) or '') + method_name = get_name(function.name) + param_strs = [get_type(param.type, {}, mapping) or '' for param in function.parameters] + rejected_functions.append(RejectedMethod('', function, function_config, get_method_signature(method_name, return_str, param_strs), motive)) + method_can_be_bound = False + break + if method_can_be_bound: + bindable_functions.append((function, function_config)) + + return bindable_functions, rejected_functions + +def split_methods_with_config(methods: List[Tuple[types.Method, Dict]], predicate: Callable[[types.Method], bool]) -> Tuple[List[Tuple[types.Method, Dict]], List[Tuple[types.Method, Dict]]]: + matching = [] + non_matching = [] + for method, method_config in methods: + if predicate(method): + matching.append((method, method_config)) + else: + non_matching.append((method, method_config)) + return matching, non_matching diff --git a/modules/python/generator/visp_python_bindgen/preprocessor.py b/modules/python/generator/visp_python_bindgen/preprocessor.py new file mode 100644 index 0000000000..392913f187 --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/preprocessor.py @@ -0,0 +1,201 @@ + +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +# ''' +# Preprocessor, derived from the command line preprocesor provided at https://github.com/ned14/pcpp/blob/master/pcpp/pcmd.py + +# ''' +# from __future__ import generators, print_function, absolute_import, division + +# import sys, argparse, traceback, os, copy, io, re +# from pcpp.preprocessor import Preprocessor, OutputDirective, Action +# from visp_python_bindgen.generator_config import PreprocessorConfig + +# class CmdPreprocessor(Preprocessor): +# def __init__(self, config: PreprocessorConfig, input: str): +# if len(argv) < 2: +# argv = [argv[0], '--help'] +# argp = argparse.ArgumentParser(prog='pcpp', +# description= +# '''A pure universal Python C (pre-)preprocessor implementation very useful for +# pre-preprocessing header only C++ libraries into single file includes and +# other such build or packaging stage malarky.''', +# epilog= +# '''Note that so pcpp can stand in for other preprocessor tooling, it +# ignores any arguments it does not understand.''') +# argp.add_argument('-o', dest = 'output', metavar = 'path', type = argparse.FileType('wt'), default=sys.stdout, nargs = '?', help = 'Output to a file instead of stdout') +# argp.add_argument('-D', dest = 'defines', metavar = 'macro[=val]', nargs = 1, action = 'append', help = 'Predefine name as a macro [with value]') +# argp.add_argument('-U', dest = 'undefines', metavar = 'macro', nargs = 1, action = 'append', help = 'Pre-undefine name as a macro') +# argp.add_argument('-N', dest = 'nevers', metavar = 'macro', nargs = 1, action = 'append', help = 'Never define name as a macro, even if defined during the preprocessing.') +# argp.add_argument('-I', dest = 'includes', metavar = 'path', nargs = 1, action = 'append', help = "Path to search for unfound #include's") +# #argp.add_argument('--passthru', dest = 'passthru', action = 'store_true', help = 'Pass through everything unexecuted except for #include and include guards (which need to be the first thing in an include file') +# argp.add_argument('--passthru-defines', dest = 'passthru_defines', action = 'store_true', help = 'Pass through but still execute #defines and #undefs if not always removed by preprocessor logic') +# argp.add_argument('--passthru-unfound-includes', dest = 'passthru_unfound_includes', action = 'store_true', help = 'Pass through #includes not found without execution') +# argp.add_argument('--passthru-unknown-exprs', dest = 'passthru_undefined_exprs', action = 'store_true', help = 'Unknown macros in expressions cause preprocessor logic to be passed through instead of executed by treating unknown macros as 0L') +# argp.add_argument('--passthru-comments', dest = 'passthru_comments', action = 'store_true', help = 'Pass through comments unmodified') +# argp.add_argument('--passthru-magic-macros', dest = 'passthru_magic_macros', action = 'store_true', help = 'Pass through double underscore magic macros unmodified') +# argp.add_argument('--passthru-includes', dest = 'passthru_includes', metavar = '', default = None, nargs = 1, help = "Regular expression for which #includes to not expand. #includes, if found, are always executed") +# argp.add_argument('--line-directive', dest = 'line_directive', metavar = 'form', default = '#line', nargs = '?', help = "Form of line directive to use, defaults to #line, specify nothing to disable output of line directives") +# args = argp.parse_known_args(argv[1:]) +# #print(args) +# for arg in args[1]: +# print("NOTE: Argument %s not known, ignoring!" % arg, file = sys.stderr) + +# self.args = args[0] +# super(CmdPreprocessor, self).__init__() + +# # Override Preprocessor instance variables +# self.define("__PCPP_ALWAYS_FALSE__ 0") +# self.define("__PCPP_ALWAYS_TRUE__ 1") + +# self.auto_pragma_once_enabled = True +# self.line_directive = config.line_directive +# if self.line_directive is not None and self.line_directive.lower() in ('nothing', 'none', ''): +# self.line_directive = None +# self.passthru_includes = re.compile(config.passthrough_includes_regex) +# self.compress = 0 +# # Pass through magic macros +# if False: +# self.undef('__DATE__') +# self.undef('__TIME__') +# self.expand_linemacro = False +# self.expand_filemacro = False +# self.expand_countermacro = False + +# # My own instance variables +# self.bypass_ifpassthru = False +# self.potential_include_guard = None + +# for d in config.defines: +# if '=' not in d: +# d += '=1' +# d = d.replace('=', ' ', 1) +# self.define(d) +# # for d in config.undefines: +# # self.undef(d) +# self.nevers = config.never_defined +# if self.args.nevers: +# self.args.nevers = [x[0] for x in self.args.nevers] + +# for include in config.include_directories: +# self.add_path(include) + +# try: +# if len(self.args.inputs) == 1: +# self.parse(self.args.inputs[0]) +# else: +# input = '' +# for i in self.args.inputs: +# input += '#include "' + i.name + '"\n' +# self.parse(input) +# self.write(self.args.output) +# except: +# print(traceback.print_exc(10), file = sys.stderr) +# print("\nINTERNAL PREPROCESSOR ERROR AT AROUND %s:%d, FATALLY EXITING NOW\n" +# % (self.lastdirective.source, self.lastdirective.lineno), file = sys.stderr) +# sys.exit(-99) +# finally: +# for i in self.args.inputs: +# i.close() +# if self.args.output != sys.stdout: +# self.args.output.close() + +# def on_include_not_found(self,is_malformed,is_system_include,curdir,includepath): +# if self.args.passthru_unfound_includes: +# raise OutputDirective(Action.IgnoreAndPassThrough) +# return super(CmdPreprocessor, self).on_include_not_found(is_malformed,is_system_include,curdir,includepath) + +# def on_unknown_macro_in_defined_expr(self,tok): +# if self.args.undefines: +# if tok.value in self.args.undefines: +# return False +# if self.args.passthru_undefined_exprs: +# return None # Pass through as expanded as possible +# return super(CmdPreprocessor, self).on_unknown_macro_in_defined_expr(tok) + +# def on_unknown_macro_in_expr(self,ident): +# if self.args.undefines: +# if ident in self.args.undefines: +# return super(CmdPreprocessor, self).on_unknown_macro_in_expr(ident) +# if self.args.passthru_undefined_exprs: +# return None # Pass through as expanded as possible +# return super(CmdPreprocessor, self).on_unknown_macro_in_expr(ident) + +# def on_unknown_macro_function_in_expr(self,ident): +# if self.args.undefines: +# if ident in self.args.undefines: +# return super(CmdPreprocessor, self).on_unknown_macro_function_in_expr(ident) +# if self.args.passthru_undefined_exprs: +# return None # Pass through as expanded as possible +# return super(CmdPreprocessor, self).on_unknown_macro_function_in_expr(ident) + +# def on_directive_handle(self,directive,toks,ifpassthru,precedingtoks): +# if ifpassthru: +# if directive.value == 'if' or directive.value == 'elif' or directive == 'else' or directive.value == 'endif': +# self.bypass_ifpassthru = len([tok for tok in toks if tok.value == '__PCPP_ALWAYS_FALSE__' or tok.value == '__PCPP_ALWAYS_TRUE__']) > 0 +# if not self.bypass_ifpassthru and (directive.value == 'define' or directive.value == 'undef'): +# if toks[0].value != self.potential_include_guard: +# raise OutputDirective(Action.IgnoreAndPassThrough) # Don't execute anything with effects when inside an #if expr with undefined macro +# if (directive.value == 'define' or directive.value == 'undef') and self.args.nevers: +# if toks[0].value in self.args.nevers: +# raise OutputDirective(Action.IgnoreAndPassThrough) +# if self.args.passthru_defines: +# super(CmdPreprocessor, self).on_directive_handle(directive,toks,ifpassthru,precedingtoks) +# return None # Pass through where possible +# return super(CmdPreprocessor, self).on_directive_handle(directive,toks,ifpassthru,precedingtoks) + +# def on_directive_unknown(self,directive,toks,ifpassthru,precedingtoks): +# if ifpassthru: +# return None # Pass through +# return super(CmdPreprocessor, self).on_directive_unknown(directive,toks,ifpassthru,precedingtoks) + +# def on_potential_include_guard(self,macro): +# self.potential_include_guard = macro +# return super(CmdPreprocessor, self).on_potential_include_guard(macro) + +# def on_comment(self,tok): +# if self.args.passthru_comments: +# return True # Pass through +# return super(CmdPreprocessor, self).on_comment(tok) + +# def main(argv=None): +# if argv is None: +# argv = sys.argv +# p = CmdPreprocessor(argv) +# return p.return_code + +# if __name__ == "__main__": +# sys.exit(main(sys.argv)) diff --git a/modules/python/generator/visp_python_bindgen/submodule.py b/modules/python/generator/visp_python_bindgen/submodule.py new file mode 100644 index 0000000000..05bcb42dbd --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/submodule.py @@ -0,0 +1,268 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List, Optional, Dict +from pathlib import Path +import json + +from visp_python_bindgen.header import HeaderFile +from visp_python_bindgen.utils import * +from visp_python_bindgen.gen_report import Report + +class Submodule(): + def __init__(self, name: str, include_path: Path, config_file_path: Path, submodule_file_path: Path): + self.name = name + self.include_path = include_path + self.submodule_file_path = submodule_file_path + self.config_path = config_file_path / (name + '.json') + self.config = self._get_config_file_or_create_default(self.config_path) + self.report = Report(self) + self.headers = self._get_headers() + assert self.include_path.exists(), f'Submodule path {self.include_path} not found' + + def set_dependencies_from_dict(self, dict_modules: Dict[str, 'Submodule'], dep_names: List[str]): + deps = [] + for dep_name in dep_names: + if dep_name in dict_modules: + deps.append(dict_modules[dep_name]) + self.dependencies = deps + def _get_headers(self) -> List[HeaderFile]: + headers = [] + for include_file in self.include_path.iterdir(): + if not include_file.name.endswith('.h') and not include_file.name.endswith('.hpp'): + continue + if self.header_should_be_ignored(include_file.name): + self.report.add_ignored_header(include_file) + continue + headers.append(HeaderFile(include_file, self)) + return headers + def _get_config_file_or_create_default(self, path: Path) -> Dict: + if not path.exists(): + default_config = { + 'ignored_headers': [], + 'ignored_classes': [], + 'user_defined_headers': [], + 'classes': {}, + 'enums': {} + } + with open(path, 'w') as config_file: + json.dump(default_config, config_file) + return default_config + else: + with open(path, 'r') as config_file: + config = json.load(config_file) + return config + + def set_headers_from_common_list(self, all_headers: List[HeaderFile]) -> None: + ''' + Set the submodule's headers from a list containing headers from multiple modules + ''' + new_headers = [] + for header in all_headers: + if header.submodule.name == self.name: + new_headers.append(header) + header.submodule = self + self.headers = new_headers + + def generate(self) -> None: + + # Sort by dependency level so that generation is in correct order + module_bindings = BindingsContainer() + includes = [] + for header in self.headers: + header.generate_binding_code(module_bindings) + includes.extend(header.includes) + submodule_declaration = f'py::module_ submodule = m.def_submodule("{self.name}");\n' + bindings = module_bindings.get_definitions() + declarations = module_bindings.get_declarations() + user_defined_headers = '\n'.join(self.get_user_defined_headers()) + + includes_set = set(includes) + includes_strs = [f'#include {include}' for include in includes_set] + includes_str = '\n'.join(includes_strs) + additional_required_headers = '\n'.join(self.get_required_headers()) + + format_str = f''' +//#define PYBIND11_DETAILED_ERROR_MESSAGES +#include +#include +#include +#include + +/*User-defined headers (e.g. additional bindings)*/ +{user_defined_headers} +/*Required headers that are not retrieved in submodule headers (e.g. there are forward definitions but no includes) */ +{additional_required_headers} +/*Submodule headers*/ +{includes_str} + +namespace py = pybind11; + +void {self.generation_function_name()}(py::module_ &m) {{ +py::options options; +options.disable_enum_members_docstring(); + +/* + * Submodule declaration + */ +{submodule_declaration} + +/* + * Class and enums declarations + */ +{declarations} + +/* + * Bindings for methods and enum values + */ +{bindings} +}} +''' + with open(self.submodule_file_path, 'w') as submodule_file: + submodule_file.write(format_str) + + logs_path = self.submodule_file_path.parent.parent / 'logs' + logs_path.mkdir(exist_ok=True) + self.report.write(logs_path / f'{self.name}_log.json') + + def generation_function_name(self) -> str: + return f'init_submodule_{self.name}' + + def class_should_be_ignored(self, class_name: str) -> bool: + if 'ignored_classes' not in self.config: + return False + return class_name in self.config['ignored_classes'] + def header_should_be_ignored(self, header_name: str) -> bool: + if 'ignored_headers' not in self.config: + return False + return header_name in self.config['ignored_headers'] + + def get_user_defined_headers(self) -> List[str]: + if 'user_defined_headers' in self.config: + header_names = self.config['user_defined_headers'] + return [f'#include "{header_name}"' for header_name in header_names] + return [] + + def get_required_headers(self) -> List[str]: + if 'required_headers' in self.config: + header_names = self.config['required_headers'] + return [f'#include <{header_name}>' for header_name in header_names] + return [] + + def get_class_config(self, class_name: str) -> Optional[Dict]: + default_config = { + 'methods': [], + 'operators': [], + 'ignored_attributes': [], + 'use_buffer_protocol': False, + 'additional_bindings': None, + 'ignore_repr': False, + 'is_virtual': False + } + if 'classes' not in self.config: + return default_config + if class_name not in self.config['classes']: + return default_config + default_config.update(self.config['classes'][class_name]) + return default_config + def get_enum_config(self, enum_name: str) -> Optional[Dict]: + default_config = { + 'ignore': False, + } + if 'enums' not in self.config: + return default_config + if enum_name not in self.config['enums']: + return default_config + default_config.update(self.config['enums'][enum_name]) + return default_config + + def get_method_config(self, class_name: Optional[str], method, owner_specs, header_mapping) -> Dict: + res = { + 'ignore': False, + 'use_default_param_policy': False, # Handling + 'param_is_input': None, + 'param_is_output': None, + 'custom_name': None, + 'custom_code': None, + 'keep_alive': None, + 'return_policy': None, + 'returns_ref_ok': False, + } + functions_container = None + keys = ['classes', class_name, 'methods'] if class_name is not None else ['functions'] + tmp = self.config + for k in keys: + if k not in tmp: + return res + tmp = tmp[k] + functions_container = tmp + for function_config in functions_container: + if method_matches_config(method, function_config, owner_specs, header_mapping): + res.update(function_config) + return res + + #import sys; sys.exit() + return res + +def get_submodules(config_path: Path, generate_path: Path) -> List[Submodule]: + modules_input_data = GeneratorConfig.module_data + result: Dict[str, Submodule] = {} + for module_data in modules_input_data: + headers = module_data.headers + if len(headers) == 0: + print(f'Module {module_data.name} has no input headers, skipping!') + + continue + include_dir = headers[0].parent + hh = "\n".join(map(lambda s: str(s), headers)) + assert all(map(lambda header_path: header_path.parent == include_dir, headers)), f'Found headers in different directory, this case is not yet handled. Headers = {hh}' + submodule = Submodule(module_data.name, include_dir, config_path, generate_path / f'{module_data.name}.cpp') + result[module_data.name] = submodule + + # Second pass to link dependencies + for module_data in modules_input_data: + if module_data.name in result: + result[module_data.name].set_dependencies_from_dict(result, module_data.dependencies) + return sort_submodules(list(result.values())) + +def sort_submodules(submodules: List[Submodule]) -> List[Submodule]: + res = [] + submodules_tmp = submodules.copy() + while len(res) < len(submodules): + can_add = lambda submodule: all(map(lambda dep: dep in res, submodule.dependencies)) + res_round = list(filter(can_add, submodules_tmp)) + submodules_tmp = [submodule for submodule in submodules_tmp if submodule not in res_round] + res += res_round + return res diff --git a/modules/python/generator/visp_python_bindgen/utils.py b/modules/python/generator/visp_python_bindgen/utils.py new file mode 100644 index 0000000000..a8c9a79302 --- /dev/null +++ b/modules/python/generator/visp_python_bindgen/utils.py @@ -0,0 +1,396 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings generator +# +############################################################################# + +from typing import List, Optional, Set, Tuple, Dict, Union +import copy +from enum import Enum +from dataclasses import dataclass + +from cxxheaderparser.tokfmt import tokfmt +from cxxheaderparser import types +from cxxheaderparser.simple import NamespaceScope, ClassScope + +from visp_python_bindgen.generator_config import GeneratorConfig + +@dataclass +class MethodData: + py_name: str + method: types.Method + lambda_variant: str + py_arg_strs: List[str] + + def as_lambda(self, py_ident: str) -> str: + args_str = ', '.join(self.py_arg_strs) + def_val = 'def' if not self.method.static else 'def_static' + maybe_comma = ',' if len(args_str.strip()) > 0 else '' + return f''' + {py_ident}.{def_val}("{self.py_name}", {self.lambda_variant}{maybe_comma} {args_str}); + ''' + +@dataclass +class BoundObjectNames: + ''' + The different names that link to a cpp class + ''' + python_ident: str # the identifier (variable) that defines the pybind object + python_name: str # the name exposed in Python + cpp_no_template_name: str # C++ name without any template => vpArray2D becomes vpArray2D (useful for dependencies) + cpp_name: str # C++ name with templates + +class GenerationObjectType(Enum): + Enum = 'enum' + Class = 'class' + Function = 'function' + Namespace = 'namespace' + +@dataclass +class MethodBinding: + binding: str + is_static: bool + is_lambda: bool + is_operator: bool + is_constructor: bool + method_data: Optional[MethodData] = None + + def get_definition_in_child_class(self, child_py_ident) -> 'MethodBinding': + if self.is_constructor: + raise RuntimeError('We should not try to redefine a constructor for a child class') + if self.is_lambda: + return copy.deepcopy(self) + new_binding_code = self.method_data.as_lambda(child_py_ident) + res = copy.deepcopy(self) + res.binding = new_binding_code + return res + +@dataclass +class ClassBindingDefinitions: + ''' + Class containing the bindings for a single class + ''' + fields: Dict[str, str] + methods: Dict[str, List[MethodBinding]] # Mapping from python method name to the bindings definitions. There can be overloads + +@dataclass +class SingleObjectBindings: + ''' + Result of running the binding generator for a single type (class, enum) + ''' + object_names: BoundObjectNames + declaration: Optional[str] # Pybind type instanciation, eg py::class_<> ... (if any) + definitions: Union[List[str], ClassBindingDefinitions] # List of method bindings, attributes etc for this pybind object + object_type: GenerationObjectType # Type of the object + +class BindingsContainer: + def __init__(self): + self.object_bindings: List[SingleObjectBindings] = [] + + def add_bindings(self, other: SingleObjectBindings) -> None: + self.object_bindings.append(other) + + def find_bindings(self, cpp_name: str) -> Optional[SingleObjectBindings]: + for sob in self.object_bindings: + if sob.object_names.cpp_name == cpp_name: + return sob + return None + + def get_declarations(self) -> str: + decls = [] + for sob in self.object_bindings: + if sob.declaration is not None: + decls.append(sob.declaration) + return '\n'.join(decls) + + def get_definitions(self) -> str: + defs = [] + for sob in self.object_bindings: + odefs = sob.definitions + if isinstance(odefs, list): + defs.extend(odefs) + elif isinstance(odefs, ClassBindingDefinitions): + defs.extend(list(odefs.fields.values())) + for method_overloads in odefs.methods.values(): + for method_overload_binding in method_overloads: + defs.append(method_overload_binding.binding) + return '\n'.join(defs) + +def get_name(name: types.PQName) -> str: + ''' + Get the fully qualified name of a type. + Template specializations will not appear! + ''' + return '::'.join([segment.name for segment in name.segments]) + +def name_is_anonymous(name: types.PQName) -> bool: + return any(isinstance(s, types.AnonymousName) for s in name.segments) + +def get_typename(typename: types.PQName, owner_specs, header_env_mapping) -> str: + '''Resolve the string representation of a raw type, resolving template specializations and using complete typenames + (aliases, shortened names when in same namescope). + This does not include constness, whether it is a pointer or a ref etc. + ''' + def segment_repr(segment: types.PQNameSegment) -> str: + if isinstance(segment, types.FundamentalSpecifier): + return segment.name + if isinstance(segment, types.AutoSpecifier): + return segment.name + + segment_name = segment.name + if segment.name in owner_specs: + segment_name = owner_specs[segment.name] + if segment.name in header_env_mapping: + segment_name = header_env_mapping[segment.name] + + spec_str = '' + if segment.specialization is not None: + template_strs = [] + + for arg in segment.specialization.args: + template_strs.append(get_type(arg.arg, owner_specs, header_env_mapping)) + spec_str = f'<{", ".join(template_strs)}>' + + return segment_name + spec_str + segment_reprs = list(map(segment_repr, typename.segments)) + # Through environment mapping, it is possible that a segment is resolved into two "segments" + # E.g. a class "vpA" in a namespace "vp" is resolve to "vp::vpA" + # If we resolve for the segments ["vp", "vpA"], we will obtain "vp::vp::vpA" + # We must thus check that this is not the case and filter out redundant segments (in this case, "vp") + final_segment_reprs = [segment_reprs[-1]] # this is always final + + for i in range(len(segment_reprs) - 1): + all_segs_prefix = '::'.join(segment_reprs[i:-1]) + # We only compare with the last one (which should be a class name) since this is what is resolved to a complete name + # TODO: When the problem arises with a templated type, this may fail. + if not final_segment_reprs[-1].startswith(all_segs_prefix + '::'): + final_segment_reprs.insert(len(final_segment_reprs) - 1, segment_reprs[i]) + else: + break + + return '::'.join(final_segment_reprs) + +def get_type(param: Union[types.FunctionType, types.DecoratedType, types.Value], owner_specs: Dict[str, str], header_env_mapping: Dict[str, str]) -> Optional[str]: + ''' + Get the type of a parameter. Compared to get_typename, this function resolves the parameter's constness, whether it is a ref, moveref or pointer. + ''' + if isinstance(param, types.Value): + return tokfmt(param.tokens) # This can appear when parsing templated types! Example: std::map*> as in MBT + if isinstance(param, types.FunctionType): + return_type = get_type(param.return_type, owner_specs, header_env_mapping) + param_types = [get_type(p.type, owner_specs, header_env_mapping) for p in param.parameters] + return f'{return_type}({", ".join(param_types)})' + if isinstance(param, types.Type): + repr_str = get_typename(param.typename, owner_specs, header_env_mapping) + + # split = repr_str.split('<') + # if split[0] in header_env_mapping: + # split[0] = header_env_mapping[split[0]] + # repr_str = '<'.join(split) + if param.const: + repr_str = 'const ' + repr_str + return repr_str + elif isinstance(param, types.Reference): + repr_str = get_type(param.ref_to, owner_specs, header_env_mapping) + if repr_str is not None: + return repr_str + '&' + else: + return None + elif isinstance(param, types.MoveReference): + repr_str = get_type(param.moveref_to, owner_specs, header_env_mapping) + if repr_str is not None: + return repr_str + '&&' + else: + return None + elif isinstance(param, types.Pointer): + repr_str = get_type(param.ptr_to, owner_specs, header_env_mapping) + if repr_str is not None: + return repr_str + '*' + else: + return None + elif isinstance(param, types.Array): + repr_str = get_type(param.array_of, owner_specs, header_env_mapping) + if repr_str is not None: + return repr_str + '[]' + else: + return None + + else: + return None + +def get_type_for_declaration(param: Union[types.FunctionType, types.DecoratedType, types.Value], owner_specs: Dict[str, str], header_env_mapping: Dict[str, str]) -> Optional[str]: + ''' + Type string for when we want to declare a variable of a certain type. + We cannot declare an lref without initializing it (which we can't really do when generating code) + Example use case: declaring a variable inside a lambda. + ''' + + if isinstance(param, types.Reference): + return get_type(param.ref_to, owner_specs, header_env_mapping) + else: + return get_type(param, owner_specs, header_env_mapping) + +def fetch_fully_qualified_id(scope: Union[NamespaceScope, ClassScope], segments: List[str]) -> Union[None, types.EnumDecl, NamespaceScope, ClassScope]: + ''' + Retrieve the declaration of an object from its fully qualified name. + This can be useful when a symbol is reference in two places: + such as in the following header: + + class vpA { + private: + enum vpEnum: unsigned int; + }; + enum vpA::vpEnum : unsigned int {...}; + + In this case, the vpA::vpEnum symbol's visibility (here it is actually private) is undefined in cxxheaderparser (None) when looking at enum declarations outside the class + Here, calling this method with the name vpA::vpEnum will retrieve the enum declaration in the class vpA, for which the visibility is correctly set to private. + ''' + + if len(segments) == 0: + return scope + + seg = segments[0] + if isinstance(scope, NamespaceScope): + for ns in scope.namespaces: + if ns == seg: + return fetch_fully_qualified_id(scope.namespaces[ns], segments[1:]) + for cls in scope.classes: + if get_name(cls.class_decl.typename) == seg: + return fetch_fully_qualified_id(cls, segments[1:]) + if len(segments) == 1: # Test objects that cannot have children + for enum in scope.enums: + if not name_is_anonymous(enum.typename) and get_name(enum.typename) == seg: + return enum + + return None + +def is_pointer_to_const_cstr(param: types.Pointer) -> bool: + ''' + Whether the passed in pointer is of type const char* + ''' + ptr_to = param.ptr_to + if isinstance(ptr_to, types.Type): + if get_typename(ptr_to.typename, {}, {}) == 'char' and ptr_to.const: + return True + + return False + +def is_non_const_ref_to_immutable_type(param: types.DecoratedType) -> bool: + ''' + Returns true if the parameter is a mutable reference to an immutable type in Python. + In python immutable types are: ints, double, string (std::string or char*) + This also takes into account STL containers that are converted from Python containers: + - a Python list is converted to a std::vector. If it is passed by ref, the changes applied to the vector are not propagated to the Python list + - Same for maps + - See https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html + ''' + if not isinstance(param, types.Reference): + return False + if param.ref_to.const: + return False + param_type = get_typename(param.ref_to.typename, {}, {}) + if GeneratorConfig.is_immutable_type(param_type): + return True + return GeneratorConfig.is_immutable_type(param_type) or GeneratorConfig.is_immutable_container(param_type) + +def is_unsupported_return_type(param: Union[types.FunctionType, types.DecoratedType]) -> bool: + ''' + Returns whether the passed param is supported as a return type for automatic code generation. + Pointers, arrays, functions are not supported. + ''' + if isinstance(param, types.FunctionType): + return True + if isinstance(param, types.Array): + return True + if isinstance(param, types.Type): + return False + if isinstance(param, types.Reference): + return is_unsupported_return_type(param.ref_to) + if isinstance(param, types.MoveReference): + return False + if isinstance(param, types.Pointer): + return not is_pointer_to_const_cstr(param) + return True + +def is_unsupported_argument_type(param: Union[types.FunctionType, types.DecoratedType]) -> bool: + ''' + Return whether the passed param is supported for automatic code generation. + Pointers, arrays, functions are not supported. + ''' + if isinstance(param, types.FunctionType): + return True + if isinstance(param, types.Array): + return True + if isinstance(param, types.Type): + return False + if isinstance(param, types.Reference): + return is_unsupported_argument_type(param.ref_to) + if isinstance(param, types.MoveReference): + return True + if isinstance(param, types.Pointer): + return not is_pointer_to_const_cstr(param) # Pointers of type "const char*" are handled by pybind + return True + +def get_method_signature(name: str, return_type: str, params: List[str]) -> str: + ''' + Get the method signature. This does not include method constness or staticness + ''' + return f'{return_type} {name}({", ".join(params)})' + +def method_matches_config(method: types.Method, config: Dict, owner_specs, header_mapping) -> bool: + ''' + Returns whether a method matches a configuration dict. + Matching is performed on the method signature. + The config should come from those defined in the submodule and its json file + ''' + params_strs = [] + if config['static'] != method.static: + return False + params_strs = [get_type(param.type, owner_specs, header_mapping) or '' for param in method.parameters] + signature = get_method_signature(get_name(method.name), get_type(method.return_type, owner_specs, header_mapping) or '', params_strs) + config_signature = config['signature'] + for template in owner_specs: + config_signature = config_signature.replace(f'<{template}>', f'<{owner_specs[template]}>') + + if signature.replace(' ', '') != config_signature.replace(' ', ''): + return False + + return True + +def get_static_and_instance_overloads(methods: List[MethodData]) -> Set[str]: + ''' + Return the set of method names that have static and non-static versions. + This is not allowed in PyBind, so this function can be used to raise an error if the resulting set is not empty + ''' + instance_methods = set([method.py_name for method in methods if not method.method.static]) + static_methods = set([method.py_name for method in methods if method.method.static]) + return instance_methods.intersection(static_methods) diff --git a/modules/python/stubs/CMakeLists.txt b/modules/python/stubs/CMakeLists.txt new file mode 100644 index 0000000000..2a344c5a02 --- /dev/null +++ b/modules/python/stubs/CMakeLists.txt @@ -0,0 +1,47 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings stubs +# +############################################################################# + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in" "${CMAKE_CURRENT_BINARY_DIR}/setup.py" @ONLY) +# MANIFEST file is not really configured by CMAKE, but doing it like this ensures that the target will be regenerated if MANIFEST.in is modified +#configure_file("${CMAKE_CURRENT_SOURCE_DIR}/MANIFEST.in" "${CMAKE_CURRENT_BINARY_DIR}/MANIFEST.in" COPYONLY) + +add_custom_target( visp_python_bindings_stubs + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/visp-stubs" + COMMAND ${PYTHON3_EXECUTABLE} -m pip install ${_pip_args} pybind11-stubgen + COMMAND ${PYTHON3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_stub_generator.py --output-root ${CMAKE_CURRENT_BINARY_DIR} + COMMAND ${PYTHON3_EXECUTABLE} -m pip install ${_pip_args} "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Generating Python stubs with pybind11-stubgen..." + DEPENDS visp_python_bindings_install +) diff --git a/modules/python/stubs/run_stub_generator.py b/modules/python/stubs/run_stub_generator.py new file mode 100644 index 0000000000..129c73f9bb --- /dev/null +++ b/modules/python/stubs/run_stub_generator.py @@ -0,0 +1,61 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings stubs +# +############################################################################# + +from shutil import copy +from pathlib import Path +import subprocess +import sys +import argparse + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--output-root', type=str, help='Path where to save the stubs') + + args = parser.parse_args() + output_root = Path(args.output_root) + assert output_root.exists() + bin_folder = Path(sys.executable).parent + stubgen_entry_point = bin_folder / 'pybind11-stubgen' + assert stubgen_entry_point.exists() + + subprocess.run([str(stubgen_entry_point), '-o', str(output_root.absolute()), '--ignore-all-errors', '_visp'], check=True) + + # Generate stubs for the bindings (C++ side) and mock it so that they appear in the true 'visp' package + p = Path('./_visp') + target_path = Path('./visp-stubs') + target_path.mkdir(exist_ok=True) + for pyi_file in p.iterdir(): + if pyi_file.name.endswith('.pyi'): + copy(pyi_file, target_path / pyi_file.name) # Copy replace old files diff --git a/modules/python/stubs/setup.py.in b/modules/python/stubs/setup.py.in new file mode 100644 index 0000000000..0ee3ae1373 --- /dev/null +++ b/modules/python/stubs/setup.py.in @@ -0,0 +1,52 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings stubs +# +############################################################################# + +from pathlib import Path +from setuptools import setup + +setup( + name='visp-stubs', + version='@VISP_PYTHON_VERSION@', + packages=['visp-stubs'], + author="Samuel Felton", + author_email="samuel.felton@irisa.fr", + description="Python stubs for the ViSP wrapper", + zip_safe=False, + #include_package_data=True, + package_data = { + 'visp-stubs': ['*.pyi'] + }, + python_requires=">=3.7", +) diff --git a/modules/python/test/test_core.py b/modules/python/test/test_core.py new file mode 100644 index 0000000000..01ffdd0f37 --- /dev/null +++ b/modules/python/test/test_core.py @@ -0,0 +1,39 @@ +def test_pixel_meter_convert_points(): + from visp.core import PixelMeterConversion, CameraParameters + import numpy as np + + h, w = 240, 320 + cam = CameraParameters(px=600, py=600, u0=320, v0=240) + + vs, us = np.meshgrid(range(h), range(w), indexing='ij') # vs and us are 2D arrays + + xs, ys = PixelMeterConversion.convertPoints(cam, us, vs) + # xs and ys have the same shape as us and vs + assert xs.shape == (h, w) and ys.shape == (h, w) + + # Converting a numpy array to normalized coords has the same effect as calling on a single image point + for v in range(h): + for u in range(w): + x, y = PixelMeterConversion.convertPoint(cam, u, v) + + assert x == xs[v, u] and y == ys[v, u] + +def test_meter_pixel_convert_points(): + from visp.core import MeterPixelConversion, CameraParameters + import numpy as np + + h, w = 240, 320 + cam = CameraParameters(px=600, py=600, u0=320, v0=240) + + # We use xs and ys as pixel coordinates here, but it's not really true (it's just more convenient) + ys, xs = np.meshgrid(range(h), range(w), indexing='ij') # vs and us are 2D arrays + + us, vs = MeterPixelConversion.convertPoints(cam, xs, ys) + # xs and ys have the same shape as us and vs + assert us.shape == (h, w) and vs.shape == (h, w) + + # Converting a numpy array to normalized coords has the same effect as calling on a single image point + for y in range(h): + for x in range(w): + u, v = MeterPixelConversion.convertPoint(cam, x, y) + assert u == us[y, x] and v == vs[y, x] diff --git a/modules/python/test/test_core_repr.py b/modules/python/test/test_core_repr.py new file mode 100644 index 0000000000..8d7c474940 --- /dev/null +++ b/modules/python/test/test_core_repr.py @@ -0,0 +1,59 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings test +# +############################################################################# + +import visp +from visp.core import ArrayDouble2D, RotationMatrix, Matrix, HomogeneousMatrix, PoseVector + +import numpy as np +import pytest + +def test_array_operations(): + array1 = ArrayDouble2D(2, 2, 1) + array2 = ArrayDouble2D(2, 2, 1) + assert array1 == array2 + +def test_matrix_operations(): + m1 = Matrix(4, 4, 2.0) + m2 = Matrix(4, 4, 1.0) + m3 = Matrix(4, 4, 3.0) + m4 = Matrix(4, 4, 6 * 4) + + assert m1 + m2 == m3 + assert m3 - m1 == m2 + assert m1 * m3 == m4 + assert m2 * 2 == m1 + +def test_rotation_repr_can_be_defined_by_hand(): + R = RotationMatrix() diff --git a/modules/python/test/test_numpy_array.py b/modules/python/test/test_numpy_array.py new file mode 100644 index 0000000000..f4fd9eff71 --- /dev/null +++ b/modules/python/test/test_numpy_array.py @@ -0,0 +1,126 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings test +# +############################################################################# + +import visp +from visp.core import ArrayDouble2D, RotationMatrix, Matrix, HomogeneousMatrix, PoseVector + +import numpy as np +import pytest + +def test_np_array_modifies_vp_array(): + # Test that numpy is a view of array and that writing to numpy array modifies vpArray + array = ArrayDouble2D(5, 5, 1.0) + assert array.getRows() == array.getCols() == 5 + array_np = np.array(array, copy=False) + assert array_np.shape == (5, 5) + assert np.all(array_np == 1.0) + array_np[0:2, 0:2] = 2 + assert array.getMinValue() == 1 and array.getMaxValue() == 2 + +def fn_test_not_writable_2d(R): + R_np = np.array(R, copy=False) + with pytest.raises(ValueError): + R_np[0, 0] = 1 + with pytest.raises(ValueError): + R.numpy()[:1] = 0 + with pytest.raises(ValueError): + row = R[0] + row[0] = 1 + with pytest.raises(ValueError): + sub = R[:2, :2] + sub[0, :] = 1 + +def test_rotation_matrix_not_writable(): + R = RotationMatrix() + fn_test_not_writable_2d(R) + +def test_homogeneous_matrix_not_writable(): + T = HomogeneousMatrix() + fn_test_not_writable_2d(T) + +def test_numpy_constructor(): + n_invalid = np.array([1, 2, 3]) + with pytest.raises(RuntimeError): + a = ArrayDouble2D(n_invalid) + n_valid = np.array([[1, 2, 3], [4, 5, 6]]) + a = ArrayDouble2D(n_valid) + + assert np.all(np.equal(a.numpy(), n_valid)) + +def test_numpy_conversion_and_back(): + a = ArrayDouble2D(10, 10, 2.0) + a_np = a.numpy().copy() + a2 = ArrayDouble2D(a_np) + mat = Matrix(a_np) + + for i in range(a.getRows()): + for j in range(a.getCols()): + assert a[i, j] == a_np[i, j] + assert a[i, j] == a2[i, j] + assert mat[i, j] == a[i, j] + +def test_indexing_array2D(): + a_np = np.asarray([[i for _ in range(10)] for i in range(10)]) + a = ArrayDouble2D(a_np) + col = list(range(10)) + for i in range(a.getRows()): + assert np.all(a[i] == float(i)) + assert np.all(a[-i - 1] == float(a.getRows() - i - 1)) + assert np.all(a[:, i] == col) + assert np.all(a[:, -i - 1] == col) + +def test_index_row_not_copy(): + a = ArrayDouble2D(5, 5, 1.0) + first_row_view = a[0] + first_row_view[0] = 0.0 + assert a[0, 0] == 0.0 + +def test_index_slice_not_copy(): + a = ArrayDouble2D(5, 5, 1.0) + sub_matrix = a[1:3] + sub_matrix[0] = 0.0 + for i in range(a.getCols()): + assert a[1, i] == 0.0 + +def test_index_tuple_not_copy(): + a = ArrayDouble2D(5, 5, 1.0) + col = a[:, -1] + col[0] = 0.0 + assert a[0, -1] == 0.0 + sub = a[0:2, 0:2] + sub[:, :] = 0.0 + for i in range(2): + for j in range(2): + assert a[i, j] == 0.0 diff --git a/modules/python/test/test_numpy_image.py b/modules/python/test/test_numpy_image.py new file mode 100644 index 0000000000..3dc2eff451 --- /dev/null +++ b/modules/python/test/test_numpy_image.py @@ -0,0 +1,116 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings test +# +############################################################################# + +from typing import Any, List, Dict +from visp.core import ImageGray, ImageRGBa, ImageRGBf, RGBa, RGBf + +import numpy as np +import pytest + +def get_data_dicts() -> List[Dict[str, Any]]: + h, w = 20, 20 + return [ + { + 'instance': ImageGray(h, w, 0), + 'base_value': 0, + 'value': 255, + 'np_value': 255, + 'shape': (h, w), + 'dtype': np.uint8 + }, + { + 'instance': ImageRGBa(h, w, RGBa(0, 50, 75, 255)), + 'base_value': RGBa(0, 50, 75, 255), + 'value': RGBa(255, 128, 0, 100), + 'np_value': np.asarray([255, 128, 0, 100], dtype=np.uint8), + 'shape': (h, w, 4), + 'dtype': np.uint8 + }, + { + 'instance': ImageRGBf(h, w, RGBf(0.0, 0.0, 0.0)), + 'base_value': RGBf(0.0, 0.0, 0.0), + 'value': RGBf(255, 0, 0), + 'np_value': np.asarray([255, 0, 0], dtype=np.float32), + 'shape': (h, w, 3), + 'dtype': np.float32 + + }, + + ] + + +def test_np_array_shape_type(): + ''' + Tests buffer definition, shape and dtype + ''' + for test_dict in get_data_dicts(): + np_array = np.array(test_dict['instance'], copy=False) + assert np_array.shape == test_dict['shape'] + assert np_array.dtype == test_dict['dtype'] + +def test_np_array_shape_type_numpy_fn(): + ''' + Tests converting to a numpy array by calling our defined function + ''' + for test_dict in get_data_dicts(): + np_array = test_dict['instance'].numpy() + assert np_array.shape == test_dict['shape'] + assert np_array.dtype == test_dict['dtype'] + +def test_np_array_replace_value(): + ''' + Tests 2D pixel indexing and correspondance between visp pixel reps and numpy reps + ''' + for test_dict in get_data_dicts(): + vp_image = test_dict['instance'] + np_array = np.array(vp_image, copy=False) + np_array[::2, ::2] = test_dict['np_value'] + for i in range(vp_image.getHeight()): + for j in range(vp_image.getWidth()): + if i % 2 == 0 and j % 2 == 0: + assert vp_image[i, j] == test_dict['value'] + assert vp_image[-i, -j] == test_dict['value'] + else: + assert vp_image[i, j] == test_dict['base_value'] + assert vp_image[-i, -j] == test_dict['base_value'] + + with pytest.raises(RuntimeError): + vp_image[vp_image.getHeight()] + with pytest.raises(RuntimeError): + vp_image[0, vp_image.getWidth()] + with pytest.raises(RuntimeError): + vp_image[-vp_image.getHeight() - 1] + with pytest.raises(RuntimeError): + vp_image[0, -vp_image.getWidth() - 1] diff --git a/modules/python/test/test_specific_cases.py b/modules/python/test/test_specific_cases.py new file mode 100644 index 0000000000..4015ed1278 --- /dev/null +++ b/modules/python/test/test_specific_cases.py @@ -0,0 +1,76 @@ +############################################################################# +# +# ViSP, open source Visual Servoing Platform software. +# Copyright (C) 2005 - 2023 by Inria. All rights reserved. +# +# This software is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# See the file LICENSE.txt at the root directory of this source +# distribution for additional information about the GNU GPL. +# +# For using ViSP with software that can not be combined with the GNU +# GPL, please contact Inria about acquiring a ViSP Professional +# Edition License. +# +# See https://visp.inria.fr for more information. +# +# This software was developed at: +# Inria Rennes - Bretagne Atlantique +# Campus Universitaire de Beaulieu +# 35042 Rennes Cedex +# France +# +# If you have questions regarding the use of this file, please contact +# Inria at visp@inria.fr +# +# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +# +# Description: +# ViSP Python bindings test +# +############################################################################# + +from pytest import approx +from visp.core import Math, ImagePoint, ColVector, ThetaUVector, Point +def test_tuple_return_basic_values_only_output(): + ''' + Test that a function that was with signature + double lineFitting(const std::vector<...>& imPts, double& a, double& b, double& c) + is now (with custom configuration) with a signature lineFitting(imPts) -> tuple[double * 4] + + all the reference parameters are used as outputs but not as inputs + + ''' + a = 45.0 + b = 52.0 + points = [ + ImagePoint(a*i+b, i) for i in range(3) + ] + res = Math.lineFitting(points) + print(res) + values = (res[0], -res[1] / res[2], res[3] / res[2]) + expected = (0, a, b) + for i, (val, exp) in enumerate(zip(values, expected)): + print(i) + assert val == approx(exp, 0.0001) + +def test_tuple_return_basic_values_only_output_2(): + theta = 3.14 + vec = [0.0, 0.0, 1.0] + thetau = ThetaUVector(*[v * theta for v in vec]) + theta_out, vec_out = thetau.extract() + assert theta_out == theta + assert vec_out == ColVector(vec) + +def test_pass_by_ref(): + values = [0, 1, 2] + p = Point(values) + vec_ref = ColVector() + p.getWorldCoordinates(vec_ref) + assert vec_ref == p.getWorldCoordinates() + +def test_tuple_return_basic_values_mixed(): + pass diff --git a/modules/tracker/mbt/include/visp3/mbt/vpMbDepthDenseTracker.h b/modules/tracker/mbt/include/visp3/mbt/vpMbDepthDenseTracker.h index 0dd98709d4..84d1ceb81f 100644 --- a/modules/tracker/mbt/include/visp3/mbt/vpMbDepthDenseTracker.h +++ b/modules/tracker/mbt/include/visp3/mbt/vpMbDepthDenseTracker.h @@ -168,5 +168,6 @@ class VISP_EXPORT vpMbDepthDenseTracker : public virtual vpMbTracker void segmentPointCloud(const pcl::PointCloud::ConstPtr &point_cloud); #endif void segmentPointCloud(const std::vector &point_cloud, unsigned int width, unsigned int height); + void segmentPointCloud(const vpMatrix &point_cloud, unsigned int width, unsigned int height); }; #endif diff --git a/modules/tracker/mbt/include/visp3/mbt/vpMbDepthNormalTracker.h b/modules/tracker/mbt/include/visp3/mbt/vpMbDepthNormalTracker.h index c48d2fd5f4..ed3cca7ab3 100644 --- a/modules/tracker/mbt/include/visp3/mbt/vpMbDepthNormalTracker.h +++ b/modules/tracker/mbt/include/visp3/mbt/vpMbDepthNormalTracker.h @@ -183,5 +183,6 @@ class VISP_EXPORT vpMbDepthNormalTracker : public virtual vpMbTracker void segmentPointCloud(const pcl::PointCloud::ConstPtr &point_cloud); #endif void segmentPointCloud(const std::vector &point_cloud, unsigned int width, unsigned int height); + void segmentPointCloud(const vpMatrix &point_cloud, unsigned int width, unsigned int height); }; #endif diff --git a/modules/tracker/mbt/include/visp3/mbt/vpMbGenericTracker.h b/modules/tracker/mbt/include/visp3/mbt/vpMbGenericTracker.h index 3175dcd0a1..b4086d14ee 100644 --- a/modules/tracker/mbt/include/visp3/mbt/vpMbGenericTracker.h +++ b/modules/tracker/mbt/include/visp3/mbt/vpMbGenericTracker.h @@ -39,6 +39,7 @@ #ifndef _vpMbGenericTracker_h_ #define _vpMbGenericTracker_h_ +#include #include #include #include @@ -605,6 +606,15 @@ class VISP_EXPORT vpMbGenericTracker : public vpMbTracker std::map &mapOfPointCloudWidths, std::map &mapOfPointCloudHeights); + virtual void track(std::map *> &mapOfImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights); + virtual void track(std::map *> &mapOfColorImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights); + protected: virtual void computeProjectionError(); @@ -641,6 +651,10 @@ class VISP_EXPORT vpMbGenericTracker : public vpMbTracker std::map *> &mapOfPointClouds, std::map &mapOfPointCloudWidths, std::map &mapOfPointCloudHeights); + virtual void preTracking(std::map *> &mapOfImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights); private: class TrackerWrapper : public vpMbEdgeTracker, @@ -769,6 +783,9 @@ class VISP_EXPORT vpMbGenericTracker : public vpMbTracker virtual void preTracking(const vpImage *const ptr_I = nullptr, const std::vector *const point_cloud = nullptr, const unsigned int pointcloud_width = 0, const unsigned int pointcloud_height = 0); + virtual void preTracking(const vpImage *const ptr_I = nullptr, + const vpMatrix *const point_cloud = nullptr, + const unsigned int pointcloud_width = 0, const unsigned int pointcloud_height = 0); virtual void reInitModel(const vpImage *const I, const vpImage *const I_color, const std::string &cad_name, const vpHomogeneousMatrix &cMo, bool verbose = false, diff --git a/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthDense.h b/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthDense.h index 0ae0eea302..beb1f433b6 100644 --- a/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthDense.h +++ b/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthDense.h @@ -53,7 +53,8 @@ class VISP_EXPORT vpMbtFaceDepthDense { public: - enum vpDepthDenseFilteringType { + enum vpDepthDenseFilteringType + { NO_FILTERING = 0, ///< Face is used if visible DEPTH_OCCUPANCY_RATIO_FILTERING = 1 << 1, ///< Face is used if there is ///< enough depth information in @@ -103,6 +104,14 @@ class VISP_EXPORT vpMbtFaceDepthDense #if DEBUG_DISPLAY_DEPTH_DENSE , vpImage &debugImage, std::vector > &roiPts_vec +#endif + , + const vpImage *mask = nullptr); + bool computeDesiredFeatures(const vpHomogeneousMatrix &cMo, unsigned int width, unsigned int height, + const vpMatrix &point_cloud, unsigned int stepX, unsigned int stepY +#if DEBUG_DISPLAY_DEPTH_DENSE + , + vpImage &debugImage, std::vector > &roiPts_vec #endif , const vpImage *mask = nullptr); @@ -146,7 +155,8 @@ class VISP_EXPORT vpMbtFaceDepthDense { if (occupancyRatio < 0.0 || occupancyRatio > 1.0) { std::cerr << "occupancyRatio < 0.0 || occupancyRatio > 1.0" << std::endl; - } else { + } + else { m_depthDenseFilteringOccupancyRatio = occupancyRatio; } } @@ -168,7 +178,7 @@ class VISP_EXPORT vpMbtFaceDepthDense //! The second extremity clipped in the image frame vpImagePoint m_imPt2; - PolygonLine() : m_p1(nullptr), m_p2(nullptr), m_poly(), m_imPt1(), m_imPt2() {} + PolygonLine() : m_p1(nullptr), m_p2(nullptr), m_poly(), m_imPt1(), m_imPt2() { } PolygonLine(const PolygonLine &polyLine) : m_p1(nullptr), m_p2(nullptr), m_poly(polyLine.m_poly), m_imPt1(polyLine.m_imPt1), m_imPt2(polyLine.m_imPt2) diff --git a/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthNormal.h b/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthNormal.h index db5a673d70..7b59a9b625 100644 --- a/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthNormal.h +++ b/modules/tracker/mbt/include/visp3/mbt/vpMbtFaceDepthNormal.h @@ -53,12 +53,14 @@ class VISP_EXPORT vpMbtFaceDepthNormal { public: - enum vpFaceCentroidType { + enum vpFaceCentroidType + { GEOMETRIC_CENTROID, ///< Compute the geometric centroid MEAN_CENTROID ///< Compute the mean centroid }; - enum vpFeatureEstimationType { + enum vpFeatureEstimationType + { ROBUST_FEATURE_ESTIMATION = 0, ROBUST_SVD_PLANE_ESTIMATION = 1, #ifdef VISP_HAVE_PCL @@ -106,6 +108,15 @@ class VISP_EXPORT vpMbtFaceDepthNormal #if DEBUG_DISPLAY_DEPTH_NORMAL , vpImage &debugImage, std::vector > &roiPts_vec +#endif + , + const vpImage *mask = nullptr); + bool computeDesiredFeatures(const vpHomogeneousMatrix &cMo, unsigned int width, unsigned int height, + const vpMatrix &point_cloud, vpColVector &desired_features, + unsigned int stepX, unsigned int stepY +#if DEBUG_DISPLAY_DEPTH_NORMAL + , + vpImage &debugImage, std::vector > &roiPts_vec #endif , const vpImage *mask = nullptr); @@ -179,7 +190,7 @@ class VISP_EXPORT vpMbtFaceDepthNormal //! The second extremity clipped in the image frame vpImagePoint m_imPt2; - PolygonLine() : m_p1(nullptr), m_p2(nullptr), m_poly(), m_imPt1(), m_imPt2() {} + PolygonLine() : m_p1(nullptr), m_p2(nullptr), m_poly(), m_imPt1(), m_imPt2() { } PolygonLine(const PolygonLine &polyLine) : m_p1(nullptr), m_p2(nullptr), m_poly(polyLine.m_poly), m_imPt1(polyLine.m_imPt1), m_imPt2(polyLine.m_imPt2) @@ -211,7 +222,7 @@ class VISP_EXPORT vpMbtFaceDepthNormal public: std::vector data; - Mat33() : data(9) {} + Mat33() : data(9) { } inline T operator[](const size_t i) const { return data[i]; } @@ -221,7 +232,7 @@ class VISP_EXPORT vpMbtFaceDepthNormal { // determinant T det = data[0] * (data[4] * data[8] - data[7] * data[5]) - data[1] * (data[3] * data[8] - data[5] * data[6]) + - data[2] * (data[3] * data[7] - data[4] * data[6]); + data[2] * (data[3] * data[7] - data[4] * data[6]); T invdet = 1 / det; Mat33 minv; @@ -305,7 +316,7 @@ class VISP_EXPORT vpMbtFaceDepthNormal #ifdef VISP_HAVE_NLOHMANN_JSON #include #ifdef VISP_HAVE_PCL -NLOHMANN_JSON_SERIALIZE_ENUM( vpMbtFaceDepthNormal::vpFeatureEstimationType, { +NLOHMANN_JSON_SERIALIZE_ENUM(vpMbtFaceDepthNormal::vpFeatureEstimationType, { {vpMbtFaceDepthNormal::ROBUST_FEATURE_ESTIMATION, "robust"}, {vpMbtFaceDepthNormal::ROBUST_SVD_PLANE_ESTIMATION, "robustSVD"}, {vpMbtFaceDepthNormal::PCL_PLANE_ESTIMATION, "pcl"} diff --git a/modules/tracker/mbt/src/depth/vpMbDepthDenseTracker.cpp b/modules/tracker/mbt/src/depth/vpMbDepthDenseTracker.cpp index ca74cdc202..db38947bd9 100644 --- a/modules/tracker/mbt/src/depth/vpMbDepthDenseTracker.cpp +++ b/modules/tracker/mbt/src/depth/vpMbDepthDenseTracker.cpp @@ -54,11 +54,11 @@ vpMbDepthDenseTracker::vpMbDepthDenseTracker() : m_depthDenseHiddenFacesDisplay(), m_depthDenseListOfActiveFaces(), m_denseDepthNbFeatures(0), m_depthDenseFaces(), - m_depthDenseSamplingStepX(2), m_depthDenseSamplingStepY(2), m_error_depthDense(), m_L_depthDense(), - m_robust_depthDense(), m_w_depthDense(), m_weightedError_depthDense() + m_depthDenseSamplingStepX(2), m_depthDenseSamplingStepY(2), m_error_depthDense(), m_L_depthDense(), + m_robust_depthDense(), m_w_depthDense(), m_weightedError_depthDense() #if DEBUG_DISPLAY_DEPTH_DENSE - , - m_debugDisp_depthDense(nullptr), m_debugImage_depthDense() + , + m_debugDisp_depthDense(nullptr), m_debugImage_depthDense() #endif { #ifdef VISP_HAVE_OGRE @@ -287,7 +287,7 @@ void vpMbDepthDenseTracker::display(const vpImage &I, const vpHom bool displayFullModel) { std::vector > models = - vpMbDepthDenseTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); + vpMbDepthDenseTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); for (size_t i = 0; i < models.size(); i++) { if (vpMath::equal(models[i][0], 0)) { @@ -303,7 +303,7 @@ void vpMbDepthDenseTracker::display(const vpImage &I, const vpHomogeneou bool displayFullModel) { std::vector > models = - vpMbDepthDenseTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); + vpMbDepthDenseTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); for (size_t i = 0; i < models.size(); i++) { if (vpMath::equal(models[i][0], 0)) { @@ -352,7 +352,7 @@ std::vector > vpMbDepthDenseTracker::getModelForDisplay(unsi ++it) { vpMbtFaceDepthDense *face_dense = *it; std::vector > modelLines = - face_dense->getModelForDisplay(width, height, cMo, cam, displayFullModel); + face_dense->getModelForDisplay(width, height, cMo, cam, displayFullModel); models.insert(models.end(), modelLines.begin(), modelLines.end()); } @@ -368,7 +368,8 @@ void vpMbDepthDenseTracker::init(const vpImage &I) bool reInitialisation = false; if (!useOgre) { faces.setVisible(I.getWidth(), I.getHeight(), m_cam, m_cMo, angleAppears, angleDisappears, reInitialisation); - } else { + } + else { #ifdef VISP_HAVE_OGRE if (!faces.isOgreInitialised()) { faces.setBackgroundSizeOgre(I.getHeight(), I.getWidth()); @@ -408,7 +409,8 @@ void vpMbDepthDenseTracker::loadConfigFile(const std::string &configFile, bool v std::cout << " *********** Parsing XML for Mb Depth Dense Tracker ************ " << std::endl; } xmlp.parse(configFile); - } catch (const vpException &e) { + } + catch (const vpException &e) { std::cerr << "Exception: " << e.what() << std::endl; throw vpException(vpException::ioError, "Cannot open XML file \"%s\"", configFile.c_str()); } @@ -658,6 +660,65 @@ void vpMbDepthDenseTracker::segmentPointCloud(const std::vector &po #endif } + +void vpMbDepthDenseTracker::segmentPointCloud(const vpMatrix &point_cloud, unsigned int width, + unsigned int height) +{ + m_depthDenseListOfActiveFaces.clear(); + +#if DEBUG_DISPLAY_DEPTH_DENSE + if (!m_debugDisp_depthDense->isInitialised()) { + m_debugImage_depthDense.resize(height, width); + m_debugDisp_depthDense->init(m_debugImage_depthDense, 50, 0, "Debug display dense depth tracker"); + } + + m_debugImage_depthDense = 0; + std::vector > roiPts_vec; +#endif + + for (std::vector::iterator it = m_depthDenseFaces.begin(); it != m_depthDenseFaces.end(); + ++it) { + vpMbtFaceDepthDense *face = *it; + + if (face->isVisible() && face->isTracked()) { +#if DEBUG_DISPLAY_DEPTH_DENSE + std::vector > roiPts_vec_; +#endif + if (face->computeDesiredFeatures(m_cMo, width, height, point_cloud, m_depthDenseSamplingStepX, + m_depthDenseSamplingStepY +#if DEBUG_DISPLAY_DEPTH_DENSE + , + m_debugImage_depthDense, roiPts_vec_ +#endif + , + m_mask)) { + m_depthDenseListOfActiveFaces.push_back(*it); + +#if DEBUG_DISPLAY_DEPTH_DENSE + roiPts_vec.insert(roiPts_vec.end(), roiPts_vec_.begin(), roiPts_vec_.end()); +#endif + } + } + } + +#if DEBUG_DISPLAY_DEPTH_DENSE + vpDisplay::display(m_debugImage_depthDense); + + for (size_t i = 0; i < roiPts_vec.size(); i++) { + if (roiPts_vec[i].empty()) + continue; + + for (size_t j = 0; j < roiPts_vec[i].size() - 1; j++) { + vpDisplay::displayLine(m_debugImage_depthDense, roiPts_vec[i][j], roiPts_vec[i][j + 1], vpColor::red, 2); + } + vpDisplay::displayLine(m_debugImage_depthDense, roiPts_vec[i][0], roiPts_vec[i][roiPts_vec[i].size() - 1], + vpColor::red, 2); + } + + vpDisplay::flush(m_debugImage_depthDense); +#endif +} + void vpMbDepthDenseTracker::setOgreVisibilityTest(const bool &v) { vpMbTracker::setOgreVisibilityTest(v); @@ -710,7 +771,7 @@ void vpMbDepthDenseTracker::setUseDepthDenseTracking(const std::string &name, co } } -void vpMbDepthDenseTracker::testTracking() {} +void vpMbDepthDenseTracker::testTracking() { } void vpMbDepthDenseTracker::track(const vpImage &) { diff --git a/modules/tracker/mbt/src/depth/vpMbDepthNormalTracker.cpp b/modules/tracker/mbt/src/depth/vpMbDepthNormalTracker.cpp index 0901245fca..2b349c8c12 100644 --- a/modules/tracker/mbt/src/depth/vpMbDepthNormalTracker.cpp +++ b/modules/tracker/mbt/src/depth/vpMbDepthNormalTracker.cpp @@ -54,14 +54,14 @@ vpMbDepthNormalTracker::vpMbDepthNormalTracker() : m_depthNormalFeatureEstimationMethod(vpMbtFaceDepthNormal::ROBUST_FEATURE_ESTIMATION), - m_depthNormalHiddenFacesDisplay(), m_depthNormalListOfActiveFaces(), m_depthNormalListOfDesiredFeatures(), - m_depthNormalFaces(), m_depthNormalPclPlaneEstimationMethod(2), m_depthNormalPclPlaneEstimationRansacMaxIter(200), - m_depthNormalPclPlaneEstimationRansacThreshold(0.001), m_depthNormalSamplingStepX(2), m_depthNormalSamplingStepY(2), - m_depthNormalUseRobust(false), m_error_depthNormal(), m_featuresToBeDisplayedDepthNormal(), m_L_depthNormal(), - m_robust_depthNormal(), m_w_depthNormal(), m_weightedError_depthNormal() + m_depthNormalHiddenFacesDisplay(), m_depthNormalListOfActiveFaces(), m_depthNormalListOfDesiredFeatures(), + m_depthNormalFaces(), m_depthNormalPclPlaneEstimationMethod(2), m_depthNormalPclPlaneEstimationRansacMaxIter(200), + m_depthNormalPclPlaneEstimationRansacThreshold(0.001), m_depthNormalSamplingStepX(2), m_depthNormalSamplingStepY(2), + m_depthNormalUseRobust(false), m_error_depthNormal(), m_featuresToBeDisplayedDepthNormal(), m_L_depthNormal(), + m_robust_depthNormal(), m_w_depthNormal(), m_weightedError_depthNormal() #if DEBUG_DISPLAY_DEPTH_NORMAL - , - m_debugDisp_depthNormal(nullptr), m_debugImage_depthNormal() + , + m_debugDisp_depthNormal(nullptr), m_debugImage_depthNormal() #endif { #ifdef VISP_HAVE_OGRE @@ -282,7 +282,7 @@ void vpMbDepthNormalTracker::display(const vpImage &I, const vpHo bool displayFullModel) { std::vector > models = - vpMbDepthNormalTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); + vpMbDepthNormalTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); for (size_t i = 0; i < models.size(); i++) { if (vpMath::equal(models[i][0], 0)) { @@ -308,7 +308,7 @@ void vpMbDepthNormalTracker::display(const vpImage &I, const vpHomogeneo bool displayFullModel) { std::vector > models = - vpMbDepthNormalTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); + vpMbDepthNormalTracker::getModelForDisplay(I.getWidth(), I.getHeight(), cMo, cam, displayFullModel); for (size_t i = 0; i < models.size(); i++) { if (vpMath::equal(models[i][0], 0)) { @@ -381,7 +381,7 @@ std::vector > vpMbDepthNormalTracker::getModelForDisplay(uns it != m_depthNormalFaces.end(); ++it) { vpMbtFaceDepthNormal *face_normal = *it; std::vector > modelLines = - face_normal->getModelForDisplay(width, height, cMo, cam, displayFullModel); + face_normal->getModelForDisplay(width, height, cMo, cam, displayFullModel); models.insert(models.end(), modelLines.begin(), modelLines.end()); } @@ -397,7 +397,8 @@ void vpMbDepthNormalTracker::init(const vpImage &I) bool reInitialisation = false; if (!useOgre) { faces.setVisible(I.getWidth(), I.getHeight(), m_cam, m_cMo, angleAppears, angleDisappears, reInitialisation); - } else { + } + else { #ifdef VISP_HAVE_OGRE if (!faces.isOgreInitialised()) { faces.setBackgroundSizeOgre(I.getHeight(), I.getWidth()); @@ -441,7 +442,8 @@ void vpMbDepthNormalTracker::loadConfigFile(const std::string &configFile, bool std::cout << " *********** Parsing XML for Mb Depth Tracker ************ " << std::endl; } xmlp.parse(configFile); - } catch (const vpException &e) { + } + catch (const vpException &e) { std::cerr << "Exception: " << e.what() << std::endl; throw vpException(vpException::ioError, "Cannot open XML file \"%s\"", configFile.c_str()); } @@ -584,7 +586,7 @@ void vpMbDepthNormalTracker::setUseDepthNormalTracking(const std::string &name, } } -void vpMbDepthNormalTracker::testTracking() {} +void vpMbDepthNormalTracker::testTracking() { } #ifdef VISP_HAVE_PCL void vpMbDepthNormalTracker::segmentPointCloud(const pcl::PointCloud::ConstPtr &point_cloud) @@ -712,6 +714,69 @@ void vpMbDepthNormalTracker::segmentPointCloud(const std::vector &p #endif } +void vpMbDepthNormalTracker::segmentPointCloud(const vpMatrix &point_cloud, unsigned int width, + unsigned int height) +{ + m_depthNormalListOfActiveFaces.clear(); + m_depthNormalListOfDesiredFeatures.clear(); + +#if DEBUG_DISPLAY_DEPTH_NORMAL + if (!m_debugDisp_depthNormal->isInitialised()) { + m_debugImage_depthNormal.resize(height, width); + m_debugDisp_depthNormal->init(m_debugImage_depthNormal, 50, 0, "Debug display normal depth tracker"); + } + + m_debugImage_depthNormal = 0; + std::vector > roiPts_vec; +#endif + + for (std::vector::iterator it = m_depthNormalFaces.begin(); it != m_depthNormalFaces.end(); + ++it) { + vpMbtFaceDepthNormal *face = *it; + + if (face->isVisible() && face->isTracked()) { + vpColVector desired_features; + +#if DEBUG_DISPLAY_DEPTH_NORMAL + std::vector > roiPts_vec_; +#endif + + if (face->computeDesiredFeatures(m_cMo, width, height, point_cloud, desired_features, m_depthNormalSamplingStepX, + m_depthNormalSamplingStepY +#if DEBUG_DISPLAY_DEPTH_NORMAL + , + m_debugImage_depthNormal, roiPts_vec_ +#endif + , + m_mask)) { + m_depthNormalListOfDesiredFeatures.push_back(desired_features); + m_depthNormalListOfActiveFaces.push_back(face); + +#if DEBUG_DISPLAY_DEPTH_NORMAL + roiPts_vec.insert(roiPts_vec.end(), roiPts_vec_.begin(), roiPts_vec_.end()); +#endif + } + } + } + +#if DEBUG_DISPLAY_DEPTH_NORMAL + vpDisplay::display(m_debugImage_depthNormal); + + for (size_t i = 0; i < roiPts_vec.size(); i++) { + if (roiPts_vec[i].empty()) + continue; + + for (size_t j = 0; j < roiPts_vec[i].size() - 1; j++) { + vpDisplay::displayLine(m_debugImage_depthNormal, roiPts_vec[i][j], roiPts_vec[i][j + 1], vpColor::red, 2); + } + vpDisplay::displayLine(m_debugImage_depthNormal, roiPts_vec[i][0], roiPts_vec[i][roiPts_vec[i].size() - 1], + vpColor::red, 2); + } + + vpDisplay::flush(m_debugImage_depthNormal); +#endif +} + void vpMbDepthNormalTracker::setCameraParameters(const vpCameraParameters &cam) { m_cam = cam; diff --git a/modules/tracker/mbt/src/depth/vpMbtFaceDepthDense.cpp b/modules/tracker/mbt/src/depth/vpMbtFaceDepthDense.cpp index 0dacb833f1..7d0249cdc2 100644 --- a/modules/tracker/mbt/src/depth/vpMbtFaceDepthDense.cpp +++ b/modules/tracker/mbt/src/depth/vpMbtFaceDepthDense.cpp @@ -421,6 +421,91 @@ bool vpMbtFaceDepthDense::computeDesiredFeatures(const vpHomogeneousMatrix &cMo, return true; } +bool vpMbtFaceDepthDense::computeDesiredFeatures(const vpHomogeneousMatrix &cMo, unsigned int width, + unsigned int height, const vpMatrix &point_cloud, + unsigned int stepX, unsigned int stepY +#if DEBUG_DISPLAY_DEPTH_DENSE + , + vpImage &debugImage, + std::vector > &roiPts_vec +#endif + , + const vpImage *mask) +{ + m_pointCloudFace.clear(); + + if (width == 0 || height == 0) + return 0; + + std::vector roiPts; + double distanceToFace; + computeROI(cMo, width, height, roiPts +#if DEBUG_DISPLAY_DEPTH_DENSE + , + roiPts_vec +#endif + , + distanceToFace); + + if (roiPts.size() <= 2) { +#ifndef NDEBUG + std::cerr << "Error: roiPts.size() <= 2 in computeDesiredFeatures" << std::endl; +#endif + return false; + } + + if (((m_depthDenseFilteringMethod & MAX_DISTANCE_FILTERING) && distanceToFace > m_depthDenseFilteringMaxDist) || + ((m_depthDenseFilteringMethod & MIN_DISTANCE_FILTERING) && distanceToFace < m_depthDenseFilteringMinDist)) { + return false; + } + + vpPolygon polygon_2d(roiPts); + vpRect bb = polygon_2d.getBoundingBox(); + + unsigned int top = (unsigned int)std::max(0.0, bb.getTop()); + unsigned int bottom = (unsigned int)std::min((double)height, std::max(0.0, bb.getBottom())); + unsigned int left = (unsigned int)std::max(0.0, bb.getLeft()); + unsigned int right = (unsigned int)std::min((double)width, std::max(0.0, bb.getRight())); + + bb.setTop(top); + bb.setBottom(bottom); + bb.setLeft(left); + bb.setRight(right); + + m_pointCloudFace.reserve((size_t)(bb.getWidth() * bb.getHeight())); + + int totalTheoreticalPoints = 0, totalPoints = 0; + for (unsigned int i = top; i < bottom; i += stepY) { + for (unsigned int j = left; j < right; j += stepX) { + if ((m_useScanLine ? (i < m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs().getHeight() && + j < m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs().getWidth() && + m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs()[i][j] == m_polygon->getIndex()) + : polygon_2d.isInside(vpImagePoint(i, j)))) { + totalTheoreticalPoints++; + + if (vpMeTracker::inMask(mask, i, j) && point_cloud[i * width + j][2] > 0) { + totalPoints++; + + m_pointCloudFace.push_back(point_cloud[i * width + j][0]); + m_pointCloudFace.push_back(point_cloud[i * width + j][1]); + m_pointCloudFace.push_back(point_cloud[i * width + j][2]); + +#if DEBUG_DISPLAY_DEPTH_DENSE + debugImage[i][j] = 255; +#endif + } + } + } + } + + if (totalPoints == 0 || ((m_depthDenseFilteringMethod & DEPTH_OCCUPANCY_RATIO_FILTERING) && + totalPoints / (double)totalTheoreticalPoints < m_depthDenseFilteringOccupancyRatio)) { + return false; + } + + return true; +} + void vpMbtFaceDepthDense::computeVisibility() { m_isVisible = m_polygon->isVisible(); } void vpMbtFaceDepthDense::computeVisibilityDisplay() diff --git a/modules/tracker/mbt/src/depth/vpMbtFaceDepthNormal.cpp b/modules/tracker/mbt/src/depth/vpMbtFaceDepthNormal.cpp index faee1d75d3..b741766410 100644 --- a/modules/tracker/mbt/src/depth/vpMbtFaceDepthNormal.cpp +++ b/modules/tracker/mbt/src/depth/vpMbtFaceDepthNormal.cpp @@ -475,6 +475,166 @@ bool vpMbtFaceDepthNormal::computeDesiredFeatures(const vpHomogeneousMatrix &cMo return true; } +bool vpMbtFaceDepthNormal::computeDesiredFeatures(const vpHomogeneousMatrix &cMo, unsigned int width, + unsigned int height, const vpMatrix &point_cloud, + vpColVector &desired_features, unsigned int stepX, unsigned int stepY +#if DEBUG_DISPLAY_DEPTH_NORMAL + , + vpImage &debugImage, + std::vector > &roiPts_vec +#endif + , + const vpImage *mask) +{ + m_faceActivated = false; + + if (width == 0 || height == 0) + return false; + + std::vector roiPts; + vpColVector desired_normal(3); + + computeROI(cMo, width, height, roiPts +#if DEBUG_DISPLAY_DEPTH_NORMAL + , + roiPts_vec +#endif + ); + + if (roiPts.size() <= 2) { +#ifndef NDEBUG + std::cerr << "Error: roiPts.size() <= 2 in computeDesiredFeatures" << std::endl; +#endif + return false; + } + + vpPolygon polygon_2d(roiPts); + vpRect bb = polygon_2d.getBoundingBox(); + + unsigned int top = (unsigned int)std::max(0.0, bb.getTop()); + unsigned int bottom = (unsigned int)std::min((double)height, std::max(0.0, bb.getBottom())); + unsigned int left = (unsigned int)std::max(0.0, bb.getLeft()); + unsigned int right = (unsigned int)std::min((double)width, std::max(0.0, bb.getRight())); + + bb.setTop(top); + bb.setBottom(bottom); + bb.setLeft(left); + bb.setRight(right); + + // Keep only 3D points inside the projected polygon face + std::vector point_cloud_face, point_cloud_face_custom; + + point_cloud_face.reserve((size_t)(3 * bb.getWidth() * bb.getHeight())); + if (m_featureEstimationMethod == ROBUST_FEATURE_ESTIMATION) { + point_cloud_face_custom.reserve((size_t)(3 * bb.getWidth() * bb.getHeight())); + } + + bool checkSSE2 = vpCPUFeatures::checkSSE2(); +#if !USE_SSE + checkSSE2 = false; +#else + bool push = false; + double prev_x, prev_y, prev_z; +#endif + + double x = 0.0, y = 0.0; + for (unsigned int i = top; i < bottom; i += stepY) { + for (unsigned int j = left; j < right; j += stepX) { + if (vpMeTracker::inMask(mask, i, j) && point_cloud[i * width + j][2] > 0 && + (m_useScanLine ? (i < m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs().getHeight() && + j < m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs().getWidth() && + m_hiddenFace->getMbScanLineRenderer().getPrimitiveIDs()[i][j] == m_polygon->getIndex()) + : polygon_2d.isInside(vpImagePoint(i, j)))) { +// Add point + point_cloud_face.push_back(point_cloud[i * width + j][0]); + point_cloud_face.push_back(point_cloud[i * width + j][1]); + point_cloud_face.push_back(point_cloud[i * width + j][2]); + + if (m_featureEstimationMethod == ROBUST_FEATURE_ESTIMATION) { + // Add point for custom method for plane equation estimation + vpPixelMeterConversion::convertPoint(m_cam, j, i, x, y); + + if (checkSSE2) { +#if USE_SSE + if (!push) { + push = true; + prev_x = x; + prev_y = y; + prev_z = point_cloud[i * width + j][2]; + } + else { + push = false; + point_cloud_face_custom.push_back(prev_x); + point_cloud_face_custom.push_back(x); + + point_cloud_face_custom.push_back(prev_y); + point_cloud_face_custom.push_back(y); + + point_cloud_face_custom.push_back(prev_z); + point_cloud_face_custom.push_back(point_cloud[i * width + j][2]); + } +#endif + } + else { + point_cloud_face_custom.push_back(x); + point_cloud_face_custom.push_back(y); + point_cloud_face_custom.push_back(point_cloud[i * width + j][2]); + } + } + +#if DEBUG_DISPLAY_DEPTH_NORMAL + debugImage[i][j] = 255; +#endif + } + } + } + +#if USE_SSE + if (checkSSE2 && push) { + point_cloud_face_custom.push_back(prev_x); + point_cloud_face_custom.push_back(prev_y); + point_cloud_face_custom.push_back(prev_z); + } +#endif + + if (point_cloud_face.empty() && point_cloud_face_custom.empty()) { + return false; + } + + // Face centroid computed by the different methods + vpColVector centroid_point(3); + +#ifdef VISP_HAVE_PCL + if (m_featureEstimationMethod == PCL_PLANE_ESTIMATION) { + pcl::PointCloud::Ptr point_cloud_face_pcl(new pcl::PointCloud); + point_cloud_face_pcl->reserve(point_cloud_face.size() / 3); + + for (size_t i = 0; i < point_cloud_face.size() / 3; i++) { + point_cloud_face_pcl->push_back( + pcl::PointXYZ(point_cloud_face[3 * i], point_cloud_face[3 * i + 1], point_cloud_face[3 * i + 2])); + } + + computeDesiredFeaturesPCL(point_cloud_face_pcl, desired_features, desired_normal, centroid_point); + } + else +#endif + if (m_featureEstimationMethod == ROBUST_SVD_PLANE_ESTIMATION) { + computeDesiredFeaturesSVD(point_cloud_face, cMo, desired_features, desired_normal, centroid_point); + } + else if (m_featureEstimationMethod == ROBUST_FEATURE_ESTIMATION) { + computeDesiredFeaturesRobustFeatures(point_cloud_face_custom, point_cloud_face, cMo, desired_features, + desired_normal, centroid_point); + } + else { + throw vpException(vpException::badValue, "Unknown feature estimation method!"); + } + + computeDesiredNormalAndCentroid(cMo, desired_normal, centroid_point); + + m_faceActivated = true; + + return true; +} #ifdef VISP_HAVE_PCL bool vpMbtFaceDepthNormal::computeDesiredFeaturesPCL(const pcl::PointCloud::ConstPtr &point_cloud_face, vpColVector &desired_features, vpColVector &desired_normal, diff --git a/modules/tracker/mbt/src/vpMbGenericTracker.cpp b/modules/tracker/mbt/src/vpMbGenericTracker.cpp index 830ff1577b..f15d2e2b03 100644 --- a/modules/tracker/mbt/src/vpMbGenericTracker.cpp +++ b/modules/tracker/mbt/src/vpMbGenericTracker.cpp @@ -1848,13 +1848,13 @@ void vpMbGenericTracker::initCircle(const vpPoint & /*p1*/, const vpPoint & /*p2 The structure of this file is the following: - \code + \verbatim # 3D point coordinates 4 # Number of points in the file (minimum is four) 0.01 0.01 0.01 # \ ... # | 3D coordinates in the object frame (X, Y, Z) 0.01 -0.01 -0.01 # / - \endcode + \endverbatim \param I1 : Input grayscale image for the first camera. \param I2 : Input grayscale image for the second camera. @@ -1918,13 +1918,13 @@ void vpMbGenericTracker::initClick(const vpImage &I1, const vpIma The structure of this file is the following: - \code + \verbatim # 3D point coordinates 4 # Number of points in the file (minimum is four) 0.01 0.01 0.01 # \ ... # | 3D coordinates in the object frame (X, Y, Z) 0.01 -0.01 -0.01 # / - \endcode + \endverbatim \param I_color1 : Input color image for the first camera. \param I_color2 : Input color image for the second camera. @@ -1988,13 +1988,13 @@ void vpMbGenericTracker::initClick(const vpImage &I_color1, const vpImag The structure of this file is the following: - \code + \verbatim # 3D point coordinates 4 # Number of points in the file (minimum is four) 0.01 0.01 0.01 # \ ... # | 3D coordinates in the object frame (X, Y, Z) 0.01 -0.01 -0.01 # / - \endcode + \endverbatim The cameras that have not an init file will be automatically initialized but the camera transformation matrices have to be set before. @@ -2093,13 +2093,13 @@ void vpMbGenericTracker::initClick(const std::map &I1, const with X, Y and Z values. 2D point coordinates are expressied in pixel coordinates, with first the line and then the column of the pixel in the image. The structure of this file is the following. - \code + \verbatim # 3D point coordinates 4 # Number of 3D points in the file (minimum is four) 0.01 0.01 0.01 # \ @@ -2285,7 +2285,7 @@ void vpMbGenericTracker::initFromPoints(const vpImage &I1, const 100 200 # \ ... # | 2D coordinates in pixel in the image 50 10 # / - \endcode + \endverbatim \param I_color1 : Input color image for the first camera. \param I_color2 : Input color image for the second camera. @@ -3276,6 +3276,19 @@ void vpMbGenericTracker::preTracking(std::map *> &mapOfImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights) +{ + for (std::map::const_iterator it = m_mapOfTrackers.begin(); + it != m_mapOfTrackers.end(); ++it) { + TrackerWrapper *tracker = it->second; + tracker->preTracking(mapOfImages[it->first], mapOfPointClouds[it->first], mapOfPointCloudWidths[it->first], + mapOfPointCloudHeights[it->first]); + } +} + /*! Re-initialize the model used by the tracker. @@ -5754,6 +5767,166 @@ void vpMbGenericTracker::track(std::map *> &m computeProjectionError(); } +void vpMbGenericTracker::track(std::map *> &mapOfImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights) +{ + for (std::map::const_iterator it = m_mapOfTrackers.begin(); + it != m_mapOfTrackers.end(); ++it) { + TrackerWrapper *tracker = it->second; + + if ((tracker->m_trackerType & (EDGE_TRACKER | +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + KLT_TRACKER | +#endif + DEPTH_NORMAL_TRACKER | DEPTH_DENSE_TRACKER)) == 0) { + throw vpException(vpException::fatalError, "Bad tracker type: %d", tracker->m_trackerType); + } + + if (tracker->m_trackerType & (EDGE_TRACKER +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + | KLT_TRACKER +#endif + ) && + mapOfImages[it->first] == nullptr) { + throw vpException(vpException::fatalError, "Image pointer is nullptr!"); + } + + if (tracker->m_trackerType & (DEPTH_NORMAL_TRACKER | DEPTH_DENSE_TRACKER) && + (mapOfPointClouds[it->first] == nullptr)) { + throw vpException(vpException::fatalError, "Pointcloud is nullptr!"); + } + } + + preTracking(mapOfImages, mapOfPointClouds, mapOfPointCloudWidths, mapOfPointCloudHeights); + + try { + computeVVS(mapOfImages); + } + catch (...) { + covarianceMatrix = -1; + throw; // throw the original exception + } + + testTracking(); + + for (std::map::const_iterator it = m_mapOfTrackers.begin(); + it != m_mapOfTrackers.end(); ++it) { + TrackerWrapper *tracker = it->second; + + if (tracker->m_trackerType & EDGE_TRACKER && displayFeatures) { + tracker->m_featuresToBeDisplayedEdge = tracker->getFeaturesForDisplayEdge(); + } + + tracker->postTracking(mapOfImages[it->first], mapOfPointCloudWidths[it->first], mapOfPointCloudHeights[it->first]); + + if (displayFeatures) { +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + if (tracker->m_trackerType & KLT_TRACKER) { + tracker->m_featuresToBeDisplayedKlt = tracker->getFeaturesForDisplayKlt(); + } +#endif + + if (tracker->m_trackerType & DEPTH_NORMAL_TRACKER) { + tracker->m_featuresToBeDisplayedDepthNormal = tracker->getFeaturesForDisplayDepthNormal(); + } + } + } + + computeProjectionError(); +} + +/*! + Realize the tracking of the object in the image. + + \throw vpException : if the tracking is supposed to have failed + + \param mapOfColorImages : Map of images. + \param mapOfPointClouds : Map of pointclouds. + \param mapOfPointCloudWidths : Map of pointcloud widths. + \param mapOfPointCloudHeights : Map of pointcloud heights. +*/ +void vpMbGenericTracker::track(std::map *> &mapOfColorImages, + std::map &mapOfPointClouds, + std::map &mapOfPointCloudWidths, + std::map &mapOfPointCloudHeights) +{ + std::map *> mapOfImages; + for (std::map::const_iterator it = m_mapOfTrackers.begin(); + it != m_mapOfTrackers.end(); ++it) { + TrackerWrapper *tracker = it->second; + + if ((tracker->m_trackerType & (EDGE_TRACKER | +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + KLT_TRACKER | +#endif + DEPTH_NORMAL_TRACKER | DEPTH_DENSE_TRACKER)) == 0) { + throw vpException(vpException::fatalError, "Bad tracker type: %d", tracker->m_trackerType); + } + + if (tracker->m_trackerType & (EDGE_TRACKER +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + | KLT_TRACKER +#endif + ) && + mapOfColorImages[it->first] == nullptr) { + throw vpException(vpException::fatalError, "Image pointer is nullptr!"); + } + else if (tracker->m_trackerType & (EDGE_TRACKER +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + | KLT_TRACKER +#endif + ) && + mapOfColorImages[it->first] != nullptr) { + vpImageConvert::convert(*mapOfColorImages[it->first], tracker->m_I); + mapOfImages[it->first] = &tracker->m_I; // update grayscale image buffer + } + + if (tracker->m_trackerType & (DEPTH_NORMAL_TRACKER | DEPTH_DENSE_TRACKER) && + (mapOfPointClouds[it->first] == nullptr)) { + throw vpException(vpException::fatalError, "Pointcloud is nullptr!"); + } + } + + preTracking(mapOfImages, mapOfPointClouds, mapOfPointCloudWidths, mapOfPointCloudHeights); + + try { + computeVVS(mapOfImages); + } + catch (...) { + covarianceMatrix = -1; + throw; // throw the original exception + } + + testTracking(); + + for (std::map::const_iterator it = m_mapOfTrackers.begin(); + it != m_mapOfTrackers.end(); ++it) { + TrackerWrapper *tracker = it->second; + + if (tracker->m_trackerType & EDGE_TRACKER && displayFeatures) { + tracker->m_featuresToBeDisplayedEdge = tracker->getFeaturesForDisplayEdge(); + } + + tracker->postTracking(mapOfImages[it->first], mapOfPointCloudWidths[it->first], mapOfPointCloudHeights[it->first]); + + if (displayFeatures) { +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + if (tracker->m_trackerType & KLT_TRACKER) { + tracker->m_featuresToBeDisplayedKlt = tracker->getFeaturesForDisplayKlt(); + } +#endif + + if (tracker->m_trackerType & DEPTH_NORMAL_TRACKER) { + tracker->m_featuresToBeDisplayedDepthNormal = tracker->getFeaturesForDisplayDepthNormal(); + } + } + } + + computeProjectionError(); +} + /** TrackerWrapper **/ vpMbGenericTracker::TrackerWrapper::TrackerWrapper() : m_error(), m_L(), m_trackerType(EDGE_TRACKER), m_w(), m_weightedError() @@ -6831,6 +7004,54 @@ void vpMbGenericTracker::TrackerWrapper::preTracking(const vpImage *const ptr_I, + const vpMatrix *const point_cloud, + const unsigned int pointcloud_width, + const unsigned int pointcloud_height) +{ + if (m_trackerType & EDGE_TRACKER) { + try { + vpMbEdgeTracker::trackMovingEdge(*ptr_I); + } + catch (...) { + std::cerr << "Error in moving edge tracking" << std::endl; + throw; + } + } + +#if defined(VISP_HAVE_MODULE_KLT) && defined(VISP_HAVE_OPENCV) && defined(HAVE_OPENCV_IMGPROC) && defined(HAVE_OPENCV_VIDEO) + if (m_trackerType & KLT_TRACKER) { + try { + vpMbKltTracker::preTracking(*ptr_I); + } + catch (const vpException &e) { + std::cerr << "Error in KLT tracking: " << e.what() << std::endl; + throw; + } + } +#endif + + if (m_trackerType & DEPTH_NORMAL_TRACKER) { + try { + vpMbDepthNormalTracker::segmentPointCloud(*point_cloud, pointcloud_width, pointcloud_height); + } + catch (...) { + std::cerr << "Error in Depth tracking" << std::endl; + throw; + } + } + + if (m_trackerType & DEPTH_DENSE_TRACKER) { + try { + vpMbDepthDenseTracker::segmentPointCloud(*point_cloud, pointcloud_width, pointcloud_height); + } + catch (...) { + std::cerr << "Error in Depth dense tracking" << std::endl; + throw; + } + } +} + void vpMbGenericTracker::TrackerWrapper::reInitModel(const vpImage *const I, const vpImage *const I_color, const std::string &cad_name, const vpHomogeneousMatrix &cMo, bool verbose, diff --git a/modules/tracker/mbt/src/vpMbTracker.cpp b/modules/tracker/mbt/src/vpMbTracker.cpp index 45a27cd8f4..aef55b010c 100644 --- a/modules/tracker/mbt/src/vpMbTracker.cpp +++ b/modules/tracker/mbt/src/vpMbTracker.cpp @@ -570,13 +570,13 @@ void vpMbTracker::initClick(const vpImage *const I, const vpImage The structure of this file is the following: - \code + \verbatim # 3D point coordinates 4 # Number of points in the file (minimum is four) 0.01 0.01 0.01 # \ ... # | 3D coordinates in the object frame (X, Y, Z) 0.01 -0.01 -0.01 # / - \endcode + \endverbatim \param I : Input grayscale image where the user has to click. \param initFile : File containing the coordinates of at least 4 3D points @@ -607,13 +607,13 @@ void vpMbTracker::initClick(const vpImage &I, const std::string & The structure of this file is the following: - \code + \verbatim # 3D point coordinates 4 # Number of points in the file (minimum is four) 0.01 0.01 0.01 # \ ... # | 3D coordinates in the object frame (X, Y, Z) 0.01 -0.01 -0.01 # / - \endcode + \endverbatim \param I_color : Input color image where the user has to click. \param initFile : File containing the coordinates of at least 4 3D points @@ -965,7 +965,7 @@ void vpMbTracker::initFromPoints(const vpImage *const I, const vp with X, Y and Z values. 2D point coordinates are expressied in pixel coordinates, with first the line and then the column of the pixel in the image. The structure of this file is the following. - \code + \verbatim # 3D point coordinates 4 # Number of 3D points in the file (minimum is four) 0.01 0.01 0.01 # \ @@ -977,7 +977,7 @@ void vpMbTracker::initFromPoints(const vpImage *const I, const vp 100 200 # \ ... # | 2D coordinates in pixel in the image 50 10 # / - \endcode + \endverbatim \param I : Input grayscale image \param initFile : Path to the file containing all the points. @@ -994,7 +994,7 @@ void vpMbTracker::initFromPoints(const vpImage &I, const std::str with X, Y and Z values. 2D point coordinates are expressied in pixel coordinates, with first the line and then the column of the pixel in the image. The structure of this file is the following. - \code + \verbatim # 3D point coordinates 4 # Number of 3D points in the file (minimum is four) 0.01 0.01 0.01 # \ @@ -1006,7 +1006,7 @@ void vpMbTracker::initFromPoints(const vpImage &I, const std::str 100 200 # \ ... # | 2D coordinates in pixel in the image 50 10 # / - \endcode + \endverbatim \param I_color : Input color image \param initFile : Path to the file containing all the points. diff --git a/modules/tracker/me/include/visp3/me/vpMeNurbs.h b/modules/tracker/me/include/visp3/me/vpMeNurbs.h index c6f9d5ec28..de056e67f1 100644 --- a/modules/tracker/me/include/visp3/me/vpMeNurbs.h +++ b/modules/tracker/me/include/visp3/me/vpMeNurbs.h @@ -242,13 +242,6 @@ class VISP_EXPORT vpMeNurbs : public vpMeTracker */ void updateDelta(); - /*! - * Seek along the edge defined by the nurbs, the two extremities of - * the edge. This function is useful in case of translation of the - * edge. - */ - void setExtremities(); - /*! * Seek along the edge defined by the nurbs, the two extremities of * the edge. This function is useful in case of translation of the diff --git a/modules/tracker/tt_mi/include/visp3/tt_mi/vpTemplateTrackerMIESM.h b/modules/tracker/tt_mi/include/visp3/tt_mi/vpTemplateTrackerMIESM.h index 570ac88289..4f92ef50a0 100644 --- a/modules/tracker/tt_mi/include/visp3/tt_mi/vpTemplateTrackerMIESM.h +++ b/modules/tracker/tt_mi/include/visp3/tt_mi/vpTemplateTrackerMIESM.h @@ -52,8 +52,10 @@ */ class VISP_EXPORT vpTemplateTrackerMIESM : public vpTemplateTrackerMI { +public: /*! Minimization method. */ - typedef enum { + typedef enum + { USE_NEWTON, // not used USE_LMA, // not used USE_GRADIENT, @@ -96,9 +98,8 @@ class VISP_EXPORT vpTemplateTrackerMIESM : public vpTemplateTrackerMI //! Default constructor. vpTemplateTrackerMIESM() : vpTemplateTrackerMI(), minimizationMethod(USE_NEWTON), CompoInitialised(false), HDirect(), HInverse(), - HdesireDirect(), HdesireInverse(), GDirect(), GInverse() - { - } + HdesireDirect(), HdesireInverse(), GDirect(), GInverse() + { } explicit vpTemplateTrackerMIESM(vpTemplateTrackerWarp *_warp); void setMinimizationMethod(vpMinimizationTypeMIESM method) { minimizationMethod = method; } diff --git a/modules/visual_features/include/visp3/visual_features/vpBasicFeature.h b/modules/visual_features/include/visp3/visual_features/vpBasicFeature.h index 7c61681309..7073877c3a 100644 --- a/modules/visual_features/include/visp3/visual_features/vpBasicFeature.h +++ b/modules/visual_features/include/visp3/visual_features/vpBasicFeature.h @@ -78,7 +78,7 @@ class VISP_EXPORT vpBasicFeature public: static const unsigned int FEATURE_LINE[32]; - enum { FEATURE_ALL = 0xffff }; + enum vpBasicFeatureSelect { FEATURE_ALL = 0xffff }; /*! * \enum vpBasicFeatureDeallocatorType * Indicates who should deallocate the feature. diff --git a/modules/visual_features/include/visp3/visual_features/vpFeatureVanishingPoint.h b/modules/visual_features/include/visp3/visual_features/vpFeatureVanishingPoint.h index e4da3cb7cb..be90bfa0fb 100644 --- a/modules/visual_features/include/visp3/visual_features/vpFeatureVanishingPoint.h +++ b/modules/visual_features/include/visp3/visual_features/vpFeatureVanishingPoint.h @@ -88,7 +88,7 @@ class VISP_EXPORT vpFeatureVanishingPoint : public vpBasicFeature vpFeatureVanishingPoint *duplicate() const override; - vpColVector error(const vpBasicFeature &s_star, unsigned int select = (selectX() | selectY())) override; + vpColVector error(const vpBasicFeature &s_star, unsigned int select = (vpFeatureVanishingPoint::selectX() | vpFeatureVanishingPoint::selectY())) override; double get_x() const; double get_y() const; @@ -97,9 +97,9 @@ class VISP_EXPORT vpFeatureVanishingPoint : public vpBasicFeature double getAlpha() const; void init() override; - vpMatrix interaction(unsigned int select = (selectX() | selectY())) override; + vpMatrix interaction(unsigned int select = (vpFeatureVanishingPoint::selectX() | vpFeatureVanishingPoint::selectY())) override; - void print(unsigned int select = (selectX() | selectY())) const override; + void print(unsigned int select = (vpFeatureVanishingPoint::selectX() | vpFeatureVanishingPoint::selectY())) const override; void set_x(double x); void set_y(double y);