vendor: OpenCV 5.0.0 snapshot at 40738fb16ceddb5fb3fea747585f7ce6abb0605b

This commit is contained in:
Gitea Mirror Bot
2026-08-22 00:10:33 +08:00
commit f7f077da11
6933 changed files with 2335208 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# ----------------------------------------------------------------------------
# CMake file for python support
# ----------------------------------------------------------------------------
if(DEFINED OPENCV_INITIAL_PASS) # OpenCV build
if(ANDROID OR APPLE_FRAMEWORK OR WINRT)
ocv_module_disable_(python3)
return()
elseif(BUILD_opencv_world OR (WIN32 AND CMAKE_BUILD_TYPE STREQUAL "Debug"))
if(NOT DEFINED BUILD_opencv_python3)
set(__disable_python3 ON)
endif()
endif()
add_subdirectory(bindings)
add_subdirectory(test)
if(__disable_python3)
ocv_module_disable_(python3)
return()
endif()
add_subdirectory(python3)
else() # standalone build
cmake_minimum_required(VERSION 3.13)
project(OpenCVPython CXX C)
include("./standalone.cmake")
endif()
+182
View File
@@ -0,0 +1,182 @@
set(MODULE_NAME "python_bindings_generator")
set(OPENCV_MODULE_IS_PART_OF_WORLD FALSE)
ocv_add_module(${MODULE_NAME} INTERNAL)
set(OPENCV_PYTHON_SIGNATURES_FILE "${CMAKE_CURRENT_BINARY_DIR}/pyopencv_signatures.json" CACHE INTERNAL "")
set(OPENCV_PYTHON_BINDINGS_DIR "${CMAKE_CURRENT_BINARY_DIR}" CACHE INTERNAL "")
# This file is included from a subdirectory
set(PYTHON_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../")
if(NOT OPENCV_SKIP_PYTHON_LOADER)
include("${PYTHON_SOURCE_DIR}/python_loader.cmake")
endif()
# get list of modules to wrap
set(OPENCV_PYTHON_MODULES)
foreach(m ${OPENCV_MODULES_BUILD})
if (";${OPENCV_MODULE_${m}_WRAPPERS};" MATCHES ";python;" AND HAVE_${m})
list(APPEND OPENCV_PYTHON_MODULES ${m})
#message(STATUS "\t${m}")
endif()
endforeach()
set(opencv_hdrs "")
set(opencv_userdef_hdrs "")
foreach(m ${OPENCV_PYTHON_MODULES})
foreach (hdr ${OPENCV_MODULE_${m}_HEADERS})
ocv_is_subdir(is_sub "${OPENCV_MODULE_${m}_LOCATION}/include" "${hdr}")
if(is_sub)
list(APPEND opencv_hdrs "${hdr}")
endif()
endforeach()
# both wrapping and C++ implementation
file(GLOB hdr2 ${OPENCV_MODULE_${m}_LOCATION}/misc/python/python_*.hpp)
list(SORT hdr2)
list(APPEND opencv_hdrs ${hdr2})
list(APPEND opencv_userdef_hdrs ${hdr2})
file(GLOB hdr ${OPENCV_MODULE_${m}_LOCATION}/misc/python/shadow*.hpp)
list(SORT hdr)
list(APPEND opencv_hdrs ${hdr})
file(GLOB userdef_hdrs ${OPENCV_MODULE_${m}_LOCATION}/misc/python/pyopencv*.hpp)
list(SORT userdef_hdrs)
list(APPEND opencv_userdef_hdrs ${userdef_hdrs})
endforeach(m)
# header blacklist
ocv_list_filterout(opencv_hdrs "modules/.*\\\\.h$")
ocv_list_filterout(opencv_hdrs "modules/core/include/opencv2/core/fast_math.hpp")
ocv_list_filterout(opencv_hdrs "modules/core/.*/cuda/")
ocv_list_filterout(opencv_hdrs "modules/core/.*/hal/")
ocv_list_filterout(opencv_hdrs "modules/core/.*/opencl/")
ocv_list_filterout(opencv_hdrs "modules/.+/utils/trace.hpp")
ocv_list_filterout(opencv_hdrs "modules/.*\\\\.inl\\\\.h*")
ocv_list_filterout(opencv_hdrs "modules/.*_inl\\\\.h*")
ocv_list_filterout(opencv_hdrs "modules/.*\\\\.details\\\\.h*")
ocv_list_filterout(opencv_hdrs "modules/.*\\\\.private\\\\.h*")
ocv_list_filterout(opencv_hdrs "modules/.*/private\\\\.h*")
ocv_list_filterout(opencv_hdrs "modules/.*/legacy/.*")
ocv_list_filterout(opencv_hdrs "modules/.*/detection_based_tracker\\\\.hpp") # Conditional compilation
if(NOT HAVE_CUDA)
ocv_list_filterout(opencv_hdrs "modules/cuda.*")
ocv_list_filterout(opencv_hdrs "modules/cudev")
endif()
set(cv2_generated_files
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_enums.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_funcs.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_include.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_modules.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_modules_content.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_types.h"
"${CMAKE_CURRENT_BINARY_DIR}/pyopencv_generated_types_content.h"
"${OPENCV_PYTHON_SIGNATURES_FILE}"
)
set(config_json_headers_list "")
foreach(header IN LISTS opencv_hdrs)
if(NOT config_json_headers_list STREQUAL "")
set(config_json_headers_list "${config_json_headers_list},\n\"${header}\"")
else()
set(config_json_headers_list "\"${header}\"")
endif()
endforeach()
include("${OpenCV_SOURCE_DIR}/cmake/OpenCVBindingsPreprocessorDefinitions.cmake")
ocv_bindings_generator_populate_preprocessor_definitions(
OPENCV_MODULES_BUILD
opencv_preprocessor_defs
)
set(__config_str
"{
\"headers\": [
${config_json_headers_list}
],
\"preprocessor_definitions\": {
${opencv_preprocessor_defs}
}
}")
set(JSON_CONFIG_FILE_PATH "${CMAKE_CURRENT_BINARY_DIR}/gen_python_config.json")
if(EXISTS "${JSON_CONFIG_FILE_PATH}")
file(READ "${JSON_CONFIG_FILE_PATH}" __content)
else()
set(__content "")
endif()
if(NOT "${__content}" STREQUAL "${__config_str}")
file(WRITE "${JSON_CONFIG_FILE_PATH}" "${__config_str}")
endif()
unset(__config_str)
file(GLOB_RECURSE typing_stubs_generation_files "${PYTHON_SOURCE_DIR}/src2/typing_stubs_generation/*.py")
add_custom_command(
OUTPUT ${cv2_generated_files}
COMMAND "${PYTHON_DEFAULT_EXECUTABLE}" "${PYTHON_SOURCE_DIR}/src2/gen2.py"
"--config" "${JSON_CONFIG_FILE_PATH}"
"--output_dir" "${CMAKE_CURRENT_BINARY_DIR}"
DEPENDS "${PYTHON_SOURCE_DIR}/src2/gen2.py"
"${PYTHON_SOURCE_DIR}/src2/hdr_parser.py"
"${typing_stubs_generation_files}"
"${PYTHON_SOURCE_DIR}/src2/typing_stubs_generator.py"
# not a real build dependency (file(WRITE) result): ${CMAKE_CURRENT_BINARY_DIR}/headers.txt
${opencv_hdrs}
COMMENT "Generate files for Python bindings and documentation"
)
add_custom_target(gen_opencv_python_source DEPENDS ${cv2_generated_files})
if(TARGET copy_opencv_typing_stubs)
add_dependencies(copy_opencv_typing_stubs gen_opencv_python_source)
endif()
set(cv2_custom_hdr "${CMAKE_CURRENT_BINARY_DIR}/pyopencv_custom_headers.h")
set(cv2_custom_hdr_str "//user-defined headers\n")
foreach(uh ${opencv_userdef_hdrs})
set(cv2_custom_hdr_str "${cv2_custom_hdr_str}#include \"${uh}\"\n")
endforeach(uh)
if(EXISTS "${cv2_custom_hdr}")
file(READ "${cv2_custom_hdr}" __content)
else()
set(__content "")
endif()
if("${__content}" STREQUAL "${cv2_custom_hdr_str}")
# Up-to-date
else()
file(WRITE "${cv2_custom_hdr}" "${cv2_custom_hdr_str}")
endif()
unset(__content)
#
# Configuration for standalone build of Python bindings
#
set(PYTHON_CONFIG_SCRIPT "")
ocv_cmake_script_append_var(PYTHON_CONFIG_SCRIPT
CMAKE_BUILD_TYPE
BUILD_SHARED_LIBS
CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE
CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
CV_GCC CV_CLANG ENABLE_NOISY_WARNINGS
CMAKE_MODULE_LINKER_FLAGS
CMAKE_INSTALL_PREFIX
OPENCV_PYTHON_INSTALL_PATH
OpenCV_SOURCE_DIR
OPENCV_FORCE_PYTHON_LIBS
OPENCV_PYTHON_SKIP_LINKER_EXCLUDE_LIBS
OPENCV_PYTHON_BINDINGS_DIR
cv2_custom_hdr
cv2_generated_files
)
set(CMAKE_HELPER_SCRIPT "${CMAKE_BINARY_DIR}/opencv_python_config.cmake")
file(GENERATE OUTPUT "${CMAKE_HELPER_SCRIPT}" CONTENT "${PYTHON_CONFIG_SCRIPT}")
+244
View File
@@ -0,0 +1,244 @@
# This file is included from a subdirectory
set(PYTHON_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
ocv_add_module(${MODULE_NAME} BINDINGS PRIVATE_REQUIRED opencv_python_bindings_generator)
include_directories(SYSTEM
"${${PYTHON}_INCLUDE_PATH}"
${${PYTHON}_NUMPY_INCLUDE_DIRS}
)
ocv_module_include_directories(
"${PYTHON_SOURCE_DIR}/src2"
"${OPENCV_PYTHON_BINDINGS_DIR}"
)
# try to use dynamic symbols linking with libpython.so
set(OPENCV_FORCE_PYTHON_LIBS OFF CACHE BOOL "")
string(REGEX REPLACE "(^| )-Wl,--no-undefined( |$)" " " CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS}")
if(NOT WIN32 AND NOT APPLE AND NOT OPENCV_PYTHON_SKIP_LINKER_EXCLUDE_LIBS)
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--exclude-libs=ALL")
endif()
ocv_add_library(${the_module} MODULE
${PYTHON_SOURCE_DIR}/src2/cv2.cpp
${PYTHON_SOURCE_DIR}/src2/cv2_util.cpp
${PYTHON_SOURCE_DIR}/src2/cv2_numpy.cpp
${PYTHON_SOURCE_DIR}/src2/cv2_convert.cpp
${PYTHON_SOURCE_DIR}/src2/cv2_highgui.cpp
${cv2_generated_hdrs}
${opencv_userdef_hdrs}
${cv2_custom_hdr}
)
if(TARGET gen_opencv_python_source)
add_dependencies(${the_module} gen_opencv_python_source)
endif()
if(TARGET copy_opencv_typing_stubs)
# Python 3.6+
add_dependencies(${the_module} copy_opencv_typing_stubs)
endif()
ocv_assert(${PYTHON}_VERSION_MAJOR)
ocv_assert(${PYTHON}_VERSION_MINOR)
if(${PYTHON}_LIMITED_API)
# support only python3.3+
ocv_assert(${PYTHON}_VERSION_MAJOR EQUAL 3 AND ${PYTHON}_VERSION_MINOR GREATER 2)
target_compile_definitions(${the_module} PRIVATE CVPY_DYNAMIC_INIT)
target_compile_definitions(${the_module} PRIVATE PYTHON3_LIMITED_API_VERSION=${PYTHON3_LIMITED_API_VERSION})
if(WIN32)
string(REPLACE
"python${${PYTHON}_VERSION_MAJOR}${${PYTHON}_VERSION_MINOR}.lib"
"python${${PYTHON}_VERSION_MAJOR}.lib"
${PYTHON}_LIBRARIES
"${${PYTHON}_LIBRARIES}")
endif()
endif()
if(APPLE)
set_target_properties(${the_module} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
elseif(WIN32 OR OPENCV_FORCE_PYTHON_LIBS)
if(${PYTHON}_DEBUG_LIBRARIES AND NOT ${PYTHON}_LIBRARIES MATCHES "optimized.*debug")
ocv_target_link_libraries(${the_module} PRIVATE ${${PYTHON}_LIBRARIES})
ocv_target_link_libraries(${the_module} PRIVATE debug ${${PYTHON}_DEBUG_LIBRARIES} optimized ${${PYTHON}_LIBRARIES})
else()
ocv_target_link_libraries(${the_module} PRIVATE ${${PYTHON}_LIBRARIES})
endif()
endif()
if(TARGET gen_opencv_python_source)
set(deps ${OPENCV_MODULE_${the_module}_DEPS})
list(REMOVE_ITEM deps opencv_python_bindings_generator) # don't add dummy module to target_link_libraries list
endif()
ocv_target_link_libraries(${the_module} PRIVATE ${deps})
if(DEFINED ${PYTHON}_CVPY_SUFFIX)
set(CVPY_SUFFIX "${${PYTHON}_CVPY_SUFFIX}")
else()
set(__python_ext_suffix_var "EXT_SUFFIX")
if("${${PYTHON}_VERSION_MAJOR}" STREQUAL "2")
set(__python_ext_suffix_var "SO")
endif()
execute_process(COMMAND ${${PYTHON}_EXECUTABLE} -c "import sysconfig; print(sysconfig.get_config_var('${__python_ext_suffix_var}'))"
RESULT_VARIABLE PYTHON_CVPY_PROCESS
OUTPUT_VARIABLE CVPY_SUFFIX
OUTPUT_STRIP_TRAILING_WHITESPACE)
if(NOT PYTHON_CVPY_PROCESS EQUAL 0)
set(CVPY_SUFFIX ".so")
endif()
if(${PYTHON}_LIMITED_API)
if(WIN32)
string(REGEX REPLACE "\\.[^\\.]*\\." "." CVPY_SUFFIX "${CVPY_SUFFIX}")
else()
string(REGEX REPLACE "\\.[^\\.]*\\." ".abi${${PYTHON}_VERSION_MAJOR}." CVPY_SUFFIX "${CVPY_SUFFIX}")
endif()
endif()
endif()
ocv_update(OPENCV_PYTHON_EXTENSION_BUILD_PATH "${LIBRARY_OUTPUT_PATH}/${MODULE_INSTALL_SUBDIR}")
set_target_properties(${the_module} PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${OPENCV_PYTHON_EXTENSION_BUILD_PATH}"
ARCHIVE_OUTPUT_NAME ${the_module} # prevent name conflict for python2/3 outputs
PREFIX ""
OUTPUT_NAME cv2
SUFFIX "${CVPY_SUFFIX}")
if(ENABLE_SOLUTION_FOLDERS)
set_target_properties(${the_module} PROPERTIES FOLDER "bindings")
endif()
if(MSVC)
add_definitions(-DCVAPI_EXPORTS)
endif()
if((CV_GCC OR CV_CLANG) AND NOT ENABLE_NOISY_WARNINGS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-function")
endif()
if(MSVC AND NOT ENABLE_NOISY_WARNINGS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4100") #unreferenced formal parameter
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4127") #conditional expression is constant
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4505") #unreferenced local function has been removed
string(REPLACE "/W4" "/W3" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
endif()
if(MSVC)
ocv_warnings_disable(CMAKE_CXX_FLAGS /wd4996)
else()
ocv_warnings_disable(CMAKE_CXX_FLAGS
-Wdeprecated-declarations
-Woverloaded-virtual -Wunused-private-field
-Wundef # accurate guard via #pragma doesn't work (C++ preprocessor doesn't handle #pragma)
)
endif()
if(MSVC AND NOT BUILD_SHARED_LIBS)
set_target_properties(${the_module} PROPERTIES LINK_FLAGS "/NODEFAULTLIB:atlthunk.lib /NODEFAULTLIB:atlsd.lib /DEBUG")
endif()
if(MSVC AND NOT ${PYTHON}_DEBUG_LIBRARIES)
set(PYTHON_INSTALL_CONFIGURATIONS CONFIGURATIONS Release)
else()
set(PYTHON_INSTALL_CONFIGURATIONS "")
endif()
if(WIN32)
set(PYTHON_INSTALL_ARCHIVE "")
else()
set(PYTHON_INSTALL_ARCHIVE ARCHIVE DESTINATION ${${PYTHON}_PACKAGES_PATH} COMPONENT python)
endif()
set(__python_loader_subdir "")
if(NOT OPENCV_SKIP_PYTHON_LOADER)
set(__python_loader_subdir "cv2/")
endif()
if(NOT " ${PYTHON}" STREQUAL " PYTHON"
AND NOT DEFINED OPENCV_PYTHON_INSTALL_PATH
)
if(DEFINED OPENCV_${PYTHON}_INSTALL_PATH)
set(OPENCV_PYTHON_INSTALL_PATH "${OPENCV_${PYTHON}_INSTALL_PATH}")
elseif(NOT OPENCV_SKIP_PYTHON_LOADER)
set(OPENCV_PYTHON_INSTALL_PATH "${${PYTHON}_PACKAGES_PATH}")
endif()
endif()
if(NOT OPENCV_SKIP_PYTHON_LOADER AND DEFINED OPENCV_PYTHON_INSTALL_PATH)
include("${CMAKE_CURRENT_LIST_DIR}/python_loader.cmake")
set(OPENCV_PYTHON_INSTALL_PATH_SETUPVARS "${OPENCV_PYTHON_INSTALL_PATH}" CACHE INTERNAL "")
endif()
if(OPENCV_SKIP_PYTHON_LOADER)
if(DEFINED OPENCV_${PYTHON}_INSTALL_PATH)
set(__python_binary_install_path "${OPENCV_${PYTHON}_INSTALL_PATH}")
elseif(DEFINED ${PYTHON}_PACKAGES_PATH)
set(__python_binary_install_path "${${PYTHON}_PACKAGES_PATH}")
else()
message(FATAL_ERROR "Specify 'OPENCV_${PYTHON}_INSTALL_PATH' variable")
endif()
else()
ocv_assert(DEFINED OPENCV_PYTHON_INSTALL_PATH)
if(${PYTHON}_LIMITED_API)
set(__python_binary_subdir "python-${${PYTHON}_VERSION_MAJOR}")
else()
set(__python_binary_subdir "python-${${PYTHON}_VERSION_MAJOR}.${${PYTHON}_VERSION_MINOR}")
endif()
set(__python_binary_install_path "${OPENCV_PYTHON_INSTALL_PATH}/${__python_loader_subdir}${__python_binary_subdir}")
endif()
install(TARGETS ${the_module}
${PYTHON_INSTALL_CONFIGURATIONS}
RUNTIME DESTINATION "${__python_binary_install_path}" COMPONENT python
LIBRARY DESTINATION "${__python_binary_install_path}" COMPONENT python
${PYTHON_INSTALL_ARCHIVE}
)
set(__INSTALL_PATH_${PYTHON} "${__python_binary_install_path}" CACHE INTERNAL "") # CMake status
if(NOT OPENCV_SKIP_PYTHON_LOADER)
ocv_assert(DEFINED OPENCV_PYTHON_INSTALL_PATH)
if(OpenCV_FOUND)
set(__loader_path "${OpenCV_BINARY_DIR}/python_loader")
else()
set(__loader_path "${CMAKE_BINARY_DIR}/python_loader")
endif()
set(__python_loader_install_tmp_path "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/install/python_loader/")
set(OpenCV_PYTHON_LOADER_FULL_INSTALL_PATH "${CMAKE_INSTALL_PREFIX}/${OPENCV_PYTHON_INSTALL_PATH}/cv2")
if(IS_ABSOLUTE "${OPENCV_PYTHON_INSTALL_PATH}")
set(CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE "'${OPENCV_PYTHON_INSTALL_PATH}/cv2'")
else()
set(CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE "LOADER_DIR")
endif()
if(DEFINED ${PYTHON}_VERSION_MINOR AND NOT ${PYTHON}_LIMITED_API)
set(__target_config "config-${${PYTHON}_VERSION_MAJOR}.${${PYTHON}_VERSION_MINOR}.py")
else()
set(__target_config "config-${${PYTHON}_VERSION_MAJOR}.py")
endif()
if(CMAKE_GENERATOR MATCHES "Visual Studio")
set(CMAKE_PYTHON_EXTENSION_PATH "'${OPENCV_PYTHON_EXTENSION_BUILD_PATH}/Release'") # TODO: CMAKE_BUILD_TYPE is not defined
else()
set(CMAKE_PYTHON_EXTENSION_PATH "'${OPENCV_PYTHON_EXTENSION_BUILD_PATH}'")
endif()
configure_file("${PYTHON_SOURCE_DIR}/package/template/config-x.y.py.in" "${__loader_path}/cv2/${__target_config}" @ONLY)
if(IS_ABSOLUTE __python_binary_install_path)
set(CMAKE_PYTHON_EXTENSION_PATH "'${__python_binary_install_path}'")
else()
file(RELATIVE_PATH OpenCV_PYTHON_BINARY_RELATIVE_INSTALL_PATH "${OpenCV_PYTHON_LOADER_FULL_INSTALL_PATH}" "${CMAKE_INSTALL_PREFIX}/${__python_binary_install_path}")
set(CMAKE_PYTHON_EXTENSION_PATH "os.path.join(${CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE}, '${OpenCV_PYTHON_BINARY_RELATIVE_INSTALL_PATH}')")
endif()
configure_file("${PYTHON_SOURCE_DIR}/package/template/config-x.y.py.in" "${__python_loader_install_tmp_path}/cv2/${__target_config}" @ONLY)
install(FILES "${__python_loader_install_tmp_path}/cv2/${__target_config}" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/cv2/" COMPONENT python)
endif() # NOT OPENCV_SKIP_PYTHON_LOADER
unset(PYTHON_SRC_DIR)
unset(PYTHON_CVPY_PROCESS)
unset(CVPY_SUFFIX)
unset(PYTHON_INSTALL_CONFIGURATIONS)
unset(PYTHON_INSTALL_ARCHIVE)
+4
View File
@@ -0,0 +1,4 @@
__pycache__
*.pyc
*.egg-info
*dist
+181
View File
@@ -0,0 +1,181 @@
'''
OpenCV Python binary extension loader
'''
import os
import importlib
import sys
__all__ = []
try:
import numpy
import numpy.core.multiarray
except ImportError:
print('OpenCV bindings requires "numpy" package.')
print('Install it via command:')
print(' pip install numpy')
raise
# TODO
# is_x64 = sys.maxsize > 2**32
def __load_extra_py_code_for_module(base, name, enable_debug_print=False):
module_name = "{}.{}".format(__name__, name)
export_module_name = "{}.{}".format(base, name)
native_module = sys.modules.pop(module_name, None)
try:
py_module = importlib.import_module(module_name)
except (ImportError, AttributeError) as err:
if enable_debug_print:
print("Can't load Python code for module:", module_name,
". Reason:", err)
# Extension doesn't contain extra py code
return False
if base in sys.modules and not hasattr(sys.modules[base], name):
setattr(sys.modules[base], name, py_module)
sys.modules[export_module_name] = py_module
# If it is C extension module it is already loaded by cv2 package
if native_module:
setattr(py_module, "_native", native_module)
for k, v in filter(lambda kv: not hasattr(py_module, kv[0]),
native_module.__dict__.items()):
if enable_debug_print: print(' symbol({}): {} = {}'.format(name, k, v))
setattr(py_module, k, v)
return True
def __collect_extra_submodules(enable_debug_print=False):
def modules_filter(module):
return all((
# module is not internal
not module.startswith("_"),
not module.startswith("python-"),
# it is not a file
os.path.isdir(os.path.join(_extra_submodules_init_path, module))
))
if sys.version_info[0] < 3:
if enable_debug_print:
print("Extra submodules is loaded only for Python 3")
return []
__INIT_FILE_PATH = os.path.abspath(__file__)
_extra_submodules_init_path = os.path.dirname(__INIT_FILE_PATH)
return filter(modules_filter, os.listdir(_extra_submodules_init_path))
def bootstrap():
import sys
import copy
save_sys_path = copy.copy(sys.path)
if hasattr(sys, 'OpenCV_LOADER'):
print(sys.path)
raise ImportError('ERROR: recursion is detected during loading of "cv2" binary extensions. Check OpenCV installation.')
sys.OpenCV_LOADER = True
DEBUG = False
if hasattr(sys, 'OpenCV_LOADER_DEBUG'):
DEBUG = True
import platform
if DEBUG: print('OpenCV loader: os.name="{}" platform.system()="{}"'.format(os.name, str(platform.system())))
LOADER_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
PYTHON_EXTENSIONS_PATHS = []
BINARIES_PATHS = []
g_vars = globals()
l_vars = locals().copy()
if sys.version_info[:2] < (3, 0):
from . load_config_py2 import exec_file_wrapper
else:
from . load_config_py3 import exec_file_wrapper
def load_first_config(fnames, required=True):
for fname in fnames:
fpath = os.path.join(LOADER_DIR, fname)
if not os.path.exists(fpath):
if DEBUG: print('OpenCV loader: config not found, skip: {}'.format(fpath))
continue
if DEBUG: print('OpenCV loader: loading config: {}'.format(fpath))
exec_file_wrapper(fpath, g_vars, l_vars)
return True
if required:
raise ImportError('OpenCV loader: missing configuration file: {}. Check OpenCV installation.'.format(fnames))
load_first_config(['config.py'], True)
load_first_config([
'config-{}.{}.py'.format(sys.version_info[0], sys.version_info[1]),
'config-{}.py'.format(sys.version_info[0])
], True)
if DEBUG: print('OpenCV loader: PYTHON_EXTENSIONS_PATHS={}'.format(str(l_vars['PYTHON_EXTENSIONS_PATHS'])))
if DEBUG: print('OpenCV loader: BINARIES_PATHS={}'.format(str(l_vars['BINARIES_PATHS'])))
applySysPathWorkaround = False
if hasattr(sys, 'OpenCV_REPLACE_SYS_PATH_0'):
applySysPathWorkaround = True
else:
try:
BASE_DIR = os.path.dirname(LOADER_DIR)
if sys.path[0] == BASE_DIR or os.path.realpath(sys.path[0]) == BASE_DIR:
applySysPathWorkaround = True
except:
if DEBUG: print('OpenCV loader: exception during checking workaround for sys.path[0]')
pass # applySysPathWorkaround is False
for p in reversed(l_vars['PYTHON_EXTENSIONS_PATHS']):
sys.path.insert(1 if not applySysPathWorkaround else 0, p)
if os.name == 'nt':
if sys.version_info[:2] >= (3, 8): # https://github.com/python/cpython/pull/12302
for p in l_vars['BINARIES_PATHS']:
try:
os.add_dll_directory(p)
except Exception as e:
if DEBUG: print('Failed os.add_dll_directory(): '+ str(e))
pass
os.environ['PATH'] = ';'.join(l_vars['BINARIES_PATHS']) + ';' + os.environ.get('PATH', '')
if DEBUG: print('OpenCV loader: PATH={}'.format(str(os.environ['PATH'])))
else:
# amending of LD_LIBRARY_PATH works for sub-processes only
os.environ['LD_LIBRARY_PATH'] = ':'.join(l_vars['BINARIES_PATHS']) + ':' + os.environ.get('LD_LIBRARY_PATH', '')
if DEBUG: print("Relink everything from native cv2 module to cv2 package")
py_module = sys.modules.pop("cv2")
native_module = importlib.import_module("cv2")
sys.modules["cv2"] = py_module
setattr(py_module, "_native", native_module)
for item_name, item in filter(lambda kv: kv[0] not in ("__file__", "__loader__", "__spec__",
"__name__", "__package__"),
native_module.__dict__.items()):
if item_name not in g_vars:
g_vars[item_name] = item
sys.path = save_sys_path # multiprocessing should start from bootstrap code (https://github.com/opencv/opencv/issues/18502)
try:
del sys.OpenCV_LOADER
except Exception as e:
if DEBUG:
print("Exception during delete OpenCV_LOADER:", e)
if DEBUG: print('OpenCV loader: binary extension... OK')
for submodule in __collect_extra_submodules(DEBUG):
if __load_extra_py_code_for_module("cv2", submodule, DEBUG):
if DEBUG: print("Extra Python code for", submodule, "is loaded")
if DEBUG: print('OpenCV loader: DONE')
bootstrap()
@@ -0,0 +1,6 @@
# flake8: noqa
import sys
if sys.version_info[:2] < (3, 0):
def exec_file_wrapper(fpath, g_vars, l_vars):
execfile(fpath, g_vars, l_vars)
@@ -0,0 +1,9 @@
# flake8: noqa
import os
import sys
if sys.version_info[:2] >= (3, 0):
def exec_file_wrapper(fpath, g_vars, l_vars):
with open(fpath) as f:
code = compile(f.read(), fpath, 'exec')
exec(code, g_vars, l_vars)
@@ -0,0 +1 @@
from .version import get_ocv_version
@@ -0,0 +1,5 @@
import cv2
def get_ocv_version():
return getattr(cv2, "__version__", "unavailable")
+82
View File
@@ -0,0 +1,82 @@
import os
import setuptools
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def collect_module_typing_stub_files(root_module_path):
stub_files = []
for module_path, _, files in os.walk(root_module_path):
stub_files.extend(
map(lambda p: os.path.join(module_path, p),
filter(lambda f: f.endswith(".pyi"), files))
)
return stub_files
def main():
os.chdir(SCRIPT_DIR)
package_name = 'opencv'
package_version = os.environ.get('OPENCV_VERSION', '5.0.0') # TODO
long_description = 'Open Source Computer Vision Library Python bindings' # TODO
root_module_path = os.path.join(SCRIPT_DIR, "cv2")
py_typed_path = os.path.join(root_module_path, "py.typed")
typing_stub_files = []
if os.path.isfile(py_typed_path):
typing_stub_files = collect_module_typing_stub_files(root_module_path)
if len(typing_stub_files) > 0:
typing_stub_files.append(py_typed_path)
setuptools.setup(
name=package_name,
version=package_version,
url='https://github.com/opencv/opencv',
license='Apache 2.0',
description='OpenCV python bindings',
long_description=long_description,
long_description_content_type="text/markdown",
packages=setuptools.find_packages(),
package_data={
"cv2": typing_stub_files
},
maintainer="OpenCV Team",
install_requires="numpy",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: Unix",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"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.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: C++",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition",
"Topic :: Software Development",
],
)
if __name__ == '__main__':
main()
@@ -0,0 +1,3 @@
PYTHON_EXTENSIONS_PATHS = [
@CMAKE_PYTHON_EXTENSION_PATH@
] + PYTHON_EXTENSIONS_PATHS
@@ -0,0 +1,5 @@
import os
BINARIES_PATHS = [
@CMAKE_PYTHON_BINARIES_PATH@
] + BINARIES_PATHS
+14
View File
@@ -0,0 +1,14 @@
if(NOT PYTHON3_INCLUDE_PATH OR NOT PYTHON3_NUMPY_INCLUDE_DIRS)
ocv_module_disable(python3)
endif()
set(the_description "The python3 bindings")
set(MODULE_NAME python3)
set(MODULE_INSTALL_SUBDIR python3)
set(PYTHON PYTHON3)
include(../common.cmake)
unset(MODULE_NAME)
unset(MODULE_INSTALL_SUBDIR)
+157
View File
@@ -0,0 +1,157 @@
ocv_assert(NOT OPENCV_SKIP_PYTHON_LOADER)
set(PYTHON_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}")
if(OpenCV_FOUND)
set(__loader_path "${OpenCV_BINARY_DIR}/python_loader")
message(STATUS "OpenCV Python: during development append to PYTHONPATH: ${__loader_path}")
else()
set(__loader_path "${CMAKE_BINARY_DIR}/python_loader")
endif()
set(__python_loader_install_tmp_path "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/install/python_loader/")
if(DEFINED OPENCV_PYTHON_INSTALL_PATH)
if(IS_ABSOLUTE "${OPENCV_PYTHON_INSTALL_PATH}")
set(OpenCV_PYTHON_INSTALL_PATH_RELATIVE_CONFIGCMAKE "${CMAKE_INSTALL_PREFIX}/")
set(CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE "'${CMAKE_INSTALL_PREFIX}'")
else()
file(RELATIVE_PATH OpenCV_PYTHON_INSTALL_PATH_RELATIVE_CONFIGCMAKE "${CMAKE_INSTALL_PREFIX}/${OPENCV_PYTHON_INSTALL_PATH}/cv2" ${CMAKE_INSTALL_PREFIX})
set(CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE "os.path.join(LOADER_DIR, '${OpenCV_PYTHON_INSTALL_PATH_RELATIVE_CONFIGCMAKE}')")
endif()
else()
set(CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE "os.path.join(LOADER_DIR, 'not_installed')")
endif()
if(OpenCV_FOUND)
return() # Ignore "standalone" builds of Python bindings
endif()
set(PYTHON_LOADER_FILES
"setup.py" "cv2/__init__.py"
"cv2/load_config_py2.py" "cv2/load_config_py3.py"
)
foreach(fname ${PYTHON_LOADER_FILES})
get_filename_component(__dir "${fname}" DIRECTORY)
# avoid using of file(COPY) to rerun CMake on changes
configure_file("${PYTHON_SOURCE_DIR}/package/${fname}" "${__loader_path}/${fname}" COPYONLY)
if(fname STREQUAL "setup.py")
if(OPENCV_PYTHON_SETUP_PY_INSTALL_PATH)
install(FILES "${PYTHON_SOURCE_DIR}/package/${fname}" DESTINATION "${OPENCV_PYTHON_SETUP_PY_INSTALL_PATH}" COMPONENT python)
endif()
elseif(DEFINED OPENCV_PYTHON_INSTALL_PATH)
install(FILES "${PYTHON_SOURCE_DIR}/package/${fname}" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/${__dir}" COMPONENT python)
endif()
endforeach()
if(WIN32)
if(CMAKE_GENERATOR MATCHES "Visual Studio")
list(APPEND CMAKE_PYTHON_BINARIES_PATH "'${EXECUTABLE_OUTPUT_PATH}/Release'") # TODO: CMAKE_BUILD_TYPE is not defined
else()
list(APPEND CMAKE_PYTHON_BINARIES_PATH "'${EXECUTABLE_OUTPUT_PATH}'")
endif()
else()
list(APPEND CMAKE_PYTHON_BINARIES_PATH "'${LIBRARY_OUTPUT_PATH}'")
endif()
string(REPLACE ";" ",\n " CMAKE_PYTHON_BINARIES_PATH "${CMAKE_PYTHON_BINARIES_PATH}")
configure_file("${PYTHON_SOURCE_DIR}/package/template/config.py.in" "${__loader_path}/cv2/config.py" @ONLY)
# install
if(DEFINED OPENCV_PYTHON_INSTALL_PATH)
if(WIN32)
list(APPEND CMAKE_PYTHON_BINARIES_INSTALL_PATH "os.path.join(${CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE}, '${OPENCV_BIN_INSTALL_PATH}')")
else()
list(APPEND CMAKE_PYTHON_BINARIES_INSTALL_PATH "os.path.join(${CMAKE_PYTHON_EXTENSION_INSTALL_PATH_BASE}, '${OPENCV_LIB_INSTALL_PATH}')")
endif()
set(CMAKE_PYTHON_BINARIES_PATH "${CMAKE_PYTHON_BINARIES_INSTALL_PATH}")
if (WIN32 AND HAVE_CUDA)
set(_cuda_bin_dir "bin")
if (ENABLE_CUDA_FIRST_CLASS_LANGUAGE)
if (DEFINED CUDAToolkit_LIBRARY_ROOT)
if(DEFINED CUDAToolkit_VERSION_MAJOR AND CUDAToolkit_VERSION_MAJOR GREATER_EQUAL 13)
set(_cuda_bin_dir "bin/x64")
endif()
list(APPEND CMAKE_PYTHON_BINARIES_PATH "os.path.join(os.getenv('CUDA_PATH', '${CUDAToolkit_LIBRARY_ROOT}'), '${_cuda_bin_dir}')")
endif()
else()
if (DEFINED CUDA_TOOLKIT_ROOT_DIR)
if(DEFINED CUDA_VERSION_MAJOR AND CUDA_VERSION_MAJOR GREATER_EQUAL 13)
set(_cuda_bin_dir "bin/x64")
endif()
list(APPEND CMAKE_PYTHON_BINARIES_PATH "os.path.join(os.getenv('CUDA_PATH', '${CUDA_TOOLKIT_ROOT_DIR}'), '${_cuda_bin_dir}')")
endif()
endif()
endif()
string(REPLACE ";" ",\n " CMAKE_PYTHON_BINARIES_PATH "${CMAKE_PYTHON_BINARIES_PATH}")
configure_file("${PYTHON_SOURCE_DIR}/package/template/config.py.in" "${__python_loader_install_tmp_path}/cv2/config.py" @ONLY)
install(FILES "${__python_loader_install_tmp_path}/cv2/config.py" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/cv2/" COMPONENT python)
endif()
#
# Handle Python extra code (submodules)
#
function(ocv_add_python_files_from_path search_path)
file(GLOB_RECURSE extra_py_files
RELATIVE "${search_path}"
# Plain Python code
"${search_path}/*.py"
# Type annotations
"${search_path}/*.pyi"
)
ocv_debug_message("Extra Py files for ${search_path}: ${extra_py_files}")
if(extra_py_files)
list(SORT extra_py_files)
foreach(filename ${extra_py_files})
get_filename_component(module "${filename}" DIRECTORY)
if(NOT ${module} IN_LIST extra_modules)
list(APPEND extra_modules ${module})
endif()
configure_file("${search_path}/${filename}" "${__loader_path}/cv2/${filename}" COPYONLY)
if(DEFINED OPENCV_PYTHON_INSTALL_PATH)
install(FILES "${search_path}/${filename}" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}/cv2/${module}/" COMPONENT python)
endif()
endforeach()
message(STATUS "Found '${extra_modules}' Python modules from ${search_path}")
else()
message(WARNING "Can't add Python files and modules from '${module_path}'. There is no .py or .pyi files")
endif()
endfunction()
ocv_add_python_files_from_path("${PYTHON_SOURCE_DIR}/package/extra_modules")
foreach(m ${OPENCV_MODULES_BUILD})
if (";${OPENCV_MODULE_${m}_WRAPPERS};" MATCHES ";python;" AND HAVE_${m}
AND EXISTS "${OPENCV_MODULE_${m}_LOCATION}/misc/python/package"
)
ocv_add_python_files_from_path("${OPENCV_MODULE_${m}_LOCATION}/misc/python/package")
endif()
endforeach(m)
if(NOT "${OPENCV_PYTHON_EXTRA_MODULES_PATH}" STREQUAL "")
foreach(extra_ocv_py_modules_path ${OPENCV_PYTHON_EXTRA_MODULES_PATH})
ocv_add_python_files_from_path(${extra_ocv_py_modules_path})
endforeach()
endif()
if(${PYTHON}_VERSION_STRING VERSION_GREATER "3.6" AND PYTHON_DEFAULT_VERSION VERSION_GREATER "3.6")
add_custom_target(copy_opencv_typing_stubs)
# Copy all generated stub files to python_loader directory only if
# generation succeeds, this behvoir can't be achieved with default
# CMake constructions, because failed generation produces a warning instead of
# halts on hard error.
add_custom_command(
TARGET copy_opencv_typing_stubs
POST_BUILD
COMMAND ${PYTHON_DEFAULT_EXECUTABLE} ${PYTHON_SOURCE_DIR}/src2/copy_typings_stubs_on_success.py
--stubs_dir ${OPENCV_PYTHON_BINDINGS_DIR}/cv2
--output_dir ${__loader_path}/cv2
)
if(DEFINED OPENCV_PYTHON_INSTALL_PATH)
install(DIRECTORY "${OPENCV_PYTHON_BINDINGS_DIR}/cv2" DESTINATION "${OPENCV_PYTHON_INSTALL_PATH}" COMPONENT python)
endif()
endif()
@@ -0,0 +1,62 @@
import argparse
import warnings
import os
import sys
if sys.version_info >= (3, 8, ):
# shutil.copytree received the `dirs_exist_ok` parameter
from functools import partial
import shutil
copy_tree = partial(shutil.copytree, dirs_exist_ok=True)
else:
from distutils.dir_util import copy_tree
def _remove_stale_pyi_files(directory):
"""Remove .pyi files and py.typed markers from the directory tree.
During incremental builds, disabling a previously enabled module leaves
stale typing stubs in the loader directory from a previous copy. Since
copy_tree merges rather than replaces, those stale files persist.
Removing all stub files before copying ensures only stubs for currently
enabled modules are present. Runtime .py files are not affected.
"""
for dirpath, dirnames, filenames in os.walk(directory):
for fname in filenames:
if fname.endswith('.pyi') or fname == 'py.typed':
os.remove(os.path.join(dirpath, fname))
def main():
args = parse_arguments()
py_typed_path = os.path.join(args.stubs_dir, 'py.typed')
if not os.path.isfile(py_typed_path):
warnings.warn(
'{} is missing, it means that typings stubs generation is either '
'failed or has been skipped. Ensure that Python 3.6+ is used for '
'build and there is no warnings during Python source code '
'generation phase.'.format(py_typed_path)
)
return
if os.path.isdir(args.output_dir):
_remove_stale_pyi_files(args.output_dir)
copy_tree(args.stubs_dir, args.output_dir)
def parse_arguments():
parser = argparse.ArgumentParser(
description='Copies generated typing stubs only when generation '
'succeeded. This is identified by presence of the `py.typed` file '
'inside typing stubs directory.'
)
parser.add_argument('--stubs_dir', type=str,
help='Path to directory containing generated typing '
'stubs file')
parser.add_argument('--output_dir', type=str,
help='Path to output directory')
return parser.parse_args()
if __name__ == '__main__':
main()
+632
View File
@@ -0,0 +1,632 @@
// must be defined before importing numpy headers
// https://numpy.org/doc/1.17/reference/c-api.array.html#importing-the-api
#define PY_ARRAY_UNIQUE_SYMBOL opencv_ARRAY_API
#include "cv2.hpp"
#include "opencv2/opencv_modules.hpp"
#include "opencv2/core.hpp"
#include "opencv2/core/utils/logger.hpp"
#include "pyopencv_generated_include.h"
#include "cv2_util.hpp"
#include "cv2_numpy.hpp"
#include "cv2_convert.hpp"
#include "cv2_highgui.hpp"
using namespace cv;
typedef std::vector<uchar> vector_uchar;
typedef std::vector<char> vector_char;
typedef std::vector<int> vector_int;
typedef std::vector<float> vector_float;
typedef std::vector<double> vector_double;
typedef std::vector<size_t> vector_size_t;
typedef std::vector<Point> vector_Point;
typedef std::vector<Point2f> vector_Point2f;
typedef std::vector<Point3f> vector_Point3f;
typedef std::vector<Size> vector_Size;
typedef std::vector<Vec2f> vector_Vec2f;
typedef std::vector<Vec3f> vector_Vec3f;
typedef std::vector<Vec4f> vector_Vec4f;
typedef std::vector<Vec6f> vector_Vec6f;
typedef std::vector<Vec4i> vector_Vec4i;
typedef std::vector<Rect> vector_Rect;
typedef std::vector<Rect2d> vector_Rect2d;
typedef std::vector<RotatedRect> vector_RotatedRect;
typedef std::vector<KeyPoint> vector_KeyPoint;
typedef std::vector<Mat> vector_Mat;
typedef std::vector<std::vector<Mat> > vector_vector_Mat;
typedef std::vector<UMat> vector_UMat;
typedef std::vector<DMatch> vector_DMatch;
typedef std::vector<String> vector_String;
typedef std::vector<std::string> vector_string;
typedef std::vector<Scalar> vector_Scalar;
#ifdef HAVE_OPENCV_OBJDETECT
typedef std::vector<aruco::Dictionary> vector_Dictionary;
#endif // HAVE_OPENCV_OBJDETECT
#ifdef HAVE_OPENCV_GEOMETRY
typedef std::vector<MSTEdge> vector_MSTEdge;
#endif // HAVE_OPENCV_GEOMETRY
typedef std::vector<std::vector<char> > vector_vector_char;
typedef std::vector<std::vector<Point> > vector_vector_Point;
typedef std::vector<std::vector<Point2f> > vector_vector_Point2f;
typedef std::vector<std::vector<Point3f> > vector_vector_Point3f;
typedef std::vector<std::vector<DMatch> > vector_vector_DMatch;
typedef std::vector<std::vector<KeyPoint> > vector_vector_KeyPoint;
// enum { ARG_NONE = 0, ARG_MAT = 1, ARG_SCALAR = 2 };
///////////////////////////////////////////////////////////////////////////////////////
static int convert_to_char(PyObject *o, char *dst, const ArgInfo& info)
{
std::string str;
if (getUnicodeString(o, str))
{
*dst = str[0];
return 1;
}
(*dst) = 0;
return failmsg("Expected single character string for argument '%s'", info.name);
}
#ifdef __GNUC__
# pragma GCC diagnostic ignored "-Wunused-parameter"
# pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#endif
#include "pyopencv_generated_enums.h"
#ifdef CVPY_DYNAMIC_INIT
#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, _1, _2, SCOPE) CVPY_TYPE_DECLARE_DYNAMIC(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE)
#else
#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, _1, _2, SCOPE) CVPY_TYPE_DECLARE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE)
#endif
#include "pyopencv_generated_types.h"
#undef CVPY_TYPE
#include "pyopencv_custom_headers.h"
#include "pyopencv_generated_types_content.h"
#include "pyopencv_generated_funcs.h"
static PyObject* pycvRegisterMatType(PyObject *self, PyObject *value)
{
CV_LOG_DEBUG(NULL, cv::format("pycvRegisterMatType %p %p\n", self, value));
if (0 == PyType_Check(value))
{
PyErr_SetString(PyExc_TypeError, "Type argument is expected");
return NULL;
}
Py_INCREF(value);
pyopencv_Mat_TypePtr = (PyTypeObject*)value;
Py_RETURN_NONE;
}
static PyMethodDef special_methods[] = {
{"_registerMatType", (PyCFunction)(pycvRegisterMatType), METH_O, "_registerMatType(cv.Mat) -> None (Internal)"},
{"redirectError", CV_PY_FN_WITH_KW(pycvRedirectError), "redirectError(onError) -> None"},
#ifdef HAVE_OPENCV_HIGHGUI
{"createTrackbar", (PyCFunction)pycvCreateTrackbar, METH_VARARGS, "createTrackbar(trackbarName, windowName, value, count, onChange) -> None"},
{"createButton", CV_PY_FN_WITH_KW(pycvCreateButton), "createButton(buttonName, onChange [, userData, buttonType, initialButtonState]) -> None"},
{"setMouseCallback", CV_PY_FN_WITH_KW(pycvSetMouseCallback), "setMouseCallback(windowName, onMouse [, param]) -> None"},
#endif
#ifdef HAVE_OPENCV_DNN
{"dnn_registerLayer", CV_PY_FN_WITH_KW(pyopencv_cv_dnn_registerLayer), "registerLayer(type, class) -> None"},
{"dnn_unregisterLayer", CV_PY_FN_WITH_KW(pyopencv_cv_dnn_unregisterLayer), "unregisterLayer(type) -> None"},
#endif
{NULL, NULL},
};
/************************************************************************/
/* Module init */
struct ConstDef
{
const char * name;
long long val;
};
static inline bool strStartsWith(const std::string& str, const std::string& prefix) {
return prefix.empty() || \
(str.size() >= prefix.size() && std::memcmp(str.data(), prefix.data(), prefix.size()) == 0);
}
static inline bool strEndsWith(const std::string& str, char symbol) {
return !str.empty() && str[str.size() - 1] == symbol;
}
/**
* \brief Creates a submodule of the `root`. Missing parents submodules
* are created as needed. If name equals to parent module name than
* borrowed reference to parent module is returned (no reference counting
* are done).
* Submodule lifetime is managed by the parent module.
* If nested submodules are created than the lifetime is managed by the
* predecessor submodule in a list.
*
* \param parent_module Parent module object.
* \param name Submodule name.
* \return borrowed reference to the created submodule.
* If any of submodules can't be created than NULL is returned.
*/
static PyObject* createSubmodule(PyObject* parent_module, const std::string& name)
{
if (!parent_module)
{
return PyErr_Format(PyExc_ImportError,
"Bindings generation error. "
"Parent module is NULL during the submodule '%s' creation",
name.c_str()
);
}
if (strEndsWith(name, '.'))
{
return PyErr_Format(PyExc_ImportError,
"Bindings generation error. "
"Submodule can't end with a dot. Got: %s", name.c_str()
);
}
const std::string parent_name = PyModule_GetName(parent_module);
/// Special case handling when caller tries to register a submodule of the parent module with
/// the same name
if (name == parent_name) {
return parent_module;
}
if (!strStartsWith(name, parent_name))
{
return PyErr_Format(PyExc_ImportError,
"Bindings generation error. "
"Submodule name should always start with a parent module name. "
"Parent name: %s. Submodule name: %s", parent_name.c_str(),
name.c_str()
);
}
size_t submodule_name_end = name.find('.', parent_name.size() + 1);
/// There is no intermediate submodules in the provided name
if (submodule_name_end == std::string::npos)
{
submodule_name_end = name.size();
}
PyObject* submodule = parent_module;
for (size_t submodule_name_start = parent_name.size() + 1;
submodule_name_start < name.size(); )
{
const std::string submodule_name = name.substr(submodule_name_start,
submodule_name_end - submodule_name_start);
const std::string full_submodule_name = name.substr(0, submodule_name_end);
PyObject* parent_module_dict = PyModule_GetDict(submodule);
/// If submodule already exists it can be found in the parent module dictionary,
/// otherwise it should be added to it.
submodule = PyDict_GetItemString(parent_module_dict,
submodule_name.c_str());
if (!submodule)
{
/// Populates global modules dictionary and returns borrowed reference to it
submodule = PyImport_AddModule(full_submodule_name.c_str());
if (!submodule)
{
/// Return `PyImport_AddModule` NULL with an exception set on failure.
return NULL;
}
/// Populates parent module dictionary. Submodule lifetime should be managed
/// by the global modules dictionary and parent module dictionary, so Py_DECREF after
/// successful call to the `PyDict_SetItemString` is redundant.
if (PyDict_SetItemString(parent_module_dict, submodule_name.c_str(), submodule) < 0) {
return PyErr_Format(PyExc_ImportError,
"Can't register a submodule '%s' (full name: '%s')",
submodule_name.c_str(), full_submodule_name.c_str()
);
}
}
submodule_name_start = submodule_name_end + 1;
submodule_name_end = name.find('.', submodule_name_start);
if (submodule_name_end == std::string::npos) {
submodule_name_end = name.size();
}
}
return submodule;
}
static bool init_submodule(PyObject * root, const char * name, PyMethodDef * methods, ConstDef * consts)
{
// traverse and create nested submodules
PyObject* submodule = createSubmodule(root, name);
if (!submodule)
{
return false;
}
// populate module's dict
PyObject * d = PyModule_GetDict(submodule);
for (PyMethodDef * m = methods; m->ml_name != NULL; ++m)
{
PyObject * method_obj = PyCFunction_NewEx(m, NULL, NULL);
if (PyDict_SetItemString(d, m->ml_name, method_obj) < 0)
{
PyErr_Format(PyExc_ImportError,
"Can't register function %s in module: %s", m->ml_name, name
);
Py_CLEAR(method_obj);
return false;
}
Py_DECREF(method_obj);
}
for (ConstDef * c = consts; c->name != NULL; ++c)
{
PyObject* const_obj = PyLong_FromLongLong(c->val);
if (PyDict_SetItemString(d, c->name, const_obj) < 0)
{
PyErr_Format(PyExc_ImportError,
"Can't register constant %s in module %s", c->name, name
);
Py_CLEAR(const_obj);
return false;
}
Py_DECREF(const_obj);
}
return true;
}
static inline
bool registerTypeInModuleScope(PyObject* module, const char* type_name, PyObject* type_obj)
{
Py_INCREF(type_obj); /// Give PyModule_AddObject a reference to steal.
if (PyModule_AddObject(module, type_name, type_obj) < 0)
{
PyErr_Format(PyExc_ImportError,
"Failed to register type '%s' in module scope '%s'",
type_name, PyModule_GetName(module)
);
Py_DECREF(type_obj);
return false;
}
return true;
}
static inline
bool registerTypeInClassScope(PyObject* cls, const char* type_name, PyObject* type_obj)
{
if (!PyType_CheckExact(cls)) {
PyErr_Format(PyExc_ImportError,
"Failed to register type '%s' in class scope. "
"Scope class object has a wrong type", type_name
);
return false;
}
if (PyObject_SetAttrString(cls, type_name, type_obj) < 0)
{
#ifndef Py_LIMITED_API
PyObject* cls_dict = reinterpret_cast<PyTypeObject*>(cls)->tp_dict;
if (PyDict_SetItemString(cls_dict, type_name, type_obj) >= 0) {
/// Clearing the error set by PyObject_SetAttrString:
/// TypeError: can't set attributes of built-in/extension type NAME
PyErr_Clear();
return true;
}
#endif
const std::string cls_name = getPyObjectNameAttr(cls);
PyErr_Format(PyExc_ImportError,
"Failed to register type '%s' in '%s' class scope. Can't update scope dictionary",
type_name, cls_name.c_str()
);
return false;
}
return true;
}
static inline
PyObject* getScopeFromTypeObject(PyObject* obj, const std::string& scope_name)
{
if (!PyType_CheckExact(obj)) {
const std::string type_name = getPyObjectNameAttr(obj);
return PyErr_Format(PyExc_ImportError,
"Failed to get scope from type '%s' "
"Scope class object has a wrong type", type_name.c_str()
);
}
/// When using LIMITED API all classes are registered in the heap
#if defined(Py_LIMITED_API)
return PyObject_GetAttrString(obj, scope_name.c_str());
#else
/// Otherwise classes may be registed on the stack or heap
PyObject* type_dict = reinterpret_cast<PyTypeObject*>(obj)->tp_dict;
if (!type_dict) {
const std::string type_name = getPyObjectNameAttr(obj);
return PyErr_Format(PyExc_ImportError,
"Failed to get scope from type '%s' "
"Type dictionary is not available", type_name.c_str()
);
}
return PyDict_GetItemString(type_dict, scope_name.c_str());
#endif // Py_LIMITED_API
}
static inline
PyObject* findTypeScope(PyObject* root_module, const std::string& scope_name)
{
PyObject* scope = root_module;
if (scope_name.empty())
{
return scope;
}
/// Starting with 1 to omit leading dot in the scope name
size_t name_end = scope_name.find('.', 1);
if (name_end == std::string::npos)
{
name_end = scope_name.size();
}
for (size_t name_start = 1; name_start < scope_name.size() && scope; )
{
const std::string current_scope_name = scope_name.substr(name_start,
name_end - name_start);
if (PyModule_CheckExact(scope))
{
PyObject* scope_dict = PyModule_GetDict(scope);
if (!scope_dict)
{
return PyErr_Format(PyExc_ImportError,
"Scope '%s' dictionary is not available during the search for "
" the '%s' scope object", current_scope_name.c_str(),
scope_name.c_str()
);
}
scope = PyDict_GetItemString(scope_dict, current_scope_name.c_str());
}
else if (PyType_CheckExact(scope))
{
scope = getScopeFromTypeObject(scope, current_scope_name);
}
else
{
return PyErr_Format(PyExc_ImportError,
"Can't find scope '%s'. '%s' doesn't reference a module or a class",
scope_name.c_str(), current_scope_name.c_str()
);
}
name_start = name_end + 1;
name_end = scope_name.find('.', name_start);
if (name_end == std::string::npos)
{
name_end = scope_name.size();
}
}
if (!scope)
{
return PyErr_Format(PyExc_ImportError,
"Module or class with name '%s' can't be found in '%s' module",
scope_name.c_str(), PyModule_GetName(root_module)
);
}
return scope;
}
static bool registerNewType(PyObject* root_module, const char* type_name,
PyObject* type_obj, const std::string& scope_name)
{
PyObject* scope = findTypeScope(root_module, scope_name);
/// If scope can't be found it means that there is an error during
/// bindings generation
if (!scope) {
return false;
}
if (PyModule_CheckExact(scope))
{
if (!registerTypeInModuleScope(scope, type_name, type_obj))
{
return false;
}
}
else
{
/// In Python 2 it is disallowed to register an inner classes
/// via modifing dictionary of the built-in type.
if (!registerTypeInClassScope(scope, type_name, type_obj))
{
return false;
}
}
/// Expose all classes that are defined in the submodules as aliases in the
/// root module for backward compatibility
/// If submodule and root module are same than no aliases registration are
/// required
if (scope != root_module)
{
std::string type_name_str(type_name);
std::string alias_name;
alias_name.reserve(scope_name.size() + type_name_str.size());
std::replace_copy(scope_name.begin() + 1, scope_name.end(), std::back_inserter(alias_name), '.', '_');
alias_name += '_';
alias_name += type_name_str;
return registerTypeInModuleScope(root_module, alias_name.c_str(), type_obj);
}
return true;
}
#include "pyopencv_generated_modules_content.h"
static bool init_body(PyObject * m)
{
#define CVPY_MODULE(NAMESTR, NAME) \
if (!init_submodule(m, MODULESTR NAMESTR, methods_##NAME, consts_##NAME)) \
{ \
return false; \
}
#include "pyopencv_generated_modules.h"
#undef CVPY_MODULE
#ifdef CVPY_DYNAMIC_INIT
#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, _1, _2, BASE, CONSTRUCTOR, SCOPE) CVPY_TYPE_INIT_DYNAMIC(EXPORT_NAME, CLASS_ID, return false, BASE, CONSTRUCTOR, SCOPE)
PyObject * pyopencv_NoBase_TypePtr = NULL;
#else
#define CVPY_TYPE(EXPORT_NAME, CLASS_ID, _1, _2, BASE, CONSTRUCTOR, SCOPE) CVPY_TYPE_INIT_STATIC(EXPORT_NAME, CLASS_ID, return false, BASE, CONSTRUCTOR, SCOPE)
PyTypeObject * pyopencv_NoBase_TypePtr = NULL;
#endif
#include "pyopencv_generated_types.h"
#undef CVPY_TYPE
PyObject* d = PyModule_GetDict(m);
PyObject* version_obj = PyString_FromString(CV_VERSION);
if (PyDict_SetItemString(d, "__version__", version_obj) < 0) {
PyErr_SetString(PyExc_ImportError, "Can't update module version");
Py_CLEAR(version_obj);
return false;
}
Py_DECREF(version_obj);
PyObject *opencv_error_dict = PyDict_New();
PyDict_SetItemString(opencv_error_dict, "file", Py_None);
PyDict_SetItemString(opencv_error_dict, "func", Py_None);
PyDict_SetItemString(opencv_error_dict, "line", Py_None);
PyDict_SetItemString(opencv_error_dict, "code", Py_None);
PyDict_SetItemString(opencv_error_dict, "msg", Py_None);
PyDict_SetItemString(opencv_error_dict, "err", Py_None);
opencv_error = PyErr_NewException((char*)MODULESTR".error", NULL, opencv_error_dict);
Py_DECREF(opencv_error_dict);
PyDict_SetItemString(d, "error", opencv_error);
#define PUBLISH_(I, var_name, type_obj) \
PyObject* type_obj = PyInt_FromLong(I); \
if (PyDict_SetItemString(d, var_name, type_obj) < 0) \
{ \
PyErr_SetString(PyExc_ImportError, "Can't register " var_name " constant"); \
Py_CLEAR(type_obj); \
return false; \
} \
Py_DECREF(type_obj);
#define PUBLISH(I) PUBLISH_(I, #I, I ## _obj)
PUBLISH(CV_8U);
PUBLISH(CV_8UC1);
PUBLISH(CV_8UC2);
PUBLISH(CV_8UC3);
PUBLISH(CV_8UC4);
PUBLISH(CV_8S);
PUBLISH(CV_8SC1);
PUBLISH(CV_8SC2);
PUBLISH(CV_8SC3);
PUBLISH(CV_8SC4);
PUBLISH(CV_16U);
PUBLISH(CV_16UC1);
PUBLISH(CV_16UC2);
PUBLISH(CV_16UC3);
PUBLISH(CV_16UC4);
PUBLISH(CV_16S);
PUBLISH(CV_16SC1);
PUBLISH(CV_16SC2);
PUBLISH(CV_16SC3);
PUBLISH(CV_16SC4);
PUBLISH(CV_32U);
PUBLISH(CV_32UC1);
PUBLISH(CV_32UC2);
PUBLISH(CV_32UC3);
PUBLISH(CV_32UC4);
PUBLISH(CV_32S);
PUBLISH(CV_32SC1);
PUBLISH(CV_32SC2);
PUBLISH(CV_32SC3);
PUBLISH(CV_32SC4);
PUBLISH(CV_64U);
PUBLISH(CV_64UC1);
PUBLISH(CV_64UC2);
PUBLISH(CV_64UC3);
PUBLISH(CV_64UC4);
PUBLISH(CV_64S);
PUBLISH(CV_64SC1);
PUBLISH(CV_64SC2);
PUBLISH(CV_64SC3);
PUBLISH(CV_64SC4);
PUBLISH(CV_32F);
PUBLISH(CV_32FC1);
PUBLISH(CV_32FC2);
PUBLISH(CV_32FC3);
PUBLISH(CV_32FC4);
PUBLISH(CV_64F);
PUBLISH(CV_64FC1);
PUBLISH(CV_64FC2);
PUBLISH(CV_64FC3);
PUBLISH(CV_64FC4);
PUBLISH(CV_16F);
PUBLISH(CV_16FC1);
PUBLISH(CV_16FC2);
PUBLISH(CV_16FC3);
PUBLISH(CV_16FC4);
PUBLISH(CV_Bool);
PUBLISH(CV_BoolC1);
PUBLISH(CV_BoolC2);
PUBLISH(CV_BoolC3);
PUBLISH(CV_BoolC4);
#undef PUBLISH_
#undef PUBLISH
return true;
}
#if defined(__GNUC__)
#pragma GCC visibility push(default)
#endif
#if defined(CV_PYTHON_3)
// === Python 3
static struct PyModuleDef cv2_moduledef =
{
PyModuleDef_HEAD_INIT,
MODULESTR,
"Python wrapper for OpenCV.",
-1, /* size of per-interpreter state of the module,
or -1 if the module keeps state in global variables. */
special_methods
};
PyMODINIT_FUNC PyInit_cv2();
PyObject* PyInit_cv2()
{
import_array(); // from numpy
PyObject* m = PyModule_Create(&cv2_moduledef);
if (!init_body(m))
return NULL;
return m;
}
#else
// === Python 2
PyMODINIT_FUNC initcv2();
void initcv2()
{
import_array(); // from numpy
PyObject* m = Py_InitModule(MODULESTR, special_methods);
init_body(m);
}
#endif
+72
View File
@@ -0,0 +1,72 @@
#ifndef CV2_HPP
#define CV2_HPP
//warning number '5033' not a valid compiler warning in vc12
#if defined(_MSC_VER) && (_MSC_VER > 1800)
// eliminating duplicated round() declaration
#define HAVE_ROUND 1
#pragma warning(push)
#pragma warning(disable:5033) // 'register' is no longer a supported storage class
#endif
// #define CVPY_DYNAMIC_INIT
// #define Py_DEBUG
#if defined(CVPY_DYNAMIC_INIT) && !defined(Py_DEBUG)
# ifndef PYTHON3_LIMITED_API_VERSION
# define PYTHON3_LIMITED_API_VERSION 0x03060000
# endif
# define Py_LIMITED_API PYTHON3_LIMITED_API_VERSION
#endif
#include <cmath>
#include <Python.h>
#include <limits>
#if PY_MAJOR_VERSION < 3
#undef CVPY_DYNAMIC_INIT
#else
#define CV_PYTHON_3 1
#endif
#if defined(_MSC_VER) && (_MSC_VER > 1800)
#pragma warning(pop)
#endif
#define MODULESTR "cv2"
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <numpy/ndarrayobject.h>
#include "pycompat.hpp"
class ArgInfo
{
private:
static const uint32_t arg_outputarg_flag = 0x1;
static const uint32_t arg_arithm_op_src_flag = 0x2;
static const uint32_t arg_pathlike_flag = 0x4;
static const uint32_t arg_nd_mat_flag = 0x8;
public:
const char* name;
bool outputarg;
bool arithm_op_src;
bool pathlike;
bool nd_mat;
// more fields may be added if necessary
ArgInfo(const char* name_, uint32_t arg_) :
name(name_),
outputarg((arg_ & arg_outputarg_flag) != 0),
arithm_op_src((arg_ & arg_arithm_op_src_flag) != 0),
pathlike((arg_ & arg_pathlike_flag) != 0),
nd_mat((arg_ & arg_nd_mat_flag) != 0) {}
private:
ArgInfo(const ArgInfo&) = delete;
ArgInfo& operator=(const ArgInfo&) = delete;
};
#endif // CV2_HPP
File diff suppressed because it is too large Load Diff
+646
View File
@@ -0,0 +1,646 @@
#ifndef CV2_CONVERT_HPP
#define CV2_CONVERT_HPP
#include "cv2.hpp"
#include "cv2_util.hpp"
#include "cv2_numpy.hpp"
#include <vector>
#include <string>
#include <unordered_map>
#include <map>
#include <type_traits> // std::enable_if
extern PyTypeObject* pyopencv_Mat_TypePtr;
#define CV_HAS_CONVERSION_ERROR(x) (((x) == -1) && PyErr_Occurred())
inline bool isBool(PyObject* obj) CV_NOEXCEPT
{
return PyArray_IsScalar(obj, Bool) || PyBool_Check(obj);
}
//======================================================================================================================
// exception-safe pyopencv_to
template<typename _Tp> static
bool pyopencv_to_safe(PyObject* obj, _Tp& value, const ArgInfo& info)
{
try
{
return pyopencv_to(obj, value, info);
}
catch (const std::exception &e)
{
PyErr_SetString(opencv_error, cv::format("Conversion error: %s, what: %s", info.name, e.what()).c_str());
return false;
}
catch (...)
{
PyErr_SetString(opencv_error, cv::format("Conversion error: %s", info.name).c_str());
return false;
}
}
//======================================================================================================================
template<typename T, class TEnable = void> // TEnable is used for SFINAE checks
struct PyOpenCV_Converter
{
//static inline bool to(PyObject* obj, T& p, const ArgInfo& info);
//static inline PyObject* from(const T& src);
};
// --- Generic
template<typename T>
bool pyopencv_to(PyObject* obj, T& p, const ArgInfo& info) { return PyOpenCV_Converter<T>::to(obj, p, info); }
template<typename T>
PyObject* pyopencv_from(const T& src) { return PyOpenCV_Converter<T>::from(src); }
// --- Matx
template<typename _Tp, int m, int n>
bool pyopencv_to(PyObject* o, cv::Matx<_Tp, m, n>& mx, const ArgInfo& info)
{
if (!o || o == Py_None) {
return true;
}
cv::Mat tmp;
if (!pyopencv_to(o, tmp, info)) {
return false;
}
tmp.copyTo(mx);
return true;
}
template<typename _Tp, int m, int n>
PyObject* pyopencv_from(const cv::Matx<_Tp, m, n>& matx)
{
return pyopencv_from(cv::Mat(matx));
}
// --- bool
template<> bool pyopencv_to(PyObject* obj, bool& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const bool& value);
// --- Mat
template<> bool pyopencv_to(PyObject* o, cv::Mat& m, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Mat& m);
// --- Ptr
template<typename T>
struct PyOpenCV_Converter< cv::Ptr<T> >
{
static PyObject* from(const cv::Ptr<T>& p)
{
if (!p)
Py_RETURN_NONE;
return pyopencv_from(*p);
}
static bool to(PyObject *o, cv::Ptr<T>& p, const ArgInfo& info)
{
if (!o || o == Py_None)
return true;
p = cv::makePtr<T>();
return pyopencv_to(o, *p, info);
}
};
// --- ptr
template<> bool pyopencv_to(PyObject* obj, void*& ptr, const ArgInfo& info);
PyObject* pyopencv_from(void*& ptr);
// --- Scalar
template<> bool pyopencv_to(PyObject *o, cv::Scalar& s, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Scalar& src);
// --- size_t
template<> bool pyopencv_to(PyObject* obj, size_t& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const size_t& value);
// --- int
template<> bool pyopencv_to(PyObject* obj, int& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const int& value);
// --- int64
template<> bool pyopencv_to(PyObject* obj, int64& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const int64& value);
// There is conflict between "size_t" and "unsigned int".
// They are the same type on some 32-bit platforms.
template<typename T>
struct PyOpenCV_Converter
< T, typename std::enable_if< std::is_same<unsigned int, T>::value && !std::is_same<unsigned int, size_t>::value >::type >
{
static inline PyObject* from(const unsigned int& value)
{
return PyLong_FromUnsignedLong(value);
}
static inline bool to(PyObject* obj, unsigned int& value, const ArgInfo& info)
{
CV_UNUSED(info);
if(!obj || obj == Py_None)
return true;
if(PyInt_Check(obj))
value = (unsigned int)PyInt_AsLong(obj);
else if(PyLong_Check(obj))
value = (unsigned int)PyLong_AsLong(obj);
else
return false;
return value != (unsigned int)-1 || !PyErr_Occurred();
}
};
// There is conflict between "uint64_t" and "size_t".
// They are the same type on some 32-bit platforms.
template<typename T>
struct PyOpenCV_Converter
< T, typename std::enable_if< std::is_same<uint64_t, T>::value && !std::is_same<uint64_t, size_t>::value >::type >
{
static inline PyObject* from(const uint64_t& value)
{
return PyLong_FromUnsignedLongLong(value);
}
static inline bool to(PyObject* obj, uint64_t& value, const ArgInfo& info)
{
CV_UNUSED(info);
if(!obj || obj == Py_None)
return true;
if(PyInt_Check(obj))
value = (uint64_t)PyInt_AsUnsignedLongLongMask(obj);
else if(PyLong_Check(obj))
value = (uint64_t)PyLong_AsUnsignedLongLong(obj);
else
return false;
return value != (uint64_t)-1 || !PyErr_Occurred();
}
};
// There is conflict between "long long" and "int64".
// They are the same type on some 32-bit platforms.
template<typename T>
struct PyOpenCV_Converter
< T, typename std::enable_if< std::is_same<long long, T>::value && !std::is_same<long long, int64>::value >::type >
{
static inline PyObject* from(const long long& value)
{
return PyLong_FromLongLong(value);
}
static inline bool to(PyObject* obj, long long& value, const ArgInfo& info)
{
CV_UNUSED(info);
if(!obj || obj == Py_None)
return true;
else if(PyLong_Check(obj))
value = PyLong_AsLongLong(obj);
else
return false;
return value != (long long)-1 || !PyErr_Occurred();
}
};
// --- uchar
template<> bool pyopencv_to(PyObject* obj, uchar& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const uchar& value);
// --- char
template<> bool pyopencv_to(PyObject* obj, char& value, const ArgInfo& info);
// --- double
template<> bool pyopencv_to(PyObject* obj, double& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const double& value);
// --- float
template<> bool pyopencv_to(PyObject* obj, float& value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const float& value);
// --- string
template<> bool pyopencv_to(PyObject* obj, cv::String &value, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::String& value);
#if CV_VERSION_MAJOR == 3
template<> PyObject* pyopencv_from(const std::string& value);
#endif
// --- Size
template<> bool pyopencv_to(PyObject* obj, cv::Size& sz, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Size& sz);
template<> bool pyopencv_to(PyObject* obj, cv::Size_<float>& sz, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Size_<float>& sz);
// --- Rect
template<> bool pyopencv_to(PyObject* obj, cv::Rect& r, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Rect& r);
template<> bool pyopencv_to(PyObject* obj, cv::Rect2f& r, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Rect2f& r);
template<> bool pyopencv_to(PyObject* obj, cv::Rect2d& r, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Rect2d& r);
// --- RotatedRect
template<> bool pyopencv_to(PyObject* obj, cv::RotatedRect& dst, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::RotatedRect& src);
// --- Range
template<> bool pyopencv_to(PyObject* obj, cv::Range& r, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Range& r);
// --- Point
template<> bool pyopencv_to(PyObject* obj, cv::Point& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point& p);
template<> bool pyopencv_to(PyObject* obj, cv::Point2f& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point2f& p);
template<> bool pyopencv_to(PyObject* obj, cv::Point2d& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point2d& p);
template<> bool pyopencv_to(PyObject* obj, cv::Point3i& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point3i& p);
template<> bool pyopencv_to(PyObject* obj, cv::Point3f& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point3f& p);
template<> bool pyopencv_to(PyObject* obj, cv::Point3d& p, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::Point3d& p);
// --- Vec
template<typename _Tp, int cn>
bool pyopencv_to(PyObject* o, cv::Vec<_Tp, cn>& vec, const ArgInfo& info)
{
return pyopencv_to(o, (cv::Matx<_Tp, cn, 1>&)vec, info);
}
bool pyopencv_to(PyObject* obj, cv::Vec4d& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec4d& v);
bool pyopencv_to(PyObject* obj, cv::Vec4f& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec4f& v);
bool pyopencv_to(PyObject* obj, cv::Vec4i& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec4i& v);
bool pyopencv_to(PyObject* obj, cv::Vec3d& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec3d& v);
bool pyopencv_to(PyObject* obj, cv::Vec3f& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec3f& v);
bool pyopencv_to(PyObject* obj, cv::Vec3i& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec3i& v);
bool pyopencv_to(PyObject* obj, cv::Vec2d& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec2d& v);
bool pyopencv_to(PyObject* obj, cv::Vec2f& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec2f& v);
bool pyopencv_to(PyObject* obj, cv::Vec2i& v, ArgInfo& info);
PyObject* pyopencv_from(const cv::Vec2i& v);
// --- TermCriteria
template<> bool pyopencv_to(PyObject* obj, cv::TermCriteria& dst, const ArgInfo& info);
template<> PyObject* pyopencv_from(const cv::TermCriteria& src);
// --- Moments
template<> PyObject* pyopencv_from(const cv::Moments& m);
// --- pair
template<> PyObject* pyopencv_from(const std::pair<int, double>& src);
// --- vector
template <typename Tp>
struct pyopencvVecConverter;
template <typename Tp>
bool pyopencv_to(PyObject* obj, std::vector<Tp>& value, const ArgInfo& info)
{
if (!obj || obj == Py_None)
{
return true;
}
return pyopencvVecConverter<Tp>::to(obj, value, info);
}
template <typename Tp>
PyObject* pyopencv_from(const std::vector<Tp>& value)
{
return pyopencvVecConverter<Tp>::from(value);
}
template<typename K, typename V>
bool pyopencv_to(PyObject *obj, std::map<K,V> &map, const ArgInfo& info)
{
if (!obj || obj == Py_None)
{
return true;
}
PyObject* py_key = nullptr;
PyObject* py_value = nullptr;
Py_ssize_t pos = 0;
if (!PyDict_Check(obj)) {
failmsg("Can't parse '%s'. Input argument isn't dict or"
" an instance of subtype of the dict type", info.name);
return false;
}
while(PyDict_Next(obj, &pos, &py_key, &py_value))
{
K cpp_key;
if (!pyopencv_to(py_key, cpp_key, ArgInfo("key", 0))) {
failmsg("Can't parse dict key. Key on position %lu has a wrong type", pos);
return false;
}
V cpp_value;
if (!pyopencv_to(py_value, cpp_value, ArgInfo("value", 0))) {
failmsg("Can't parse dict value. Value on position %lu has a wrong type", pos);
return false;
}
map.emplace(cpp_key, cpp_value);
}
return true;
}
template <typename Tp>
static bool pyopencv_to_generic_vec(PyObject* obj, std::vector<Tp>& value, const ArgInfo& info)
{
if (!obj || obj == Py_None)
{
return true;
}
if (info.nd_mat && PyArray_Check(obj))
{
/*
If obj is marked as nd mat and of array type, it is parsed to a single
mat in the target vector to avoid being split into multiple mats
*/
value.resize(1);
if (!pyopencv_to(obj, value.front(), info))
{
failmsg("Can't parse '%s'. Array item has a wrong type", info.name);
return false;
}
}
else // parse as sequence
{
if (!PySequence_Check(obj))
{
failmsg("Can't parse '%s'. Input argument doesn't provide sequence protocol", info.name);
return false;
}
const size_t n = static_cast<size_t>(PySequence_Size(obj));
value.resize(n);
for (size_t i = 0; i < n; i++)
{
SafeSeqItem item_wrap(obj, i);
if (!pyopencv_to(item_wrap.item, value[i], info))
{
failmsg("Can't parse '%s'. Sequence item with index %lu has a wrong type", info.name, i);
return false;
}
}
}
return true;
}
template<> inline bool pyopencv_to_generic_vec(PyObject* obj, std::vector<bool>& value, const ArgInfo& info)
{
if (!obj || obj == Py_None)
{
return true;
}
if (!PySequence_Check(obj))
{
failmsg("Can't parse '%s'. Input argument doesn't provide sequence protocol", info.name);
return false;
}
const size_t n = static_cast<size_t>(PySequence_Size(obj));
value.resize(n);
for (size_t i = 0; i < n; i++)
{
SafeSeqItem item_wrap(obj, i);
bool elem{};
if (!pyopencv_to(item_wrap.item, elem, info))
{
failmsg("Can't parse '%s'. Sequence item with index %lu has a wrong type", info.name, i);
return false;
}
value[i] = elem;
}
return true;
}
template <typename Tp>
static PyObject* pyopencv_from_generic_vec(const std::vector<Tp>& value)
{
Py_ssize_t n = static_cast<Py_ssize_t>(value.size());
PySafeObject seq(PyTuple_New(n));
for (Py_ssize_t i = 0; i < n; i++)
{
PyObject* item = pyopencv_from(value[i]);
// If item can't be assigned - PyTuple_SetItem raises exception and returns -1.
if (!item || PyTuple_SetItem(seq, i, item) == -1)
{
return NULL;
}
}
return seq.release();
}
template<> inline PyObject* pyopencv_from_generic_vec(const std::vector<bool>& value)
{
Py_ssize_t n = static_cast<Py_ssize_t>(value.size());
PySafeObject seq(PyTuple_New(n));
for (Py_ssize_t i = 0; i < n; i++)
{
bool elem = value[i];
PyObject* item = pyopencv_from(elem);
// If item can't be assigned - PyTuple_SetItem raises exception and returns -1.
if (!item || PyTuple_SetItem(seq, i, item) == -1)
{
return NULL;
}
}
return seq.release();
}
namespace traits {
template <bool Value>
struct BooleanConstant
{
static const bool value = Value;
typedef BooleanConstant<Value> type;
};
typedef BooleanConstant<true> TrueType;
typedef BooleanConstant<false> FalseType;
template <class T>
struct VoidType {
typedef void type;
};
template <class T, class DType = void>
struct IsRepresentableAsMatDataType : FalseType
{
};
template <class T>
struct IsRepresentableAsMatDataType<T, typename VoidType<typename cv::DataType<T>::channel_type>::type> : TrueType
{
};
// https://github.com/opencv/opencv/issues/20930
template <> struct IsRepresentableAsMatDataType<cv::RotatedRect, void> : FalseType {};
} // namespace traits
template <typename Tp>
struct pyopencvVecConverter
{
typedef typename std::vector<Tp>::iterator VecIt;
static bool to(PyObject* obj, std::vector<Tp>& value, const ArgInfo& info)
{
if (!PyArray_Check(obj))
{
return pyopencv_to_generic_vec(obj, value, info);
}
// If user passed an array it is possible to make faster conversions in several cases
PyArrayObject* array_obj = reinterpret_cast<PyArrayObject*>(obj);
const NPY_TYPES target_type = asNumpyType<Tp>();
const NPY_TYPES source_type = static_cast<NPY_TYPES>(PyArray_TYPE(array_obj));
if (target_type == NPY_OBJECT)
{
// Non-planar arrays representing objects (e.g. array of N Rect is an array of shape Nx4) have NPY_OBJECT
// as their target type.
return pyopencv_to_generic_vec(obj, value, info);
}
if (PyArray_NDIM(array_obj) > 1)
{
failmsg("Can't parse %dD array as '%s' vector argument", PyArray_NDIM(array_obj), info.name);
return false;
}
if (target_type != source_type)
{
// Source type requires conversion
// Allowed conversions for target type is handled in the corresponding pyopencv_to function
return pyopencv_to_generic_vec(obj, value, info);
}
// For all other cases, all array data can be directly copied to std::vector data
// Simple `memcpy` is not possible because NumPy array can reference a slice of the bigger array:
// ```
// arr = np.ones((8, 4, 5), dtype=np.int32)
// convertible_to_vector_of_int = arr[:, 0, 1]
// ```
value.resize(static_cast<size_t>(PyArray_SIZE(array_obj)));
const npy_intp item_step = PyArray_STRIDE(array_obj, 0) / PyArray_ITEMSIZE(array_obj);
const Tp* data_ptr = static_cast<Tp*>(PyArray_DATA(array_obj));
for (VecIt it = value.begin(); it != value.end(); ++it, data_ptr += item_step) {
*it = *data_ptr;
}
return true;
}
static PyObject* from(const std::vector<Tp>& value)
{
if (value.empty())
{
return PyTuple_New(0);
}
return from(value, ::traits::IsRepresentableAsMatDataType<Tp>());
}
private:
static PyObject* from(const std::vector<Tp>& value, ::traits::FalseType)
{
// Underlying type is not representable as Mat Data Type
return pyopencv_from_generic_vec(value);
}
static PyObject* from(const std::vector<Tp>& value, ::traits::TrueType)
{
// Underlying type is representable as Mat Data Type, so faster return type is available
typedef cv::DataType<Tp> DType;
typedef typename DType::channel_type UnderlyingArrayType;
// If Mat is always exposed as NumPy array this code path can be reduced to the following snipped:
// Mat src(value);
// PyObject* array = pyopencv_from(src);
// return PyArray_Squeeze(reinterpret_cast<PyArrayObject*>(array));
// This puts unnecessary restrictions on Mat object those might be avoided without losing the performance.
// Moreover, this version is a bit faster, because it doesn't create temporary objects with reference counting.
const NPY_TYPES target_type = asNumpyType<UnderlyingArrayType>();
const int cols = DType::channels;
PyObject* array = NULL;
if (cols == 1)
{
npy_intp dims = static_cast<npy_intp>(value.size());
array = PyArray_SimpleNew(1, &dims, target_type);
}
else
{
npy_intp dims[2] = {static_cast<npy_intp>(value.size()), cols};
array = PyArray_SimpleNew(2, dims, target_type);
}
if(!array)
{
// NumPy arrays with shape (N, 1) and (N) are not equal, so correct error message should distinguish
// them too.
cv::String shape;
if (cols > 1)
{
shape = cv::format("(%d x %d)", static_cast<int>(value.size()), cols);
}
else
{
shape = cv::format("(%d)", static_cast<int>(value.size()));
}
const cv::String error_message = cv::format("Can't allocate NumPy array for vector with dtype=%d and shape=%s",
static_cast<int>(target_type), shape.c_str());
emit_failmsg(PyExc_MemoryError, error_message.c_str());
return array;
}
// Fill the array
PyArrayObject* array_obj = reinterpret_cast<PyArrayObject*>(array);
UnderlyingArrayType* array_data = static_cast<UnderlyingArrayType*>(PyArray_DATA(array_obj));
// if Tp is representable as Mat DataType, so the following cast is pretty safe...
const UnderlyingArrayType* value_data = reinterpret_cast<const UnderlyingArrayType*>(value.data());
memcpy(array_data, value_data, sizeof(UnderlyingArrayType) * value.size() * static_cast<size_t>(cols));
return array;
}
};
// --- tuple
template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I == sizeof...(Tp), void>::type
convert_to_python_tuple(const std::tuple<Tp...>&, PyObject*) { }
template<std::size_t I = 0, typename... Tp>
inline typename std::enable_if<I < sizeof...(Tp), void>::type
convert_to_python_tuple(const std::tuple<Tp...>& cpp_tuple, PyObject* py_tuple)
{
PyObject* item = pyopencv_from(std::get<I>(cpp_tuple));
if (!item)
return;
PyTuple_SetItem(py_tuple, I, item);
convert_to_python_tuple<I + 1, Tp...>(cpp_tuple, py_tuple);
}
template<typename... Ts>
PyObject* pyopencv_from(const std::tuple<Ts...>& cpp_tuple)
{
size_t size = sizeof...(Ts);
PyObject* py_tuple = PyTuple_New(size);
convert_to_python_tuple(cpp_tuple, py_tuple);
size_t actual_size = PyTuple_Size(py_tuple);
if (actual_size < size)
{
Py_DECREF(py_tuple);
return NULL;
}
return py_tuple;
}
#endif // CV2_CONVERT_HPP
+184
View File
@@ -0,0 +1,184 @@
#include "cv2_highgui.hpp"
#ifdef HAVE_OPENCV_HIGHGUI
#include "cv2_util.hpp"
#include "opencv2/highgui.hpp"
#include <map>
using namespace cv;
//======================================================================================================================
static void OnMouse(int event, int x, int y, int flags, void* param)
{
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
PyObject *o = (PyObject*)param;
PyObject *args = Py_BuildValue("iiiiO", event, x, y, flags, PyTuple_GetItem(o, 1));
PyObject *r = PyObject_Call(PyTuple_GetItem(o, 0), args, NULL);
if (r == NULL)
PyErr_Print();
else
Py_DECREF(r);
Py_DECREF(args);
PyGILState_Release(gstate);
}
PyObject *pycvSetMouseCallback(PyObject*, PyObject *args, PyObject *kw)
{
const char *keywords[] = { "window_name", "on_mouse", "param", NULL };
char* name;
PyObject *on_mouse;
PyObject *param = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kw, "sO|O", (char**)keywords, &name, &on_mouse, &param))
return NULL;
if (!PyCallable_Check(on_mouse)) {
PyErr_SetString(PyExc_TypeError, "on_mouse must be callable");
return NULL;
}
if (param == NULL) {
param = Py_None;
}
PyObject* py_callback_info = Py_BuildValue("OO", on_mouse, param);
static std::map<std::string, PyObject*> registered_callbacks;
std::map<std::string, PyObject*>::iterator i = registered_callbacks.find(name);
if (i != registered_callbacks.end())
{
Py_DECREF(i->second);
i->second = py_callback_info;
}
else
{
registered_callbacks.insert(std::pair<std::string, PyObject*>(std::string(name), py_callback_info));
}
ERRWRAP2(setMouseCallback(name, OnMouse, py_callback_info));
Py_RETURN_NONE;
}
//======================================================================================================================
static void OnChange(int pos, void *param)
{
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
PyObject *o = (PyObject*)param;
PyObject *args = Py_BuildValue("(i)", pos);
PyObject *r = PyObject_Call(PyTuple_GetItem(o, 0), args, NULL);
if (r == NULL)
PyErr_Print();
else
Py_DECREF(r);
Py_DECREF(args);
PyGILState_Release(gstate);
}
// workaround for #20408, use nullptr, set value later
static int _createTrackbar(const String &trackbar_name, const String &window_name, int value, int count,
TrackbarCallback onChange, PyObject* py_callback_info)
{
int n = createTrackbar(trackbar_name, window_name, NULL, count, onChange, py_callback_info);
setTrackbarPos(trackbar_name, window_name, value);
return n;
}
PyObject *pycvCreateTrackbar(PyObject*, PyObject *args)
{
PyObject *on_change;
char* trackbar_name;
char* window_name;
int value;
int count;
if (!PyArg_ParseTuple(args, "ssiiO", &trackbar_name, &window_name, &value, &count, &on_change))
return NULL;
if (!PyCallable_Check(on_change)) {
PyErr_SetString(PyExc_TypeError, "on_change must be callable");
return NULL;
}
PyObject* py_callback_info = Py_BuildValue("OO", on_change, Py_None);
std::string name = std::string(window_name) + ":" + std::string(trackbar_name);
static std::map<std::string, PyObject*> registered_callbacks;
std::map<std::string, PyObject*>::iterator i = registered_callbacks.find(name);
if (i != registered_callbacks.end())
{
Py_DECREF(i->second);
i->second = py_callback_info;
}
else
{
registered_callbacks.insert(std::pair<std::string, PyObject*>(name, py_callback_info));
}
ERRWRAP2(_createTrackbar(trackbar_name, window_name, value, count, OnChange, py_callback_info));
Py_RETURN_NONE;
}
//======================================================================================================================
static void OnButtonChange(int state, void *param)
{
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
PyObject *o = (PyObject*)param;
PyObject *args;
if(PyTuple_GetItem(o, 1) != NULL)
{
args = Py_BuildValue("(iO)", state, PyTuple_GetItem(o,1));
}
else
{
args = Py_BuildValue("(i)", state);
}
PyObject *r = PyObject_Call(PyTuple_GetItem(o, 0), args, NULL);
if (r == NULL)
PyErr_Print();
else
Py_DECREF(r);
Py_DECREF(args);
PyGILState_Release(gstate);
}
PyObject *pycvCreateButton(PyObject*, PyObject *args, PyObject *kw)
{
const char* keywords[] = {"buttonName", "onChange", "userData", "buttonType", "initialButtonState", NULL};
PyObject *on_change;
PyObject *userdata = NULL;
char* button_name;
int button_type = 0;
int initial_button_state = 0;
if (!PyArg_ParseTupleAndKeywords(args, kw, "sO|Oii", (char**)keywords, &button_name, &on_change, &userdata, &button_type, &initial_button_state))
return NULL;
if (!PyCallable_Check(on_change)) {
PyErr_SetString(PyExc_TypeError, "onChange must be callable");
return NULL;
}
if (userdata == NULL) {
userdata = Py_None;
}
PyObject* py_callback_info = Py_BuildValue("OO", on_change, userdata);
std::string name(button_name);
static std::map<std::string, PyObject*> registered_callbacks;
std::map<std::string, PyObject*>::iterator i = registered_callbacks.find(name);
if (i != registered_callbacks.end())
{
Py_DECREF(i->second);
i->second = py_callback_info;
}
else
{
registered_callbacks.insert(std::pair<std::string, PyObject*>(name, py_callback_info));
}
ERRWRAP2(createButton(button_name, OnButtonChange, py_callback_info, button_type, initial_button_state != 0));
Py_RETURN_NONE;
}
#endif // HAVE_OPENCV_HIGHGUI
+14
View File
@@ -0,0 +1,14 @@
#ifndef CV2_HIGHGUI_HPP
#define CV2_HIGHGUI_HPP
#include "cv2.hpp"
#include "opencv2/opencv_modules.hpp"
#ifdef HAVE_OPENCV_HIGHGUI
PyObject *pycvSetMouseCallback(PyObject*, PyObject *args, PyObject *kw);
// workaround for #20408, use nullptr, set value later
PyObject *pycvCreateTrackbar(PyObject*, PyObject *args);
PyObject *pycvCreateButton(PyObject*, PyObject *args, PyObject *kw);
#endif
#endif // CV2_HIGHGUI_HPP
+68
View File
@@ -0,0 +1,68 @@
// must be defined before importing numpy headers
// https://numpy.org/doc/1.17/reference/c-api.array.html#importing-the-api
#define NO_IMPORT_ARRAY
#define PY_ARRAY_UNIQUE_SYMBOL opencv_ARRAY_API
#include "cv2_numpy.hpp"
#include "cv2_util.hpp"
using namespace cv;
UMatData* NumpyAllocator::allocate(PyObject* o, int dims, const int* sizes, int type, size_t* step) const
{
UMatData* u = new UMatData(this);
u->data = u->origdata = (uchar*)PyArray_DATA((PyArrayObject*) o);
npy_intp* _strides = PyArray_STRIDES((PyArrayObject*) o);
for( int i = 0; i < dims - 1; i++ )
step[i] = (size_t)_strides[i];
if( dims > 0 )
step[dims-1] = CV_ELEM_SIZE(type);
u->size = dims > 0 ? sizes[0]*step[0] : CV_ELEM_SIZE(type);
u->userdata = o;
return u;
}
UMatData* NumpyAllocator::allocate(int dims0, const int* sizes, int type, void* data, size_t* step, AccessFlag flags, UMatUsageFlags usageFlags) const
{
if( data != 0 )
{
// issue #6969: CV_Error(Error::StsAssert, "The data should normally be NULL!");
// probably this is safe to do in such extreme case
return stdAllocator->allocate(dims0, sizes, type, data, step, flags, usageFlags);
}
PyEnsureGIL gil;
int depth = CV_MAT_DEPTH(type);
int cn = CV_MAT_CN(type);
int typenum = cvDepthToNumpyType(depth);
int i, dims = dims0;
cv::AutoBuffer<npy_intp> _sizes(dims + 1);
for( i = 0; i < dims; i++ )
_sizes[i] = sizes[i];
if( cn > 1 )
_sizes[dims++] = cn;
PyObject* o = PyArray_SimpleNew(dims, _sizes.data(), typenum);
if(!o)
CV_Error_(Error::StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims));
return allocate(o, dims0, sizes, type, step);
}
bool NumpyAllocator::allocate(UMatData* u, AccessFlag accessFlags, UMatUsageFlags usageFlags) const
{
return stdAllocator->allocate(u, accessFlags, usageFlags);
}
void NumpyAllocator::deallocate(UMatData* u) const
{
if(!u)
return;
PyEnsureGIL gil;
CV_Assert(u->urefcount >= 0);
CV_Assert(u->refcount >= 0);
if(u->refcount == 0)
{
PyObject* o = (PyObject*)u->userdata;
Py_XDECREF(o);
delete u;
}
}
+217
View File
@@ -0,0 +1,217 @@
#ifndef CV2_NUMPY_HPP
#define CV2_NUMPY_HPP
#include "cv2.hpp"
#include "opencv2/core.hpp"
class NumpyAllocator : public cv::MatAllocator
{
public:
NumpyAllocator() { stdAllocator = cv::Mat::getStdAllocator(); }
~NumpyAllocator() {}
cv::UMatData* allocate(PyObject* o, int dims, const int* sizes, int type, size_t* step) const;
cv::UMatData* allocate(int dims0, const int* sizes, int type, void* data, size_t* step, cv::AccessFlag flags, cv::UMatUsageFlags usageFlags) const CV_OVERRIDE;
bool allocate(cv::UMatData* u, cv::AccessFlag accessFlags, cv::UMatUsageFlags usageFlags) const CV_OVERRIDE;
void deallocate(cv::UMatData* u) const CV_OVERRIDE;
const cv::MatAllocator* stdAllocator;
};
inline NumpyAllocator& GetNumpyAllocator() {static NumpyAllocator gNumpyAllocator;return gNumpyAllocator;}
//======================================================================================================================
// HACK(?): function from cv2_util.hpp
extern int failmsg(const char *fmt, ...);
namespace {
template<class T>
NPY_TYPES asNumpyType()
{
return NPY_OBJECT;
}
template<>
NPY_TYPES asNumpyType<bool>()
{
return NPY_BOOL;
}
#define CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(src, dst) \
template<> \
NPY_TYPES asNumpyType<src>() \
{ \
return NPY_##dst; \
} \
template<> \
NPY_TYPES asNumpyType<u##src>() \
{ \
return NPY_U##dst; \
}
CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int8_t, INT8)
CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int16_t, INT16)
CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int32_t, INT32)
CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION(int64_t, INT64)
#undef CV_GENERATE_INTEGRAL_TYPE_NPY_CONVERSION
template<>
NPY_TYPES asNumpyType<float>()
{
return NPY_FLOAT;
}
template<>
NPY_TYPES asNumpyType<double>()
{
return NPY_DOUBLE;
}
template <class T>
PyArray_Descr* getNumpyTypeDescriptor()
{
return PyArray_DescrFromType(asNumpyType<T>());
}
template <>
PyArray_Descr* getNumpyTypeDescriptor<size_t>()
{
#if SIZE_MAX == ULONG_MAX
return PyArray_DescrFromType(NPY_ULONG);
#elif SIZE_MAX == ULLONG_MAX
return PyArray_DescrFromType(NPY_ULONGLONG);
#else
return PyArray_DescrFromType(NPY_UINT);
#endif
}
template <class T, class U>
bool isRepresentable(U value) {
return (std::numeric_limits<T>::min() <= value) && (value <= std::numeric_limits<T>::max());
}
template<class T>
bool canBeSafelyCasted(PyObject* obj, PyArray_Descr* to)
{
return PyArray_CanCastTo(PyArray_DescrFromScalar(obj), to) != 0;
}
template<>
bool canBeSafelyCasted<size_t>(PyObject* obj, PyArray_Descr* to)
{
PyArray_Descr* from = PyArray_DescrFromScalar(obj);
if (PyArray_CanCastTo(from, to))
{
return true;
}
else
{
// False negative scenarios:
// - Signed input is positive so it can be safely cast to unsigned output
// - Input has wider limits but value is representable within output limits
// - All the above
if (PyDataType_ISSIGNED(from))
{
int64_t input = 0;
PyArray_CastScalarToCtype(obj, &input, getNumpyTypeDescriptor<int64_t>());
return (input >= 0) && isRepresentable<size_t>(static_cast<uint64_t>(input));
}
else
{
uint64_t input = 0;
PyArray_CastScalarToCtype(obj, &input, getNumpyTypeDescriptor<uint64_t>());
return isRepresentable<size_t>(input);
}
return false;
}
}
template<class T>
bool parseNumpyScalar(PyObject* obj, T& value)
{
if (PyArray_CheckScalar(obj))
{
// According to the numpy documentation:
// There are 21 statically-defined PyArray_Descr objects for the built-in data-types
// So descriptor pointer is not owning.
PyArray_Descr* to = getNumpyTypeDescriptor<T>();
if (canBeSafelyCasted<T>(obj, to))
{
PyArray_CastScalarToCtype(obj, &value, to);
return true;
}
}
return false;
}
struct SafeSeqItem
{
PyObject * item;
SafeSeqItem(PyObject *obj, size_t idx) { item = PySequence_GetItem(obj, idx); }
~SafeSeqItem() { Py_XDECREF(item); }
private:
SafeSeqItem(const SafeSeqItem&); // = delete
SafeSeqItem& operator=(const SafeSeqItem&); // = delete
};
template <class T>
class RefWrapper
{
public:
RefWrapper(T& item) : item_(item) {}
T& get() CV_NOEXCEPT { return item_; }
private:
T& item_;
};
// In order to support this conversion on 3.x branch - use custom reference_wrapper
// and C-style array instead of std::array<T, N>
template <class T, std::size_t N>
bool parseSequence(PyObject* obj, RefWrapper<T> (&value)[N], const ArgInfo& info)
{
if (!obj || obj == Py_None)
{
return true;
}
if (!PySequence_Check(obj))
{
failmsg("Can't parse '%s'. Input argument doesn't provide sequence "
"protocol", info.name);
return false;
}
const std::size_t sequenceSize = PySequence_Size(obj);
if (sequenceSize != N)
{
failmsg("Can't parse '%s'. Expected sequence length %lu, got %lu",
info.name, N, sequenceSize);
return false;
}
for (std::size_t i = 0; i < N; ++i)
{
SafeSeqItem seqItem(obj, i);
if (!pyopencv_to(seqItem.item, value[i].get(), info))
{
failmsg("Can't parse '%s'. Sequence item with index %lu has a "
"wrong type", info.name, i);
return false;
}
}
return true;
}
} // namespace
#endif // CV2_NUMPY_HPP
+205
View File
@@ -0,0 +1,205 @@
#include "cv2_util.hpp"
#include "opencv2/core.hpp"
#include "opencv2/core/utils/configuration.private.hpp"
#include "opencv2/core/utils/logger.hpp"
PyObject* opencv_error = NULL;
cv::TLSData<std::vector<std::string> > conversionErrorsTLS;
int cvDepthToNumpyType(int depth)
{
const int f = (int)(sizeof(size_t)/8);
return depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE :
depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT :
depth == CV_32U ? NPY_UINT32 : depth == CV_32S ? NPY_INT32 : depth == CV_64S ? NPY_INT64 :
depth == CV_32F ? NPY_FLOAT : depth == CV_64F ? NPY_DOUBLE : depth == CV_16F ? NPY_HALF :
depth == CV_Bool ? NPY_BOOL : f*NPY_ULONGLONG + (f^1)*NPY_UINT;
}
int numpyTypeToCvDepth(int typenum)
{
return typenum == NPY_UBYTE ? CV_8U : typenum == NPY_BYTE ? CV_8S :
typenum == NPY_USHORT ? CV_16U : typenum == NPY_SHORT ? CV_16S :
typenum == NPY_INT ? CV_32S : typenum == NPY_UINT32 ? CV_32U : typenum == NPY_INT32 ? CV_32S :
typenum == NPY_HALF ? CV_16F : typenum == NPY_FLOAT ? CV_32F : typenum == NPY_DOUBLE ? CV_64F :
typenum == NPY_BOOL ? CV_Bool : -1;
}
using namespace cv;
//======================================================================================================================
bool isPythonBindingsDebugEnabled()
{
static bool param_debug = cv::utils::getConfigurationParameterBool("OPENCV_PYTHON_DEBUG", false);
return param_debug;
}
void emit_failmsg(PyObject * exc, const char *msg)
{
static bool param_debug = isPythonBindingsDebugEnabled();
if (param_debug)
{
CV_LOG_WARNING(NULL, "Bindings conversion failed: " << msg);
}
PyErr_SetString(exc, msg);
}
int failmsg(const char *fmt, ...)
{
char str[1000];
va_list ap;
va_start(ap, fmt);
vsnprintf(str, sizeof(str), fmt, ap);
va_end(ap);
emit_failmsg(PyExc_TypeError, str);
return 0;
}
PyObject* failmsgp(const char *fmt, ...)
{
char str[1000];
va_list ap;
va_start(ap, fmt);
vsnprintf(str, sizeof(str), fmt, ap);
va_end(ap);
emit_failmsg(PyExc_TypeError, str);
return 0;
}
void pyRaiseCVException(const cv::Exception &e)
{
PyObject* temp_obj = PyString_FromString(e.file.c_str());
PyObject_SetAttrString(opencv_error, "file", temp_obj);
Py_DECREF(temp_obj);
temp_obj = PyString_FromString(e.func.c_str());
PyObject_SetAttrString(opencv_error, "func", temp_obj);
Py_DECREF(temp_obj);
temp_obj = PyInt_FromLong(e.line);
PyObject_SetAttrString(opencv_error, "line", temp_obj);
Py_DECREF(temp_obj);
temp_obj = PyInt_FromLong(e.code);
PyObject_SetAttrString(opencv_error, "code", temp_obj);
Py_DECREF(temp_obj);
temp_obj = PyString_FromString(e.msg.c_str());
PyObject_SetAttrString(opencv_error, "msg", temp_obj);
Py_DECREF(temp_obj);
temp_obj = PyString_FromString(e.err.c_str());
PyObject_SetAttrString(opencv_error, "err", temp_obj);
Py_DECREF(temp_obj);
PyErr_SetString(opencv_error, e.what());
}
//======================================================================================================================
void pyRaiseCVOverloadException(const std::string& functionName)
{
const std::vector<std::string>& conversionErrors = conversionErrorsTLS.getRef();
const std::size_t conversionErrorsCount = conversionErrors.size();
if (conversionErrorsCount > 0)
{
// In modern std libraries small string optimization is used = no dynamic memory allocations,
// but it can be applied only for string with length < 18 symbols (in GCC)
const std::string bullet = "\n - ";
// Estimate required buffer size - save dynamic memory allocations = faster
std::size_t requiredBufferSize = bullet.size() * conversionErrorsCount;
for (std::size_t i = 0; i < conversionErrorsCount; ++i)
{
requiredBufferSize += conversionErrors[i].size();
}
// Only string concatenation is required so std::string is way faster than
// std::ostringstream
std::string errorMessage("Overload resolution failed:");
errorMessage.reserve(errorMessage.size() + requiredBufferSize);
for (std::size_t i = 0; i < conversionErrorsCount; ++i)
{
errorMessage += bullet;
errorMessage += conversionErrors[i];
}
cv::Exception exception(Error::StsBadArg, errorMessage, functionName, "", -1);
pyRaiseCVException(exception);
}
else
{
cv::Exception exception(Error::StsInternal, "Overload resolution failed, but no errors reported",
functionName, "", -1);
pyRaiseCVException(exception);
}
}
void pyPopulateArgumentConversionErrors()
{
if (PyErr_Occurred())
{
PySafeObject exception_type;
PySafeObject exception_value;
PySafeObject exception_traceback;
PyErr_Fetch(exception_type, exception_value, exception_traceback);
PyErr_NormalizeException(exception_type, exception_value,
exception_traceback);
PySafeObject exception_message(PyObject_Str(exception_value));
std::string message;
getUnicodeString(exception_message, message);
conversionErrorsTLS.getRef().push_back(std::move(message));
}
}
//======================================================================================================================
static int OnError(int status, const char *func_name, const char *err_msg, const char *file_name, int line, void *userdata)
{
PyGILState_STATE gstate;
gstate = PyGILState_Ensure();
PyObject *on_error = (PyObject*)userdata;
PyObject *args = Py_BuildValue("isssi", status, func_name, err_msg, file_name, line);
PyObject *r = PyObject_Call(on_error, args, NULL);
if (r == NULL) {
PyErr_Print();
} else {
Py_DECREF(r);
}
Py_DECREF(args);
PyGILState_Release(gstate);
return 0; // The return value isn't used
}
PyObject *pycvRedirectError(PyObject*, PyObject *args, PyObject *kw)
{
const char *keywords[] = { "on_error", NULL };
PyObject *on_error;
if (!PyArg_ParseTupleAndKeywords(args, kw, "O", (char**)keywords, &on_error))
return NULL;
if ((on_error != Py_None) && !PyCallable_Check(on_error)) {
PyErr_SetString(PyExc_TypeError, "on_error must be callable");
return NULL;
}
// Keep track of the previous handler parameter, so we can decref it when no longer used
static PyObject* last_on_error = NULL;
if (last_on_error) {
Py_DECREF(last_on_error);
last_on_error = NULL;
}
if (on_error == Py_None) {
ERRWRAP2(redirectError(NULL));
} else {
last_on_error = on_error;
Py_INCREF(last_on_error);
ERRWRAP2(redirectError(OnError, last_on_error));
}
Py_RETURN_NONE;
}
+141
View File
@@ -0,0 +1,141 @@
#ifndef CV2_UTIL_HPP
#define CV2_UTIL_HPP
#include "cv2.hpp"
#include "opencv2/core.hpp"
#include "opencv2/core/utils/tls.hpp"
#include <vector>
#include <string>
//======================================================================================================================
bool isPythonBindingsDebugEnabled();
void emit_failmsg(PyObject * exc, const char *msg);
int failmsg(const char *fmt, ...);
PyObject* failmsgp(const char *fmt, ...);
int cvDepthToNumpyType(int depth);
int numpyTypeToCvDepth(int typenum);
//======================================================================================================================
class PyAllowThreads
{
public:
PyAllowThreads() : _state(PyEval_SaveThread()) {}
~PyAllowThreads()
{
PyEval_RestoreThread(_state);
}
private:
PyThreadState* _state;
};
class PyEnsureGIL
{
public:
PyEnsureGIL() : _state(PyGILState_Ensure()) {}
~PyEnsureGIL()
{
PyGILState_Release(_state);
}
private:
PyGILState_STATE _state;
};
/**
* Light weight RAII wrapper for `PyObject*` owning references.
* In comparison to C++11 `std::unique_ptr` with custom deleter, it provides
* implicit conversion functions that might be useful to initialize it with
* Python functions those returns owning references through the `PyObject**`
* e.g. `PyErr_Fetch` or directly pass it to functions those want to borrow
* reference to object (doesn't extend object lifetime) e.g. `PyObject_Str`.
*/
class PySafeObject
{
public:
PySafeObject() : obj_(NULL) {}
explicit PySafeObject(PyObject* obj) : obj_(obj) {}
~PySafeObject()
{
Py_CLEAR(obj_);
}
operator PyObject*()
{
return obj_;
}
operator PyObject**()
{
return &obj_;
}
operator bool() {
return obj_ != nullptr;
}
PyObject* release()
{
PyObject* obj = obj_;
obj_ = NULL;
return obj;
}
private:
PyObject* obj_;
// Explicitly disable copy operations
PySafeObject(const PySafeObject*); // = delete
PySafeObject& operator=(const PySafeObject&); // = delete
};
//======================================================================================================================
extern PyObject* opencv_error;
void pyRaiseCVException(const cv::Exception &e);
#define ERRWRAP2(expr) \
try \
{ \
PyAllowThreads allowThreads; \
expr; \
} \
catch (const cv::Exception &e) \
{ \
pyRaiseCVException(e); \
return 0; \
} \
catch (const std::exception &e) \
{ \
PyErr_SetString(opencv_error, e.what()); \
return 0; \
} \
catch (...) \
{ \
PyErr_SetString(opencv_error, "Unknown C++ exception from OpenCV code"); \
return 0; \
}
//======================================================================================================================
extern cv::TLSData<std::vector<std::string> > conversionErrorsTLS;
inline void pyPrepareArgumentConversionErrorsStorage(std::size_t size)
{
std::vector<std::string>& conversionErrors = conversionErrorsTLS.getRef();
conversionErrors.clear();
conversionErrors.reserve(size);
}
void pyRaiseCVOverloadException(const std::string& functionName);
void pyPopulateArgumentConversionErrors();
//======================================================================================================================
PyObject *pycvRedirectError(PyObject*, PyObject *args, PyObject *kw);
#endif // CV2_UTIL_HPP
+1558
View File
File diff suppressed because it is too large Load Diff
+1255
View File
File diff suppressed because it is too large Load Diff
+371
View File
@@ -0,0 +1,371 @@
/*M///////////////////////////////////////////////////////////////////////////////////////
//
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
// By downloading, copying, installing or using the software you agree to this license.
// If you do not agree to this license, do not download, install,
// copy or use the software.
//
//
// License Agreement
// For Open Source Computer Vision Library
//
// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.
// Copyright (C) 2009-2011, Willow Garage Inc., all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistribution's of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistribution's in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * The name of the copyright holders may not be used to endorse or promote products
// derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
//M*/
// Defines for Python 2/3 compatibility.
#ifndef __PYCOMPAT_HPP__
#define __PYCOMPAT_HPP__
#include <string>
#if PY_MAJOR_VERSION >= 3
// Python3 treats all ints as longs, PyInt_X functions have been removed.
#define PyInt_Check PyLong_Check
#define PyInt_CheckExact PyLong_CheckExact
#define PyInt_AsLong PyLong_AsLong
#define PyInt_AS_LONG PyLong_AS_LONG
#define PyInt_AsUnsignedLongLongMask PyLong_AsUnsignedLongLongMask
#define PyInt_FromLong PyLong_FromLong
#define PyNumber_Int PyNumber_Long
#define PyString_FromString PyUnicode_FromString
#define PyString_FromStringAndSize PyUnicode_FromStringAndSize
#endif // PY_MAJOR >=3
#ifndef PyType_CheckExact
#define PyType_CheckExact(obj) (Py_TYPE(op) == &PyType_Type)
#endif // !PyType_CheckExact
static inline bool getUnicodeString(PyObject * obj, std::string &str)
{
bool res = false;
if (PyUnicode_Check(obj))
{
PyObject * bytes = PyUnicode_AsUTF8String(obj);
if (PyBytes_Check(bytes))
{
const char * raw = PyBytes_AsString(bytes);
if (raw)
{
str = std::string(raw);
res = true;
}
}
Py_XDECREF(bytes);
}
else if (PyBytes_Check(obj))
{
const char * raw = PyBytes_AsString(obj);
if (raw)
{
str = std::string(raw);
res = true;
}
}
#if PY_MAJOR_VERSION < 3
else if (PyString_Check(obj))
{
const char * raw = PyString_AsString(obj);
if (raw)
{
str = std::string(raw);
res = true;
}
}
#endif
return res;
}
static inline
std::string getPyObjectAttr(PyObject* obj, const char* attrName)
{
std::string obj_name;
PyObject* cls_name_obj = PyObject_GetAttrString(obj, attrName);
if (cls_name_obj && !getUnicodeString(cls_name_obj, obj_name)) {
obj_name.clear();
}
#ifndef Py_LIMITED_API
if (PyType_CheckExact(obj) && obj_name.empty())
{
obj_name = reinterpret_cast<PyTypeObject*>(obj)->tp_name;
}
#endif
if (obj_name.empty()) {
obj_name = "<UNAVAILABLE>";
}
return obj_name;
}
static inline
std::string getPyObjectNameAttr(PyObject* obj)
{
return getPyObjectAttr(obj, "__name__");
}
//==================================================================================================
#define CV_PY_FN_WITH_KW_(fn, flags) (PyCFunction)(void*)(PyCFunctionWithKeywords)(fn), (flags) | METH_VARARGS | METH_KEYWORDS
#define CV_PY_FN_NOARGS_(fn, flags) (PyCFunction)(fn), (flags) | METH_NOARGS
#define CV_PY_FN_WITH_KW(fn) CV_PY_FN_WITH_KW_(fn, 0)
#define CV_PY_FN_NOARGS(fn) CV_PY_FN_NOARGS_(fn, 0)
#define CV_PY_TO_CLASS(TYPE) \
template<> \
bool pyopencv_to(PyObject* dst, TYPE& src, const ArgInfo& info) \
{ \
if (!dst || dst == Py_None) \
return true; \
Ptr<TYPE> ptr; \
\
if (!pyopencv_to(dst, ptr, info)) return false; \
src = *ptr; \
return true; \
}
#define CV_PY_FROM_CLASS(TYPE) \
template<> \
PyObject* pyopencv_from(const TYPE& src) \
{ \
Ptr<TYPE> ptr(new TYPE()); \
\
*ptr = src; \
return pyopencv_from(ptr); \
}
#define CV_PY_TO_CLASS_PTR(TYPE) \
template<> \
bool pyopencv_to(PyObject* dst, TYPE*& src, const ArgInfo& info) \
{ \
if (!dst || dst == Py_None) \
return true; \
Ptr<TYPE> ptr; \
\
if (!pyopencv_to(dst, ptr, info)) return false; \
src = ptr; \
return true; \
}
#define CV_PY_FROM_CLASS_PTR(TYPE) \
static PyObject* pyopencv_from(TYPE*& src) \
{ \
return pyopencv_from(Ptr<TYPE>(src)); \
}
#define CV_PY_TO_ENUM(TYPE) \
template<> \
bool pyopencv_to(PyObject* dst, TYPE& src, const ArgInfo& info) \
{ \
if (!dst || dst == Py_None) \
return true; \
int underlying = 0; \
\
if (!pyopencv_to(dst, underlying, info)) return false; \
src = static_cast<TYPE>(underlying); \
return true; \
}
#define CV_PY_FROM_ENUM(TYPE) \
template<> \
PyObject* pyopencv_from(const TYPE& src) \
{ \
return pyopencv_from(static_cast<int>(src)); \
}
//==================================================================================================
#if PY_MAJOR_VERSION >= 3
#define CVPY_TYPE_HEAD PyVarObject_HEAD_INIT(&PyType_Type, 0)
#define CVPY_TYPE_INCREF(T) Py_INCREF(T)
#else
#define CVPY_TYPE_HEAD PyObject_HEAD_INIT(&PyType_Type) 0,
#define CVPY_TYPE_INCREF(T) _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA (T)->ob_refcnt++
#endif
#define CVPY_TYPE_DECLARE(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) \
struct pyopencv_##CLASS_ID##_t \
{ \
PyObject_HEAD \
STORAGE v; \
}; \
static PyTypeObject pyopencv_##CLASS_ID##_TypeXXX = \
{ \
CVPY_TYPE_HEAD \
MODULESTR SCOPE"."#EXPORT_NAME, \
sizeof(pyopencv_##CLASS_ID##_t), \
}; \
static PyTypeObject * pyopencv_##CLASS_ID##_TypePtr = &pyopencv_##CLASS_ID##_TypeXXX; \
static bool pyopencv_##CLASS_ID##_getp(PyObject * self, STORAGE * & dst) \
{ \
if (PyObject_TypeCheck(self, pyopencv_##CLASS_ID##_TypePtr)) \
{ \
dst = &(((pyopencv_##CLASS_ID##_t*)self)->v); \
return true; \
} \
return false; \
} \
static PyObject * pyopencv_##CLASS_ID##_Instance(const STORAGE &r) \
{ \
pyopencv_##CLASS_ID##_t *m = PyObject_NEW(pyopencv_##CLASS_ID##_t, pyopencv_##CLASS_ID##_TypePtr); \
new (&(m->v)) STORAGE(r); \
return (PyObject*)m; \
} \
static void pyopencv_##CLASS_ID##_dealloc(PyObject* self) \
{ \
((pyopencv_##CLASS_ID##_t*)self)->v.STORAGE::~SNAME(); \
PyObject_Del(self); \
} \
static PyObject* pyopencv_##CLASS_ID##_repr(PyObject* self) \
{ \
char str[1000]; \
snprintf(str, sizeof(str), "< " MODULESTR SCOPE"."#EXPORT_NAME" %p>", self); \
return PyString_FromString(str); \
}
#define CVPY_TYPE_INIT_STATIC(EXPORT_NAME, CLASS_ID, ERROR_HANDLER, BASE, CONSTRUCTOR, SCOPE) \
{ \
pyopencv_##CLASS_ID##_TypePtr->tp_base = pyopencv_##BASE##_TypePtr; \
pyopencv_##CLASS_ID##_TypePtr->tp_dealloc = pyopencv_##CLASS_ID##_dealloc; \
pyopencv_##CLASS_ID##_TypePtr->tp_repr = pyopencv_##CLASS_ID##_repr; \
pyopencv_##CLASS_ID##_TypePtr->tp_getset = pyopencv_##CLASS_ID##_getseters; \
pyopencv_##CLASS_ID##_TypePtr->tp_init = (initproc) CONSTRUCTOR; \
pyopencv_##CLASS_ID##_TypePtr->tp_methods = pyopencv_##CLASS_ID##_methods; \
pyopencv_##CLASS_ID##_TypePtr->tp_alloc = PyType_GenericAlloc; \
pyopencv_##CLASS_ID##_TypePtr->tp_new = PyType_GenericNew; \
pyopencv_##CLASS_ID##_TypePtr->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; \
if (PyType_Ready(pyopencv_##CLASS_ID##_TypePtr) != 0) \
{ \
ERROR_HANDLER; \
} \
CVPY_TYPE_INCREF(pyopencv_##CLASS_ID##_TypePtr); \
if (!registerNewType(m, #EXPORT_NAME, (PyObject*)pyopencv_##CLASS_ID##_TypePtr, SCOPE)) \
{ \
printf("Failed to register a new type: " #EXPORT_NAME ", base (" #BASE ") in " SCOPE " \n"); \
ERROR_HANDLER; \
} \
}
//==================================================================================================
#define CVPY_TYPE_DECLARE_DYNAMIC(EXPORT_NAME, CLASS_ID, STORAGE, SNAME, SCOPE) \
struct pyopencv_##CLASS_ID##_t \
{ \
PyObject_HEAD \
STORAGE v; \
}; \
static PyObject * pyopencv_##CLASS_ID##_TypePtr = 0; \
static bool pyopencv_##CLASS_ID##_getp(PyObject * self, STORAGE * & dst) \
{ \
if (PyObject_TypeCheck(self, (PyTypeObject*)pyopencv_##CLASS_ID##_TypePtr)) \
{ \
dst = &(((pyopencv_##CLASS_ID##_t*)self)->v); \
return true; \
} \
return false; \
} \
static PyObject * pyopencv_##CLASS_ID##_Instance(const STORAGE &r) \
{ \
pyopencv_##CLASS_ID##_t *m = PyObject_New(pyopencv_##CLASS_ID##_t, (PyTypeObject*)pyopencv_##CLASS_ID##_TypePtr); \
new (&(m->v)) STORAGE(r); \
return (PyObject*)m; \
} \
static void pyopencv_##CLASS_ID##_dealloc(PyObject* self) \
{ \
((pyopencv_##CLASS_ID##_t*)self)->v.STORAGE::~SNAME(); \
PyObject_Del(self); \
} \
static PyObject* pyopencv_##CLASS_ID##_repr(PyObject* self) \
{ \
char str[1000]; \
snprintf(str, sizeof(str), "< " MODULESTR SCOPE"."#EXPORT_NAME" %p>", self); \
return PyString_FromString(str); \
} \
static PyType_Slot pyopencv_##CLASS_ID##_Slots[] = \
{ \
{Py_tp_dealloc, 0}, \
{Py_tp_repr, 0}, \
{Py_tp_getset, 0}, \
{Py_tp_init, 0}, \
{Py_tp_methods, 0}, \
{Py_tp_alloc, 0}, \
{Py_tp_new, 0}, \
{0, 0} \
}; \
static PyType_Spec pyopencv_##CLASS_ID##_Spec = \
{ \
MODULESTR SCOPE"."#EXPORT_NAME, \
sizeof(pyopencv_##CLASS_ID##_t), \
0, \
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, \
pyopencv_##CLASS_ID##_Slots \
};
#define CVPY_TYPE_INIT_DYNAMIC(EXPORT_NAME, CLASS_ID, ERROR_HANDLER, BASE, CONSTRUCTOR, SCOPE) \
{ \
pyopencv_##CLASS_ID##_Slots[0].pfunc /*tp_dealloc*/ = (void*)pyopencv_##CLASS_ID##_dealloc; \
pyopencv_##CLASS_ID##_Slots[1].pfunc /*tp_repr*/ = (void*)pyopencv_##CLASS_ID##_repr; \
pyopencv_##CLASS_ID##_Slots[2].pfunc /*tp_getset*/ = (void*)pyopencv_##CLASS_ID##_getseters; \
pyopencv_##CLASS_ID##_Slots[3].pfunc /*tp_init*/ = (void*) CONSTRUCTOR; \
pyopencv_##CLASS_ID##_Slots[4].pfunc /*tp_methods*/ = pyopencv_##CLASS_ID##_methods; \
pyopencv_##CLASS_ID##_Slots[5].pfunc /*tp_alloc*/ = (void*)PyType_GenericAlloc; \
pyopencv_##CLASS_ID##_Slots[6].pfunc /*tp_new*/ = (void*)PyType_GenericNew; \
PyObject * bases = 0; \
if (pyopencv_##BASE##_TypePtr) \
bases = PyTuple_Pack(1, pyopencv_##BASE##_TypePtr); \
pyopencv_##CLASS_ID##_TypePtr = PyType_FromSpecWithBases(&pyopencv_##CLASS_ID##_Spec, bases); \
if (!pyopencv_##CLASS_ID##_TypePtr) \
{ \
printf("Failed to create type from spec: " #CLASS_ID ", base (" #BASE ")\n"); \
ERROR_HANDLER; \
} \
if (!registerNewType(m, #EXPORT_NAME, (PyObject*)pyopencv_##CLASS_ID##_TypePtr, SCOPE)) \
{ \
printf("Failed to register a new type: " #EXPORT_NAME ", base (" #BASE ") in " SCOPE " \n"); \
Py_DECREF(pyopencv_##CLASS_ID##_TypePtr); \
ERROR_HANDLER; \
} \
Py_DECREF(pyopencv_##CLASS_ID##_TypePtr); \
}
// Debug module load:
//
// else \
// { \
// printf("Init: " #NAME ", base (" #BASE ") -> %p" "\n", pyopencv_##NAME##_TypePtr); \
// } \
#endif // END HEADER GUARD
@@ -0,0 +1,35 @@
from .nodes import (
NamespaceNode,
ClassNode,
ClassProperty,
EnumerationNode,
FunctionNode,
ConstantNode,
TypeNode,
OptionalTypeNode,
TupleTypeNode,
AliasTypeNode,
SequenceTypeNode,
AnyTypeNode,
AggregatedTypeNode,
PathLikeTypeNode,
)
from .types_conversion import (
replace_template_parameters_with_placeholders,
get_template_instantiation_type,
create_type_node
)
from .ast_utils import (
SymbolName,
ScopeNotFoundError,
SymbolNotFoundError,
find_scope,
find_class_node,
create_class_node,
create_function_node,
resolve_enum_scopes
)
from .generation import generate_typing_stubs
@@ -0,0 +1,474 @@
__all__ = [
"apply_manual_api_refinement"
]
from typing import cast, Sequence, Callable, Iterable, Optional
from .nodes import (NamespaceNode, FunctionNode, OptionalTypeNode, TypeNode,
ClassProperty, PrimitiveTypeNode, ASTNodeTypeNode,
AggregatedTypeNode, CallableTypeNode, AnyTypeNode,
TupleTypeNode, UnionTypeNode, ProtocolClassNode,
DictTypeNode, ClassTypeNode, AliasRefTypeNode)
from .ast_utils import (find_function_node, SymbolName,
for_each_function_overload)
from .types_conversion import create_type_node
def apply_manual_api_refinement(root: NamespaceNode) -> None:
refine_highgui_module(root)
refine_cuda_module(root)
export_matrix_type_constants(root)
refine_dnn_module(root)
# Export OpenCV exception class
builtin_exception = root.add_class("Exception")
builtin_exception.is_exported = False
root.add_class("error", (builtin_exception, ), ERROR_CLASS_PROPERTIES)
for symbol_name, refine_symbol in NODES_TO_REFINE.items():
refine_symbol(root, symbol_name)
version_constant = root.add_constant("__version__", "<unused>")
version_constant._value_type = "str"
convert_returned_scalar_to_tuple(root)
"""
def redirectError(
onError: Callable[[int, str, str, str, int], None] | None
) -> None: ...
"""
root.add_function("redirectError", [
FunctionNode.Arg(
"onError",
OptionalTypeNode(
CallableTypeNode(
"ErrorCallback",
[
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.str_(),
PrimitiveTypeNode.int_()
]
)
)
)
])
def make_optional_none_return(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
"""
Make return type Optional[MatLike],
for the functions that may return None.
"""
function = find_function_node(root_node, function_symbol_name)
for overload in function.overloads:
if overload.return_type is not None:
if not isinstance(overload.return_type.type_node, OptionalTypeNode):
overload.return_type.type_node = OptionalTypeNode(
overload.return_type.type_node
)
def export_matrix_type_constants(root: NamespaceNode) -> None:
MAX_PREDEFINED_CHANNELS = 4
depth_names = ("CV_8U", "CV_8S", "CV_16U", "CV_16S", "CV_32U", "CV_32S",
"CV_64U", "CV_64S", "CV_32F", "CV_64F", "CV_16F", "CV_16BF" "CV_Bool")
for depth_value, depth_name in enumerate(depth_names):
# Export depth constants
root.add_constant(depth_name, str(depth_value))
# Export predefined types
for c in range(MAX_PREDEFINED_CHANNELS):
root.add_constant(f"{depth_name}C{c + 1}",
f"{depth_value + 8 * c}")
# Export type creation function
root.add_function(
f"{depth_name}C",
(FunctionNode.Arg("channels", PrimitiveTypeNode.int_()), ),
FunctionNode.RetType(PrimitiveTypeNode.int_())
)
# Export CV_MAKETYPE
root.add_function(
"CV_MAKETYPE",
(FunctionNode.Arg("depth", PrimitiveTypeNode.int_()),
FunctionNode.Arg("channels", PrimitiveTypeNode.int_())),
FunctionNode.RetType(PrimitiveTypeNode.int_())
)
def make_optional_arg(*arg_names: str) -> Callable[[NamespaceNode, SymbolName], None]:
def _make_optional_arg(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
function = find_function_node(root_node, function_symbol_name)
for arg_name in arg_names:
found_overload_with_arg = False
for overload in function.overloads:
arg_idx = _find_argument_index(overload.arguments, arg_name)
# skip overloads without this argument
if arg_idx is None:
continue
# Avoid multiplying optional qualification
if isinstance(overload.arguments[arg_idx].type_node, OptionalTypeNode):
continue
overload.arguments[arg_idx].type_node = OptionalTypeNode(
cast(TypeNode, overload.arguments[arg_idx].type_node)
)
found_overload_with_arg = True
if not found_overload_with_arg:
raise RuntimeError(
f"Failed to find argument with name: '{arg_name}'"
f" in '{function_symbol_name.name}' overloads"
)
return _make_optional_arg
def convert_returned_scalar_to_tuple(root: NamespaceNode) -> None:
"""Force `tuple[float, float, float, float]` usage instead of Scalar alias
for return types due to `pyopencv_from` specialization for Scalar type.
"""
float_4_tuple_node = TupleTypeNode(
"ScalarOutput",
items=(PrimitiveTypeNode.float_(),) * 4
)
def fix_scalar_return_type(fn: FunctionNode.Overload):
if fn.return_type is None:
return
if fn.return_type.type_node.typename == "Scalar":
fn.return_type.type_node = float_4_tuple_node
for overload in for_each_function_overload(root):
fix_scalar_return_type(overload)
for ns in root.namespaces.values():
for overload in for_each_function_overload(ns):
fix_scalar_return_type(overload)
def refine_cuda_module(root: NamespaceNode) -> None:
def fix_cudaoptflow_enums_names() -> None:
for class_name in ("NvidiaOpticalFlow_1_0", "NvidiaOpticalFlow_2_0"):
if class_name not in cuda_root.classes:
continue
opt_flow_class = cuda_root.classes[class_name]
_trim_class_name_from_argument_types(
for_each_function_overload(opt_flow_class), class_name
)
def fix_namespace_usage_scope(cuda_ns: NamespaceNode) -> None:
USED_TYPES = ("GpuMat", "Stream")
def fix_type_usage(type_node: TypeNode) -> None:
if isinstance(type_node, AggregatedTypeNode):
for item in type_node.items:
fix_type_usage(item)
if isinstance(type_node, ASTNodeTypeNode):
if type_node._typename in USED_TYPES:
type_node._typename = f"cuda_{type_node._typename}"
for overload in for_each_function_overload(cuda_ns):
if overload.return_type is not None:
fix_type_usage(overload.return_type.type_node)
for type_node in [arg.type_node for arg in overload.arguments
if arg.type_node is not None]:
fix_type_usage(type_node)
if "cuda" not in root.namespaces:
return
cuda_root = root.namespaces["cuda"]
fix_cudaoptflow_enums_names()
for ns in [ns for ns_name, ns in root.namespaces.items()
if ns_name.startswith("cuda")]:
fix_namespace_usage_scope(ns)
def refine_highgui_module(root: NamespaceNode) -> None:
# Check if library is built with enabled highgui module
if "destroyAllWindows" not in root.functions:
return
"""
def createTrackbar(trackbarName: str,
windowName: str,
value: int,
count: int,
onChange: Callable[[int], None]) -> None: ...
"""
root.add_function(
"createTrackbar",
[
FunctionNode.Arg("trackbarName", PrimitiveTypeNode.str_()),
FunctionNode.Arg("windowName", PrimitiveTypeNode.str_()),
FunctionNode.Arg("value", PrimitiveTypeNode.int_()),
FunctionNode.Arg("count", PrimitiveTypeNode.int_()),
FunctionNode.Arg("onChange",
CallableTypeNode("TrackbarCallback",
PrimitiveTypeNode.int_("int"))),
]
)
"""
def createButton(buttonName: str,
onChange: Callable[[tuple[int] | tuple[int, Any]], None],
userData: Any | None = ...,
buttonType: int = ...,
initialButtonState: int = ...) -> None: ...
"""
root.add_function(
"createButton",
[
FunctionNode.Arg("buttonName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"onChange",
CallableTypeNode(
"ButtonCallback",
UnionTypeNode(
"onButtonChangeCallbackData",
[
TupleTypeNode("onButtonChangeCallbackData",
[PrimitiveTypeNode.int_(), ]),
TupleTypeNode("onButtonChangeCallbackData",
[PrimitiveTypeNode.int_(),
AnyTypeNode("void*")])
]
)
)),
FunctionNode.Arg("userData",
OptionalTypeNode(AnyTypeNode("void*")),
default_value="None"),
FunctionNode.Arg("buttonType", PrimitiveTypeNode.int_(),
default_value="0"),
FunctionNode.Arg("initialButtonState", PrimitiveTypeNode.int_(),
default_value="0")
]
)
"""
def setMouseCallback(
windowName: str,
onMouse: Callback[[int, int, int, int, Any | None], None],
param: Any | None = ...
) -> None: ...
"""
root.add_function(
"setMouseCallback",
[
FunctionNode.Arg("windowName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"onMouse",
CallableTypeNode("MouseCallback", [
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_(),
OptionalTypeNode(AnyTypeNode("void*"))
])
),
FunctionNode.Arg("param", OptionalTypeNode(AnyTypeNode("void*")),
default_value="None")
]
)
def refine_dnn_module(root: NamespaceNode) -> None:
if "dnn" not in root.namespaces:
return
dnn_module = root.namespaces["dnn"]
"""
class LayerProtocol(Protocol):
def __init__(
self, params: dict[str, DictValue],
blobs: typing.Sequence[cv2.typing.MatLike]
) -> None: ...
def getMemoryShapes(
self, inputs: typing.Sequence[typing.Sequence[int]]
) -> typing.Sequence[typing.Sequence[int]]: ...
def forward(
self, inputs: typing.Sequence[cv2.typing.MatLike]
) -> typing.Sequence[cv2.typing.MatLike]: ...
"""
layer_proto = ProtocolClassNode("LayerProtocol", dnn_module)
layer_proto.add_function(
"__init__",
arguments=[
FunctionNode.Arg(
"params",
DictTypeNode(
"LayerParams", PrimitiveTypeNode.str_(),
create_type_node("cv::dnn::DictValue")
)
),
FunctionNode.Arg("blobs", create_type_node("vector<cv::Mat>"))
]
)
layer_proto.add_function(
"getMemoryShapes",
arguments=[
FunctionNode.Arg("inputs",
create_type_node("vector<vector<int>>"))
],
return_type=FunctionNode.RetType(
create_type_node("vector<vector<int>>")
)
)
layer_proto.add_function(
"forward",
arguments=[
FunctionNode.Arg("inputs", create_type_node("vector<cv::Mat>"))
],
return_type=FunctionNode.RetType(create_type_node("vector<cv::Mat>"))
)
"""
def dnn_registerLayer(layerTypeName: str,
layerClass: typing.Type[LayerProtocol]) -> None: ...
"""
root.add_function(
"dnn_registerLayer",
arguments=[
FunctionNode.Arg("layerTypeName", PrimitiveTypeNode.str_()),
FunctionNode.Arg(
"layerClass",
ClassTypeNode(ASTNodeTypeNode(
layer_proto.export_name, f"dnn.{layer_proto.export_name}"
))
)
]
)
"""
def dnn_unregisterLayer(layerTypeName: str) -> None: ...
"""
root.add_function(
"dnn_unregisterLayer",
arguments=[
FunctionNode.Arg("layerTypeName", PrimitiveTypeNode.str_())
]
)
def _trim_class_name_from_argument_types(
overloads: Iterable[FunctionNode.Overload],
class_name: str
) -> None:
separator = f"{class_name}_"
for overload in overloads:
for arg in [arg for arg in overload.arguments
if arg.type_node is not None]:
ast_node = cast(ASTNodeTypeNode, arg.type_node)
if class_name in ast_node.ctype_name:
fixed_name = ast_node._typename.split(separator)[-1]
ast_node._typename = fixed_name
def _find_argument_index(arguments: Sequence[FunctionNode.Arg],
name: str) -> Optional[int]:
for i, arg in enumerate(arguments):
if arg.name == name:
return i
return None
def make_matlike_or_scalar_arg(*arg_names: str) -> Callable[[NamespaceNode, SymbolName], None]:
"""Make arguments accept both MatLike and Scalar types.
This is used for functions like inRange where the C++ InputArray parameter
can accept both Mat objects and Scalar values (tuples, floats, etc.).
Example: cv2.inRange(img, (0, 0, 0), (255, 255, 255)) should be valid.
"""
def _make_matlike_or_scalar_arg(root_node: NamespaceNode,
function_symbol_name: SymbolName) -> None:
from .predefined_types import PREDEFINED_TYPES
function = find_function_node(root_node, function_symbol_name)
for arg_name in arg_names:
found_overload_with_arg = False
for overload in function.overloads:
arg_idx = _find_argument_index(overload.arguments, arg_name)
# skip overloads without this argument
if arg_idx is None:
continue
current_type = overload.arguments[arg_idx].type_node
# Check if it's already a union or if it already includes Scalar
if isinstance(current_type, UnionTypeNode):
# Check if Scalar is already in the union
has_scalar = any(
isinstance(item, AliasRefTypeNode) and item.typename == "Scalar"
for item in current_type.items
)
if has_scalar:
continue
# Add Scalar to existing union
scalar_ref = AliasRefTypeNode("Scalar")
current_type.items = current_type.items + (scalar_ref,)
else:
# Create a union of current type and Scalar
scalar_ref = AliasRefTypeNode("Scalar")
overload.arguments[arg_idx].type_node = UnionTypeNode(
f"{arg_name}_type",
(cast(TypeNode, current_type), scalar_ref)
)
found_overload_with_arg = True
if not found_overload_with_arg:
raise RuntimeError(
f"Failed to find argument with name: '{arg_name}'"
f" in '{function_symbol_name.name}' overloads"
)
return _make_matlike_or_scalar_arg
NODES_TO_REFINE = {
SymbolName(("cv", ), (), "resize"): make_optional_arg("dsize"),
SymbolName(("cv", ), (), "calcHist"): make_optional_arg("mask"),
SymbolName(("cv", ), (), "floodFill"): make_optional_arg("mask"),
SymbolName(("cv", ), ("Feature2D", ), "detectAndCompute"): make_optional_arg("mask"),
SymbolName(("cv", ), (), "findEssentialMat"): make_optional_arg(
"distCoeffs1", "distCoeffs2", "dist_coeff1", "dist_coeff2"
),
SymbolName(("cv", ), (), "drawFrameAxes"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "getOptimalNewCameraMatrix"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "initInverseRectificationMap"): make_optional_arg("distCoeffs", "R"),
SymbolName(("cv", ), (), "initUndistortRectifyMap"): make_optional_arg("distCoeffs", "R"),
SymbolName(("cv", ), (), "projectPoints"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solveP3P"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnP"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPGeneric"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRansac"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRefineLM"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "solvePnPRefineVVS"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "undistort"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "undistortPoints"): make_optional_arg("distCoeffs"),
SymbolName(("cv", ), (), "calibrateCamera"): make_optional_arg("cameraMatrix", "distCoeffs"),
SymbolName(("cv", "fisheye"), (), "initUndistortRectifyMap"): make_optional_arg("D"),
SymbolName(("cv", ), (), "imread"): make_optional_none_return,
SymbolName(("cv", ), (), "imdecode"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughCircles"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughLines"): make_optional_none_return,
SymbolName(("cv", ), (), "HoughLinesP"): make_optional_none_return,
# Fix for issue #28534: inRange should accept Scalar for lowerb and upperb
SymbolName(("cv", ), (), "inRange"): make_matlike_or_scalar_arg("lowerb", "upperb"),
}
ERROR_CLASS_PROPERTIES = (
ClassProperty("code", PrimitiveTypeNode.int_(), False),
ClassProperty("err", PrimitiveTypeNode.str_(), False),
ClassProperty("file", PrimitiveTypeNode.str_(), False),
ClassProperty("func", PrimitiveTypeNode.str_(), False),
ClassProperty("line", PrimitiveTypeNode.int_(), False),
ClassProperty("msg", PrimitiveTypeNode.str_(), False),
)
@@ -0,0 +1,440 @@
from typing import (NamedTuple, Sequence, Tuple, Union, List,
Dict, Callable, Optional, Generator, cast)
import keyword
from .nodes import (ASTNode, NamespaceNode, ClassNode, FunctionNode,
EnumerationNode, ClassProperty, OptionalTypeNode,
TupleTypeNode, PathLikeTypeNode)
from .types_conversion import create_type_node
class ScopeNotFoundError(Exception):
pass
class SymbolNotFoundError(Exception):
pass
class SymbolName(NamedTuple):
namespaces: Tuple[str, ...]
classes: Tuple[str, ...]
name: str
def __str__(self) -> str:
return '(namespace="{}", classes="{}", name="{}")'.format(
'::'.join(self.namespaces),
'::'.join(self.classes),
self.name
)
def __repr__(self) -> str:
return str(self)
@classmethod
def parse(cls, full_symbol_name: str,
known_namespaces: Sequence[str],
symbol_parts_delimiter: str = '.') -> "SymbolName":
"""Performs contextual symbol name parsing into namespaces, classes
and "bare" symbol name.
Args:
full_symbol_name (str): Input string to parse symbol name from.
known_namespaces (Sequence[str]): Collection of namespace that was
met during C++ headers parsing.
symbol_parts_delimiter (str, optional): Delimiter string used to
split `full_symbol_name` string into chunks. Defaults to '.'.
Returns:
SymbolName: Parsed symbol name structure.
>>> SymbolName.parse('cv.ns.Feature', ('cv', 'cv.ns'))
(namespace="cv::ns", classes="", name="Feature")
>>> SymbolName.parse('cv.ns.Feature', ())
(namespace="", classes="cv::ns", name="Feature")
>>> SymbolName.parse('cv.ns.Feature.Params', ('cv', 'cv.ns'))
(namespace="cv::ns", classes="Feature", name="Params")
>>> SymbolName.parse('cv::ns::Feature::Params::serialize',
... known_namespaces=('cv', 'cv.ns'),
... symbol_parts_delimiter='::')
(namespace="cv::ns", classes="Feature::Params", name="serialize")
"""
chunks = full_symbol_name.split(symbol_parts_delimiter)
namespaces, name = chunks[:-1], chunks[-1]
classes: List[str] = []
while len(namespaces) > 0 and '.'.join(namespaces) not in known_namespaces:
classes.insert(0, namespaces.pop())
return SymbolName(tuple(namespaces), tuple(classes), name)
def find_scope(root: NamespaceNode, symbol_name: SymbolName,
create_missing_namespaces: bool = True) -> Union[NamespaceNode, ClassNode]:
"""Traverses down nodes hierarchy to the direct parent of the node referred
by `symbol_name`.
Args:
root (NamespaceNode): Root node of the hierarchy.
symbol_name (SymbolName): Full symbol name to find scope for.
create_missing_namespaces (bool, optional): Set to True to create missing
namespaces while traversing the hierarchy. Defaults to True.
Raises:
ScopeNotFoundError: If direct parent for the node referred by `symbol_name`
can't be found e.g. one of classes doesn't exist.
Returns:
Union[NamespaceNode, ClassNode]: Direct parent for the node referred by
`symbol_name`.
>>> root = NamespaceNode('cv')
>>> algorithm_node = root.add_class('Algorithm')
>>> find_scope(root, SymbolName(('cv', ), ('Algorithm',), 'Params')) == algorithm_node
True
>>> root = NamespaceNode('cv')
>>> scope = find_scope(root, SymbolName(('cv', 'gapi', 'detail'), (), 'function'))
>>> scope.full_export_name
'cv.gapi.detail'
>>> root = NamespaceNode('cv')
>>> scope = find_scope(root, SymbolName(('cv', 'gapi'), ('GOpaque',), 'function'))
Traceback (most recent call last):
...
ast_utils.ScopeNotFoundError: Can't find a scope for 'function', with \
'(namespace="cv::gapi", classes="GOpaque", name="function")', \
because 'GOpaque' class is not registered yet
"""
assert isinstance(root, NamespaceNode), \
'Wrong hierarchy root type: {}'.format(type(root))
assert symbol_name.namespaces[0] == root.name, \
"Trying to find scope for '{}' with root namespace different from: '{}'".format(
symbol_name, root.name
)
scope: Union[NamespaceNode, ClassNode] = root
for namespace in symbol_name.namespaces[1:]:
if namespace not in scope.namespaces: # type: ignore
if not create_missing_namespaces:
raise ScopeNotFoundError(
"Can't find a scope for '{}', with '{}', because namespace"
" '{}' is not created yet and `create_missing_namespaces`"
" flag is set to False".format(
symbol_name.name, symbol_name, namespace
)
)
scope = scope.add_namespace(namespace) # type: ignore
else:
scope = scope.namespaces[namespace] # type: ignore
for class_name in symbol_name.classes:
if class_name not in scope.classes:
raise ScopeNotFoundError(
"Can't find a scope for '{}', with '{}', because '{}' "
"class is not registered yet".format(
symbol_name.name, symbol_name, class_name
)
)
scope = scope.classes[class_name]
return scope
def find_class_node(root: NamespaceNode, class_symbol: SymbolName,
create_missing_namespaces: bool = False) -> ClassNode:
scope = find_scope(root, class_symbol, create_missing_namespaces)
if class_symbol.name not in scope.classes:
raise SymbolNotFoundError(
"Can't find {} in its scope".format(class_symbol)
)
return scope.classes[class_symbol.name]
def find_function_node(root: NamespaceNode, function_symbol: SymbolName,
create_missing_namespaces: bool = False) -> FunctionNode:
scope = find_scope(root, function_symbol, create_missing_namespaces)
if function_symbol.name not in scope.functions:
raise SymbolNotFoundError(
"Can't find {} in its scope".format(function_symbol)
)
return scope.functions[function_symbol.name]
def create_function_node_in_scope(scope: Union[NamespaceNode, ClassNode],
func_info) -> FunctionNode:
def prepare_overload_arguments_and_return_type(variant):
arguments = [] # type: list[FunctionNode.Arg]
# Enumerate is required, because `argno` in `variant.py_arglist`
# refers to position of argument in C++ function interface,
# but `variant.py_noptargs` refers to position in `py_arglist`
for i, (_, argno) in enumerate(variant.py_arglist):
arg_info = variant.args[argno]
type_node = create_type_node(arg_info.tp)
# Special handling for string representation of the file system path
if arg_info.pathlike and type_node.typename == "str":
type_node = PathLikeTypeNode.string_or_pathlike_()
default_value = None
if len(arg_info.defval):
default_value = arg_info.defval
# If argument is optional and can be None - make its type optional
if variant.is_arg_optional(i):
# NOTE: should UMat be always mandatory for better type hints?
# otherwise overload won't be selected e.g. VideoCapture.read()
if arg_info.py_outputarg:
type_node = OptionalTypeNode(type_node)
default_value = "None"
elif arg_info.isbig() and "None" not in type_node.typename:
# but avoid duplication of the optioness
type_node = OptionalTypeNode(type_node)
arguments.append(
FunctionNode.Arg(arg_info.export_name, type_node=type_node,
default_value=default_value)
)
if func_info.isconstructor:
return arguments, None
# Function has more than 1 output argument, so its return type is a tuple
if len(variant.py_outlist) > 1:
ret_types = []
# Actual returned value of the function goes first
if variant.py_outlist[0][1] == -1:
ret_types.append(create_type_node(variant.rettype))
outlist = variant.py_outlist[1:]
else:
outlist = variant.py_outlist
for _, argno in outlist:
assert argno >= 0, \
f"Logic Error! Outlist contains function return type: {outlist}"
ret_types.append(create_type_node(variant.args[argno].tp))
return arguments, FunctionNode.RetType(
TupleTypeNode("return_type", ret_types)
)
# Function with 1 output argument in Python
if len(variant.py_outlist) == 1:
# Can be represented as a function with a non-void return type in C++
if variant.rettype:
return arguments, FunctionNode.RetType(
create_type_node(variant.rettype)
)
# or a function with void return type and output argument type
# such non-const reference
ret_type = variant.args[variant.py_outlist[0][1]].tp
return arguments, FunctionNode.RetType(
create_type_node(ret_type)
)
# Function without output types returns None in Python
return arguments, None
function_node = FunctionNode(func_info.name)
function_node.parent = scope
if func_info.isconstructor:
function_node.export_name = "__init__"
for variant in func_info.variants:
arguments, ret_type = prepare_overload_arguments_and_return_type(variant)
if isinstance(scope, ClassNode):
if func_info.is_static:
if ret_type is not None and ret_type.typename.endswith(scope.name):
function_node.is_classmethod = True
arguments.insert(0, FunctionNode.Arg("cls"))
else:
function_node.is_static = True
else:
arguments.insert(0, FunctionNode.Arg("self"))
function_node.add_overload(arguments, ret_type)
return function_node
def create_function_node(root: NamespaceNode, func_info) -> FunctionNode:
func_symbol_name = SymbolName(
func_info.namespace.split(".") if len(func_info.namespace) else (),
func_info.classname.split(".") if len(func_info.classname) else (),
func_info.name
)
return create_function_node_in_scope(find_scope(root, func_symbol_name),
func_info)
def create_class_node_in_scope(scope: Union[NamespaceNode, ClassNode],
symbol_name: SymbolName,
class_info) -> ClassNode:
properties = []
for property in class_info.props:
export_property_name = property.name
if keyword.iskeyword(export_property_name):
export_property_name += "_"
properties.append(
ClassProperty(
name=export_property_name,
type_node=create_type_node(property.tp),
is_readonly=property.readonly
)
)
class_node = scope.add_class(symbol_name.name,
properties=properties)
class_node.export_name = class_info.export_name
if class_info.constructor is not None:
create_function_node_in_scope(class_node, class_info.constructor)
for method in class_info.methods.values():
create_function_node_in_scope(class_node, method)
return class_node
def create_class_node(root: NamespaceNode, class_info,
namespaces: Sequence[str]) -> ClassNode:
symbol_name = SymbolName.parse(class_info.full_original_name, namespaces)
scope = find_scope(root, symbol_name)
return create_class_node_in_scope(scope, symbol_name, class_info)
def resolve_enum_scopes(root: NamespaceNode,
enums: Dict[SymbolName, EnumerationNode]):
"""Attaches all enumeration nodes to the appropriate classes and modules
If classes containing enumeration can't be found in the AST - they will
be created and marked as not exportable. This behavior is required to cover
cases, when enumeration is defined in base class, but only its derivatives
are used. Example:
```cpp
class CV_EXPORTS TermCriteria {
public:
enum Type { /* ... */ };
// ...
};
```
Args:
root (NamespaceNode): root of the reconstructed AST
enums (Dict[SymbolName, EnumerationNode]): Mapping between enumerations
symbol names and corresponding nodes without parents.
"""
for symbol_name, enum_node in enums.items():
if symbol_name.classes:
try:
scope = find_scope(root, symbol_name)
except ScopeNotFoundError:
# Scope can't be found if enumeration is a part of class
# that is not exported.
# Create class node, but mark it as not exported
for i, class_name in enumerate(symbol_name.classes):
scope = find_scope(root,
SymbolName(symbol_name.namespaces,
classes=symbol_name.classes[:i],
name=class_name))
if class_name in scope.classes:
continue
class_node = scope.add_class(class_name)
class_node.is_exported = False
scope = find_scope(root, symbol_name)
else:
scope = find_scope(root, symbol_name)
enum_node.parent = scope
def get_enclosing_namespace(
node: ASTNode,
class_node_callback: Optional[Callable[[ClassNode], None]] = None
) -> NamespaceNode:
"""Traverses up nodes hierarchy to find closest enclosing namespace of the
passed node
Args:
node (ASTNode): Node to find a namespace for.
class_node_callback (Optional[Callable[[ClassNode], None]]): Optional
callable object invoked for each traversed class node in bottom-up
order. Defaults: None.
Returns:
NamespaceNode: Closest enclosing namespace of the provided node.
Raises:
AssertionError: if nodes hierarchy missing a namespace node.
>>> root = NamespaceNode('cv')
>>> feature_class = root.add_class("Feature")
>>> get_enclosing_namespace(feature_class) == root
True
>>> root = NamespaceNode('cv')
>>> feature_class = root.add_class("Feature")
>>> feature_params_class = feature_class.add_class("Params")
>>> serialize_params_func = feature_params_class.add_function("serialize")
>>> get_enclosing_namespace(serialize_params_func) == root
True
>>> root = NamespaceNode('cv')
>>> detail_ns = root.add_namespace('detail')
>>> flags_enum = detail_ns.add_enumeration('Flags')
>>> get_enclosing_namespace(flags_enum) == detail_ns
True
"""
parent_node = node.parent
while not isinstance(parent_node, NamespaceNode):
assert parent_node is not None, \
"Can't find enclosing namespace for '{}' known as: '{}'".format(
node.full_export_name, node.native_name
)
if class_node_callback:
class_node_callback(cast(ClassNode, parent_node))
parent_node = parent_node.parent
return parent_node
def get_enum_module_and_export_name(enum_node: EnumerationNode) -> Tuple[str, str]:
"""Get export name of the enum node with its module name.
Note: Enumeration export names are prefixed with enclosing class names.
Args:
enum_node (EnumerationNode): Enumeration node to construct name for.
Returns:
Tuple[str, str]: a pair of enum export name and its full module name.
"""
enum_export_name = enum_node.export_name
def update_full_export_name(class_node: ClassNode) -> None:
nonlocal enum_export_name
enum_export_name = class_node.export_name + "_" + enum_export_name
namespace_node = get_enclosing_namespace(enum_node,
update_full_export_name)
return enum_export_name, namespace_node.full_export_name
def for_each_class(
node: Union[NamespaceNode, ClassNode]
) -> Generator[ClassNode, None, None]:
for cls in node.classes.values():
yield cls
if len(cls.classes):
yield from for_each_class(cls)
def for_each_function(
node: Union[NamespaceNode, ClassNode],
traverse_class_nodes: bool = True
) -> Generator[FunctionNode, None, None]:
yield from node.functions.values()
if traverse_class_nodes:
for cls in for_each_class(node):
yield from for_each_function(cls)
def for_each_function_overload(
node: Union[NamespaceNode, ClassNode],
traverse_class_nodes: bool = True
) -> Generator[FunctionNode.Overload, None, None]:
for func in for_each_function(node, traverse_class_nodes):
yield from func.overloads
if __name__ == '__main__':
import doctest
doctest.testmod()
@@ -0,0 +1,864 @@
__all__ = ("generate_typing_stubs", )
from io import StringIO
from pathlib import Path
import re
import shutil
from typing import (Callable, NamedTuple, Union, Set, Dict,
Collection, Tuple, List)
import warnings
from .ast_utils import (get_enclosing_namespace,
get_enum_module_and_export_name,
for_each_function_overload,
for_each_class)
from .predefined_types import PREDEFINED_TYPES
from .api_refinement import apply_manual_api_refinement
from .nodes import (ASTNode, ASTNodeType, NamespaceNode, ClassNode,
FunctionNode, EnumerationNode, ConstantNode,
ProtocolClassNode)
from .nodes.type_node import (TypeNode, AliasTypeNode, AliasRefTypeNode,
AggregatedTypeNode, ASTNodeTypeNode,
ConditionalAliasTypeNode, PrimitiveTypeNode)
def _clean_stale_stubs_dirs(stubs_root: Path) -> None:
"""Remove all subdirectories under stubs_root.
During incremental builds, disabling a previously enabled module leaves
behind its typing stub directory (e.g. cv2/gapi/). Removing all
subdirectories before regeneration ensures only stubs for currently
enabled modules are present. Top-level files (py.typed, __init__.pyi)
are kept because they are managed separately.
"""
if not stubs_root.is_dir():
return
for item in stubs_root.iterdir():
if item.is_dir():
shutil.rmtree(item)
def generate_typing_stubs(root: NamespaceNode, output_path: Path):
"""Generates typing stubs for the AST with root `root` and outputs
created files tree to directory pointed by `output_path`.
Stubs generation consist from 4 steps:
1. Reconstruction of AST tree for header parser output.
2. "Lazy" AST nodes resolution (type nodes used as function arguments
and return types). Resolution procedure attaches every "lazy"
AST node to the corresponding node in the AST created during step 1.
3. Generation of the typing module content. Typing module doesn't exist
in library code, but is essential place to define aliases widely used
in stub files.
4. Generation of typing stubs from the reconstructed AST.
Every namespace corresponds to a Python module with the same name.
Generation procedure is recursive repetition of the following steps
for each namespace (module):
- Collect and write required imports for the module
- Write all module constants stubs
- Write all module enumerations stubs
- Write all module classes stubs, preserving correct declaration
order, when base classes go before their derivatives.
- Write all module functions stubs
- Repeat steps above for nested namespaces
Args:
root (NamespaceNode): Root namespace node of the library AST.
output_path (Path): Path to output directory.
"""
# Perform special handling for function arguments that has some conventions
# not expressed in their API e.g. optionality of mutually exclusive arguments
# without default values:
# ```cxx
# cv::resize(cv::InputArray src, cv::OutputArray dst, cv::Size dsize,
# double fx = 0.0, double fy = 0.0, int interpolation);
# ```
# should accept `None` as `dsize`:
# ```python
# cv2.resize(image, dsize=None, fx=0.5, fy=0.5)
# ```
apply_manual_api_refinement(root)
# Most of the time type nodes miss their full name (especially function
# arguments and return types), so resolution should start from the narrowest
# scope and gradually expanded.
# Example:
# ```cpp
# namespace cv {
# enum AlgorithmType {
# // ...
# };
# namespace detail {
# struct Algorithm {
# static Ptr<Algorithm> create(AlgorithmType alg_type);
# };
# } // namespace detail
# } // namespace cv
# ```
# To resolve `alg_type` argument of function `create` having `AlgorithmType`
# type from above example the following steps are done:
# 1. Try to resolve against `cv::detail::Algorithm` - fail
# 2. Try to resolve against `cv::detail` - fail
# 3. Try to resolve against `cv` - success
# The whole process should fail !only! when all possible scopes are
# checked and at least 1 node is still unresolved.
root.resolve_type_nodes()
# Remove stale typing stub subdirectories from previous builds.
# In incremental builds, disabling a module (e.g. -DBUILD_opencv_gapi=OFF)
# no longer generates its stubs, but leftover directories from a previous
# build persist and propagate through the copy/install steps, causing
# type-checker errors for stubs referencing unavailable modules.
_clean_stale_stubs_dirs(Path(output_path) / root.export_name)
_generate_typing_module(root, output_path)
_populate_reexported_symbols(root)
_generate_typing_stubs(root, output_path)
def _generate_typing_stubs(root: NamespaceNode, output_path: Path) -> None:
output_path = Path(output_path) / root.export_name
output_path.mkdir(parents=True, exist_ok=True)
# Collect all imports required for module items declaration
required_imports = _collect_required_imports(root)
output_stream = StringIO()
# Add empty __all__ dunder on top of the module
output_stream.write("__all__: list[str] = []\n\n")
# Write required imports at the top of file
_write_required_imports(required_imports, output_stream)
_write_reexported_symbols_section(root, output_stream)
# NOTE: Enumerations require special handling, because all enumeration
# constants are exposed as module attributes
has_enums = _generate_section_stub(
StubSection("# Enumerations", ASTNodeType.Enumeration), root,
output_stream, 0
)
# Collect all enums from class level and export them to module level
for class_node in root.classes.values():
if _generate_enums_from_classes_tree(class_node, output_stream,
indent=0):
has_enums = True
# 2 empty lines between enum and classes definitions
if has_enums:
output_stream.write("\n")
# Write the rest of module content - classes and functions
for section in STUB_SECTIONS:
_generate_section_stub(section, root, output_stream, 0)
# Dump content to the output file
(output_path / "__init__.pyi").write_text(output_stream.getvalue())
# Process nested namespaces
for ns in root.namespaces.values():
_generate_typing_stubs(ns, output_path)
class StubSection(NamedTuple):
name: str
node_type: ASTNodeType
STUB_SECTIONS = (
StubSection("# Constants", ASTNodeType.Constant),
# Enumerations are skipped due to special handling rules
# StubSection("# Enumerations", ASTNodeType.Enumeration),
StubSection("# Classes", ASTNodeType.Class),
StubSection("# Functions", ASTNodeType.Function)
)
def _generate_section_stub(section: StubSection, node: ASTNode,
output_stream: StringIO, indent: int) -> bool:
"""Generates stub for a single type of children nodes of the provided node.
Args:
section (StubSection): section identifier that carries section name and
type its nodes.
node (ASTNode): root node with children nodes used for
output_stream (StringIO): Output stream for all nodes stubs related to
the given section.
indent (int): Indent used for each line written to `output_stream`.
Returns:
bool: `True` if section has a content, `False` otherwise.
"""
if section.node_type not in node._children:
return False
children = node._children[section.node_type]
if len(children) == 0:
return False
output_stream.write(" " * indent)
output_stream.write(section.name)
output_stream.write("\n")
stub_generator = NODE_TYPE_TO_STUB_GENERATOR[section.node_type]
children = filter(lambda c: c.is_exported, children.values()) # type: ignore
if hasattr(section.node_type, "weight"):
children = sorted(children, key=lambda child: getattr(child, "weight")) # type: ignore
for child in children:
stub_generator(child, output_stream, indent) # type: ignore
output_stream.write("\n")
return True
def _generate_class_stub(class_node: ClassNode, output_stream: StringIO,
indent: int = 0) -> None:
"""Generates stub for the provided class node.
Rules:
- Read/write properties are converted to object attributes.
- Readonly properties are converted to functions decorated with `@property`.
- When return type of static functions matches class name - these functions
are treated as factory functions and annotated with `@classmethod`.
- In contrast to implicit `this` argument in C++ methods, in Python all
"normal" methods have explicit `self` as their first argument.
- Body of empty classes is replaced with `...`
Example:
```cpp
struct Object : public BaseObject {
struct InnerObject {
int param;
bool param2;
float readonlyParam();
};
Object(int param, bool param2 = false);
Object(InnerObject obj);
static Object create();
};
```
becomes
```python
class Object(BaseObject):
class InnerObject:
param: int
param2: bool
@property
def readonlyParam() -> float: ...
@typing.override
def __init__(self, param: int, param2: bool = ...) -> None: ...
@typing.override
def __init__(self, obj: "Object.InnerObject") -> None: ...
@classmethod
def create(cls) -> Object: ...
```
Args:
class_node (ClassNode): Class node to generate stub entry for.
output_stream (StringIO): Output stream for class stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
"""
class_module = get_enclosing_namespace(class_node)
class_module_name = class_module.full_export_name
if len(class_node.bases) > 0:
bases = []
for base in class_node.bases:
base_module = get_enclosing_namespace(base) # type: ignore
if base_module != class_module:
bases.append(base.full_export_name)
else:
bases.append(base.export_name)
inheritance_str = f"({', '.join(bases)})"
elif isinstance(class_node, ProtocolClassNode):
inheritance_str = "(Protocol)"
else:
inheritance_str = ""
output_stream.write(
"{indent}class {name}{bases}:\n".format(
indent=" " * indent,
name=class_node.export_name,
bases=inheritance_str
)
)
has_content = len(class_node.properties) > 0
# Processing class properties
for property in class_node.properties:
if property.is_readonly:
template = "{indent}@property\n{indent}def {name}(self) -> {type}: ...\n"
else:
template = "{indent}{name}: {type}\n"
output_stream.write(
template.format(indent=" " * (indent + 4),
name=property.name,
type=property.relative_typename(class_module_name))
)
if len(class_node.properties) > 0:
output_stream.write("\n")
for section in STUB_SECTIONS:
if _generate_section_stub(section, class_node,
output_stream, indent + 4):
has_content = True
if not has_content:
output_stream.write(" " * (indent + 4))
output_stream.write("...\n\n")
def _generate_constant_stub(constant_node: ConstantNode,
output_stream: StringIO, indent: int = 0,
extra_export_prefix: str = "",
generate_uppercase_version: bool = True) -> Tuple[str, ...]:
"""Generates stub for the provided constant node.
Args:
constant_node (ConstantNode): Constant node to generate stub entry for.
output_stream (StringIO): Output stream for constant stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
extra_export_prefix (str, optional): Extra prefix added to the export
constant name. Defaults to empty string.
generate_uppercase_version (bool, optional): Generate uppercase version
alongside the normal one. Defaults to True.
Returns:
Tuple[str, ...]: exported constants names.
"""
def write_constant_to_stream(export_name: str) -> None:
output_stream.write(
"{indent}{name}: {value_type}\n".format(
name=export_name,
value_type=constant_node.value_type,
indent=" " * indent
)
)
export_name = extra_export_prefix + constant_node.export_name
write_constant_to_stream(export_name)
if generate_uppercase_version:
# Handle Python "magic" constants like __version__
if re.match(r"^__.*__$", export_name) is not None:
return export_name,
uppercase_name = re.sub(r"([a-z])([A-Z])", r"\1_\2", export_name).upper()
if export_name != uppercase_name:
write_constant_to_stream(uppercase_name)
return export_name, uppercase_name
return export_name,
def _generate_enumeration_stub(enumeration_node: EnumerationNode,
output_stream: StringIO, indent: int = 0,
extra_export_prefix: str = "") -> None:
"""Generates stub for the provided enumeration node. In contrast to the
Python `enum.Enum` class, C++ enumerations are exported as module-level
(or class-level) constants.
Example:
```cpp
enum Flags {
Flag1 = 0,
Flag2 = 1,
Flag3
};
```
becomes
```python
Flag1: int
Flag2: int
Flag3: int
Flags = int # One of [Flag1, Flag2, Flag3]
```
Unnamed enumerations don't export their names to Python:
```cpp
enum {
Flag1 = 0,
Flag2 = 1
};
```
becomes
```python
Flag1: int
Flag2: int
```
Scoped enumeration adds its name before each item name:
```cpp
enum struct ScopedEnum {
Flag1,
Flag2
};
```
becomes
```python
ScopedEnum_Flag1: int
ScopedEnum_Flag2: int
ScopedEnum = int # One of [ScopedEnum_Flag1, ScopedEnum_Flag2]
```
Args:
enumeration_node (EnumerationNode): Enumeration node to generate stub entry for.
output_stream (StringIO): Output stream for enumeration stub.
indent (int, optional): Indent used for each line written to `output_stream`.
Defaults to 0.
extra_export_prefix (str, optional) Extra prefix added to the export
enumeration name. Defaults to empty string.
"""
entries_extra_prefix = extra_export_prefix
if enumeration_node.is_scoped:
entries_extra_prefix += enumeration_node.export_name + "_"
generated_constants_entries: List[str] = []
for entry in enumeration_node.constants.values():
generated_constants_entries.extend(
_generate_constant_stub(entry, output_stream, indent, entries_extra_prefix)
)
# Unnamed enumerations are skipped as definition
if enumeration_node.export_name.endswith("<unnamed>"):
output_stream.write("\n")
return
output_stream.write(
'{indent}{export_prefix}{name} = int\n{indent}"""One of [{entries}]"""\n\n'.format(
export_prefix=extra_export_prefix,
name=enumeration_node.export_name,
entries=", ".join(generated_constants_entries),
indent=" " * indent
)
)
def _generate_function_stub(function_node: FunctionNode,
output_stream: StringIO, indent: int = 0) -> None:
"""Generates stub entry for the provided function node. Function node can
refer free function or class method.
Args:
function_node (FunctionNode): Function node to generate stub entry for.
output_stream (StringIO): Output stream for function stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
"""
# Function is a stub without any arguments information
if not function_node.overloads:
warnings.warn(
'Function node "{}" exported as "{}" has no overloads'.format(
function_node.full_name, function_node.full_export_name
)
)
return
decorators = []
if function_node.is_classmethod:
decorators.append(" " * indent + "@classmethod")
elif function_node.is_static:
decorators.append(" " * indent + "@staticmethod")
if len(function_node.overloads) > 1:
decorators.append(" " * indent + "@_typing.overload")
function_module = get_enclosing_namespace(function_node)
function_module_name = function_module.full_export_name
for overload in function_node.overloads:
# Annotate every function argument
annotated_args = []
for arg in overload.arguments:
annotated_arg = arg.name
typename = arg.relative_typename(function_module_name)
if typename is not None:
annotated_arg += ": " + typename
if arg.default_value is not None:
annotated_arg += " = ..."
annotated_args.append(annotated_arg)
# And convert return type to the actual type
if overload.return_type is not None:
ret_type = overload.return_type.relative_typename(function_module_name)
else:
ret_type = "None"
output_stream.write(
"{decorators}"
"{indent}def {name}({args}) -> {ret_type}: ...\n".format(
decorators="\n".join(decorators) +
"\n" if len(decorators) > 0 else "",
name=function_node.export_name,
args=", ".join(annotated_args),
ret_type=ret_type,
indent=" " * indent
)
)
output_stream.write("\n")
def _generate_enums_from_classes_tree(class_node: ClassNode,
output_stream: StringIO,
indent: int = 0,
class_name_prefix: str = "") -> bool:
"""Recursively generates class-level enumerations on the module level
starting from the `class_node`.
NOTE: This function is required, because all enumerations are exported as
module-level constants.
Example:
```cpp
namespace cv {
struct TermCriteria {
enum Type {
COUNT = 1,
MAX_ITER = COUNT,
EPS = 2
};
};
} // namespace cv
```
is exported to `__init__.pyi` of `cv` module as as
```python
TermCriteria_COUNT: int
TermCriteria_MAX_ITER: int
TermCriteria_EPS: int
TermCriteria_Type = int # One of [COUNT, MAX_ITER, EPS]
```
Args:
class_node (ClassNode): Class node to generate enumerations stubs for.
output_stream (StringIO): Output stream for enumerations stub.
indent (int, optional): Indent used for each line written to
`output_stream`. Defaults to 0.
class_name_prefix (str, optional): Prefix used for enumerations and
constants names. Defaults to "".
Returns:
bool: `True` if classes tree declares at least 1 enum, `False` otherwise.
"""
class_name_prefix = class_node.export_name + "_" + class_name_prefix
has_content = len(class_node.enumerations) > 0
for enum_node in class_node.enumerations.values():
_generate_enumeration_stub(enum_node, output_stream, indent,
class_name_prefix)
for cls in class_node.classes.values():
if _generate_enums_from_classes_tree(cls, output_stream, indent,
class_name_prefix):
has_content = True
return has_content
def check_overload_presence(node: Union[NamespaceNode, ClassNode]) -> bool:
"""Checks that node has at least 1 function with overload.
Args:
node (Union[NamespaceNode, ClassNode]): Node to check for overload
presence.
Returns:
bool: True if input node has at least 1 function with overload, False
otherwise.
"""
for func_node in node.functions.values():
if len(func_node.overloads) > 1:
return True
return False
def _collect_required_imports(root: NamespaceNode) -> Collection[str]:
"""Collects all imports required for classes and functions typing stubs
declarations.
Args:
root (NamespaceNode): Namespace node to collect imports for
Returns:
Collection[str]: Collection of unique `import smth` statements required
for classes and function declarations of `root` node.
"""
def _add_required_usage_imports(type_node: TypeNode, imports: Set[str]):
for required_import in type_node.required_usage_imports:
imports.add(required_import)
required_imports: Set[str] = set()
# Check if typing module is required due to @overload decorator usage
# Looking for module-level function with at least 1 overload
has_overload = check_overload_presence(root)
# if there is no module-level functions with overload, check its presence
# during class traversing, including their inner-classes
has_protocol = False
for cls in for_each_class(root):
if not has_overload and check_overload_presence(cls):
has_overload = True
required_imports.add("import typing as _typing")
# Add required imports for class properties
for prop in cls.properties:
_add_required_usage_imports(prop.type_node, required_imports)
# Add required imports for class bases
for base in cls.bases:
base_namespace = get_enclosing_namespace(base) # type: ignore
if base_namespace != root:
required_imports.add(
"import " + base_namespace.full_export_name
)
if isinstance(cls, ProtocolClassNode):
has_protocol = True
if has_overload:
required_imports.add("import typing as _typing")
# Importing modules required to resolve functions arguments
for overload in for_each_function_overload(root):
for arg in filter(lambda a: a.type_node is not None,
overload.arguments):
_add_required_usage_imports(arg.type_node, required_imports) # type: ignore
if overload.return_type is not None:
_add_required_usage_imports(overload.return_type.type_node,
required_imports)
root_import = "import " + root.full_export_name
if root_import in required_imports:
required_imports.remove(root_import)
if has_protocol:
required_imports.add("import sys")
ordered_required_imports = sorted(required_imports)
# Protocol import always goes as last import statement
if has_protocol:
ordered_required_imports.append(
"""if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol"""
)
return ordered_required_imports
def _populate_reexported_symbols(root: NamespaceNode) -> None:
# Re-export all submodules to allow referencing symbols in submodules
# without submodule import. Example:
# `cv2.aruco.ArucoDetector` should be accessible without `import cv2.aruco`
def _reexport_submodule(ns: NamespaceNode) -> None:
for submodule in ns.namespaces.values():
ns.reexported_submodules.append(submodule.export_name)
_reexport_submodule(submodule)
_reexport_submodule(root)
root.reexported_submodules.append("typing")
# Special cases, symbols defined in possible pure Python submodules
# should be
root.reexported_submodules_symbols["mat_wrapper"].append("Mat")
def _write_reexported_symbols_section(module: NamespaceNode,
output_stream: StringIO) -> None:
"""Write re-export section for the given module.
Re-export statements have from `from module_name import smth as smth`.
Example:
```python
from cv2 import aruco as aruco
from cv2 import cuda as cuda
from cv2 import ml as ml
from cv2.mat_wrapper import Mat as Mat
```
Args:
module (NamespaceNode): Module with re-exported symbols.
output_stream (StringIO): Output stream for re-export statements.
"""
parent_name = module.full_export_name
for submodule in sorted(module.reexported_submodules):
output_stream.write(
"from {0} import {1} as {1}\n".format(parent_name, submodule)
)
for submodule, symbols in sorted(module.reexported_submodules_symbols.items(),
key=lambda kv: kv[0]):
for symbol in symbols:
output_stream.write(
"from {0}.{1} import {2} as {2}\n".format(
parent_name, submodule, symbol
)
)
if len(module.reexported_submodules) or \
len(module.reexported_submodules_symbols):
output_stream.write("\n\n")
def _write_required_imports(required_imports: Collection[str],
output_stream: StringIO) -> None:
"""Writes all entries of `required_imports` to the `output_stream`.
Args:
required_imports (Collection[str]): Imports to write into the output
stream.
output_stream (StringIO): Output stream for import statements.
"""
for required_import in required_imports:
output_stream.write(required_import)
output_stream.write("\n")
if len(required_imports):
output_stream.write("\n\n")
def _generate_typing_module(root: NamespaceNode, output_path: Path) -> None:
"""Generates stub file for typings module.
Actual module doesn't exist, but it is an appropriate place to define
all widely-used aliases.
Args:
root (NamespaceNode): AST root node used for type nodes resolution.
output_path (Path): Path to typing module directory, where __init__.pyi
will be written.
"""
def has_all_required_modules(type_node: TypeNode) -> bool:
return all(em in root.namespaces for em in type_node.required_modules)
def register_alias_links_from_aggregated_type(type_node: TypeNode) -> None:
assert isinstance(type_node, AggregatedTypeNode), \
f"Provided type node '{type_node.ctype_name}' is not an aggregated type"
for item in filter(lambda i: isinstance(i, AliasRefTypeNode), type_node):
type_node = PREDEFINED_TYPES[item.ctype_name]
if isinstance(type_node, AliasTypeNode):
register_alias(type_node)
elif isinstance(type_node, ConditionalAliasTypeNode):
conditional_type_nodes[type_node.ctype_name] = type_node
def create_alias_for_enum_node(enum_node_alias: AliasTypeNode) -> ConditionalAliasTypeNode:
"""Create conditional int alias corresponding to the given enum node.
Args:
enum_node (AliasTypeNode): Enumeration node to create conditional
int alias for.
Returns:
ConditionalAliasTypeNode: conditional int alias node with same
export name as enum.
"""
enum_node = enum_node_alias.ast_node
assert enum_node.node_type == ASTNodeType.Enumeration, \
f"{enum_node} has wrong node type. Expected type: Enumeration."
enum_export_name, enum_module_name = get_enum_module_and_export_name(
enum_node
)
return ConditionalAliasTypeNode(
enum_export_name,
"_typing.TYPE_CHECKING",
positive_branch_type=enum_node_alias,
negative_branch_type=PrimitiveTypeNode.int_(enum_export_name),
condition_required_imports=("import typing as _typing", )
)
def register_alias(alias_node: AliasTypeNode) -> None:
typename = alias_node.typename
# Check if alias is already registered
if typename in aliases:
return
# Collect required imports for alias definition
for required_import in alias_node.required_definition_imports:
required_imports.add(required_import)
if isinstance(alias_node.value, AggregatedTypeNode):
# Check if collection contains a link to another alias
register_alias_links_from_aggregated_type(alias_node.value)
# Remove references to alias nodes
for i, item in enumerate(alias_node.value.items):
# Process enumerations only
if not isinstance(item, ASTNodeTypeNode) or item.ast_node is None:
continue
if item.ast_node.node_type != ASTNodeType.Enumeration:
continue
enum_node = create_alias_for_enum_node(item)
alias_node.value.items[i] = enum_node
conditional_type_nodes[enum_node.ctype_name] = enum_node
if isinstance(alias_node.value, ASTNodeTypeNode) \
and alias_node.value.ast_node == ASTNodeType.Enumeration:
enum_node = create_alias_for_enum_node(alias_node.ast_node)
conditional_type_nodes[enum_node.ctype_name] = enum_node
return
# Strip module prefix from aliased types
aliases[typename] = alias_node.value.full_typename.replace(
root.export_name + ".typing.", ""
)
if alias_node.doc is not None:
aliases[typename] += f'\n"""{alias_node.doc}"""'
output_path = Path(output_path) / root.export_name / "typing"
output_path.mkdir(parents=True, exist_ok=True)
required_imports: Set[str] = set()
aliases: Dict[str, str] = {}
conditional_type_nodes: Dict[str, ConditionalAliasTypeNode] = {}
# Resolve each node and register aliases
TypeNode.compatible_to_runtime_usage = True
for node in PREDEFINED_TYPES.values():
# if node does not have at least one required module skip it
# e.g. GArgs requires G-API module, so if build without G-API GArgs is not included
if not has_all_required_modules(node):
continue
node.resolve(root)
if isinstance(node, AliasTypeNode):
register_alias(node)
elif isinstance(node, ConditionalAliasTypeNode):
conditional_type_nodes[node.ctype_name] = node
for node in conditional_type_nodes.values():
for required_import in node.required_definition_imports:
required_imports.add(required_import)
output_stream = StringIO()
output_stream.write("__all__ = [\n")
for alias_name in aliases:
output_stream.write(f' "{alias_name}",\n')
output_stream.write("]\n\n")
_write_required_imports(required_imports, output_stream)
# Add type checking time definitions as generated __init__.py content
for _, type_node in conditional_type_nodes.items():
output_stream.write(f"if {type_node.condition}:\n ")
output_stream.write(f"{type_node.typename} = {type_node.positive_branch_type.full_typename}\nelse:\n")
output_stream.write(f" {type_node.typename} = {type_node.negative_branch_type.full_typename}\n\n\n")
for alias_name, alias_type in aliases.items():
output_stream.write(f"{alias_name} = {alias_type}\n")
TypeNode.compatible_to_runtime_usage = False
(output_path / "__init__.py").write_text(output_stream.getvalue())
StubGenerator = Callable[[ASTNode, StringIO, int], None]
NODE_TYPE_TO_STUB_GENERATOR = {
ASTNodeType.Class: _generate_class_stub,
ASTNodeType.Constant: _generate_constant_stub,
ASTNodeType.Enumeration: _generate_enumeration_stub,
ASTNodeType.Function: _generate_function_stub
}
@@ -0,0 +1,12 @@
from .node import ASTNode, ASTNodeType
from .namespace_node import NamespaceNode
from .class_node import ClassNode, ClassProperty, ProtocolClassNode
from .function_node import FunctionNode
from .enumeration_node import EnumerationNode
from .constant_node import ConstantNode
from .type_node import (
TypeNode, OptionalTypeNode, UnionTypeNode, NoneTypeNode, TupleTypeNode,
ASTNodeTypeNode, AliasTypeNode, SequenceTypeNode, AnyTypeNode,
AggregatedTypeNode, NDArrayTypeNode, AliasRefTypeNode, PrimitiveTypeNode,
CallableTypeNode, DictTypeNode, ClassTypeNode, PathLikeTypeNode
)
@@ -0,0 +1,190 @@
from typing import Type, Sequence, NamedTuple, Optional, Tuple, Dict
import itertools
import weakref
from .node import ASTNode, ASTNodeType
from .function_node import FunctionNode
from .enumeration_node import EnumerationNode
from .constant_node import ConstantNode
from .type_node import TypeNode, TypeResolutionError
class ClassProperty(NamedTuple):
name: str
type_node: TypeNode
is_readonly: bool
@property
def typename(self) -> str:
return self.type_node.full_typename
def resolve_type_nodes(self, root: ASTNode) -> None:
try:
self.type_node.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve "{}" property'.format(self.name)
) from e
def relative_typename(self, full_node_name: str) -> str:
"""Typename relative to the passed AST node name.
Args:
full_node_name (str): Full export name of the AST node
Returns:
str: typename relative to the passed AST node name
"""
return self.type_node.relative_typename(full_node_name)
class ClassNode(ASTNode):
"""Represents a C++ class that is also a class in Python.
ClassNode can have functions (methods), enumerations, constants and other
classes as its children nodes.
Class properties are not treated as a part of AST for simplicity and have
extra handling if required.
"""
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> None:
super().__init__(name, parent, export_name)
self.bases = list(bases)
self.properties = properties
@property
def weight(self) -> int:
return 1 + sum(base.weight for base in self.bases)
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Class
@property
def classes(self) -> Dict[str, "ClassNode"]:
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_class(self, name: str,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> "ClassNode":
return self._add_child(ClassNode, name, bases=bases,
properties=properties)
def add_function(self, name: str, arguments: Sequence[FunctionNode.Arg] = (),
return_type: Optional[FunctionNode.RetType] = None,
is_static: bool = False) -> FunctionNode:
"""Adds function as a child node of a class.
Function is classified in 3 categories:
1. Instance method.
If function is an instance method then `self` argument is
inserted at the beginning of its arguments list.
2. Class method (or factory method)
If `is_static` flag is `True` and typename of the function
return type matches name of the class then function is treated
as class method.
If function is a class method then `cls` argument is inserted
at the beginning of its arguments list.
3. Static method
Args:
name (str): Name of the function.
arguments (Sequence[FunctionNode.Arg], optional): Function arguments.
Defaults to ().
return_type (Optional[FunctionNode.RetType], optional): Function
return type. Defaults to None.
is_static (bool, optional): Flag whenever function is static or not.
Defaults to False.
Returns:
FunctionNode: created function node.
"""
arguments = list(arguments)
if return_type is not None:
is_classmethod = return_type.typename == self.name
else:
is_classmethod = False
if not is_static:
arguments.insert(0, FunctionNode.Arg("self"))
elif is_classmethod:
is_static = False
arguments.insert(0, FunctionNode.Arg("cls"))
return self._add_child(FunctionNode, name, arguments=arguments,
return_type=return_type, is_static=is_static,
is_classmethod=is_classmethod)
def add_enumeration(self, name: str) -> EnumerationNode:
return self._add_child(EnumerationNode, name)
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
def add_base(self, base_class_node: "ClassNode") -> None:
self.bases.append(weakref.proxy(base_class_node))
def resolve_type_nodes(self, root: ASTNode) -> None:
"""Resolves type nodes for all inner-classes, methods and properties
in 2 steps:
1. Resolve against `self` as a tree root
2. Resolve against `root` as a tree root
Type resolution errors are postponed until all children nodes are
examined.
Args:
root (Optional[ASTNode], optional): Root of the AST sub-tree.
Defaults to None.
"""
errors = []
for child in itertools.chain(self.properties,
self.functions.values(),
self.classes.values()):
try:
try:
# Give priority to narrowest scope (class-level scope in this case)
child.resolve_type_nodes(self) # type: ignore
except TypeResolutionError:
child.resolve_type_nodes(root) # type: ignore
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" class against "{}". Errors: {}'.format(
self.full_export_name, root.full_export_name, errors
)
)
class ProtocolClassNode(ClassNode):
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None,
properties: Sequence[ClassProperty] = ()) -> None:
super().__init__(name, parent, export_name, bases=(),
properties=properties)
@@ -0,0 +1,31 @@
from typing import Optional, Tuple
from .node import ASTNode, ASTNodeType
class ConstantNode(ASTNode):
"""Represents C++ constant that is also a constant in Python.
"""
def __init__(self, name: str, value: str,
parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.value = value
self._value_type = "int"
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return ()
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Constant
@property
def value_type(self) -> str:
return self._value_type
def __str__(self) -> str:
return "Constant('{}' exported as '{}': {})".format(
self.name, self.export_name, self.value
)
@@ -0,0 +1,33 @@
from typing import Type, Tuple, Optional, Dict
from .node import ASTNode, ASTNodeType
from .constant_node import ConstantNode
class EnumerationNode(ASTNode):
"""Represents C++ enumeration that treated as named set of constants in
Python.
EnumerationNode can have only constants as its children nodes.
"""
def __init__(self, name: str, is_scoped: bool = False,
parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.is_scoped = is_scoped
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Constant, )
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Enumeration
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
@@ -0,0 +1,140 @@
from typing import NamedTuple, Sequence, Optional, Tuple, List
from .node import ASTNode, ASTNodeType
from .type_node import TypeNode, NoneTypeNode, TypeResolutionError
class FunctionNode(ASTNode):
"""Represents a function (or class method) in both C++ and Python.
This class defines an overload set rather then function itself, because
function without overloads is represented as FunctionNode with 1 overload.
"""
class Arg:
def __init__(self, name: str, type_node: Optional[TypeNode] = None,
default_value: Optional[str] = None) -> None:
self.name = name
self.type_node = type_node
self.default_value = default_value
@property
def typename(self) -> Optional[str]:
return getattr(self.type_node, "full_typename", None)
def relative_typename(self, root: str) -> Optional[str]:
if self.type_node is not None:
return self.type_node.relative_typename(root)
return None
def __str__(self) -> str:
return (
f"Arg(name={self.name}, type_node={self.type_node},"
f" default_value={self.default_value})"
)
def __repr__(self) -> str:
return str(self)
class RetType:
def __init__(self, type_node: TypeNode = NoneTypeNode("void")) -> None:
self.type_node = type_node
@property
def typename(self) -> str:
return self.type_node.full_typename
def relative_typename(self, root: str) -> Optional[str]:
return self.type_node.relative_typename(root)
def __str__(self) -> str:
return f"RetType(type_node={self.type_node})"
def __repr__(self) -> str:
return str(self)
class Overload(NamedTuple):
arguments: Sequence["FunctionNode.Arg"] = ()
return_type: Optional["FunctionNode.RetType"] = None
def __init__(self, name: str,
arguments: Optional[Sequence["FunctionNode.Arg"]] = None,
return_type: Optional["FunctionNode.RetType"] = None,
is_static: bool = False,
is_classmethod: bool = False,
parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
"""Function node initializer
Args:
name (str): Name of the function overload set
arguments (Optional[Sequence[FunctionNode.Arg]], optional): Function
arguments. If this argument is None, then no overloads are
added and node should be treated like a "function stub" rather
than function. This might be helpful if there is a knowledge
that function with the defined name exists, but information
about its interface is not available at that moment.
Defaults to None.
return_type (Optional[FunctionNode.RetType], optional): Function
return type. Defaults to None.
is_static (bool, optional): Flag pointing that function is
a static method of some class. Defaults to False.
is_classmethod (bool, optional): Flag pointing that function is
a class method of some class. Defaults to False.
parent (Optional[ASTNode], optional): Parent ASTNode of the function.
Can be class or namespace. Defaults to None.
export_name (Optional[str], optional): Export name of the function.
Defaults to None.
"""
super().__init__(name, parent, export_name)
self.overloads: List[FunctionNode.Overload] = []
self.is_static = is_static
self.is_classmethod = is_classmethod
if arguments is not None:
self.add_overload(arguments, return_type)
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Function
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return ()
def add_overload(self, arguments: Sequence["FunctionNode.Arg"] = (),
return_type: Optional["FunctionNode.RetType"] = None):
self.overloads.append(FunctionNode.Overload(arguments, return_type))
def resolve_type_nodes(self, root: ASTNode):
"""Resolves type nodes in all overloads against `root`
Type resolution errors are postponed until all type nodes are examined.
Args:
root (ASTNode): Root of AST sub-tree used for type nodes resolution.
"""
def has_unresolved_type_node(item) -> bool:
return item.type_node is not None and not item.type_node.is_resolved
errors = []
for overload in self.overloads:
for arg in filter(has_unresolved_type_node, overload.arguments):
try:
arg.type_node.resolve(root) # type: ignore
except TypeResolutionError as e:
errors.append(
'Failed to resolve "{}" argument: {}'.format(arg.name, e)
)
if overload.return_type is not None and \
has_unresolved_type_node(overload.return_type):
try:
overload.return_type.type_node.resolve(root)
except TypeResolutionError as e:
errors.append('Failed to resolve return type: {}'.format(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" function against "{}". Errors: {}'.format(
self.full_export_name, root.full_export_name,
", ".join("[{}]: {}".format(i, e) for i, e in enumerate(errors))
)
)
@@ -0,0 +1,113 @@
import itertools
import weakref
from collections import defaultdict
from typing import Dict, List, Optional, Sequence, Tuple
from .class_node import ClassNode, ClassProperty
from .constant_node import ConstantNode
from .enumeration_node import EnumerationNode
from .function_node import FunctionNode
from .node import ASTNode, ASTNodeType
from .type_node import TypeResolutionError
class NamespaceNode(ASTNode):
"""Represents C++ namespace that treated as module in Python.
NamespaceNode can have other namespaces, classes, functions, enumerations
and global constants as its children nodes.
"""
def __init__(self, name: str, parent: Optional[ASTNode] = None,
export_name: Optional[str] = None) -> None:
super().__init__(name, parent, export_name)
self.reexported_submodules: List[str] = []
"""List of reexported submodules"""
self.reexported_submodules_symbols: Dict[str, List[str]] = defaultdict(list)
"""Mapping between submodules export names and their symbols re-exported
in this module"""
@property
def node_type(self) -> ASTNodeType:
return ASTNodeType.Namespace
@property
def children_types(self) -> Tuple[ASTNodeType, ...]:
return (ASTNodeType.Namespace, ASTNodeType.Class, ASTNodeType.Function,
ASTNodeType.Enumeration, ASTNodeType.Constant)
@property
def namespaces(self) -> Dict[str, "NamespaceNode"]:
return self._children[ASTNodeType.Namespace]
@property
def classes(self) -> Dict[str, ClassNode]:
return self._children[ASTNodeType.Class]
@property
def functions(self) -> Dict[str, FunctionNode]:
return self._children[ASTNodeType.Function]
@property
def enumerations(self) -> Dict[str, EnumerationNode]:
return self._children[ASTNodeType.Enumeration]
@property
def constants(self) -> Dict[str, ConstantNode]:
return self._children[ASTNodeType.Constant]
def add_namespace(self, name: str) -> "NamespaceNode":
return self._add_child(NamespaceNode, name)
def add_class(self, name: str,
bases: Sequence["weakref.ProxyType[ClassNode]"] = (),
properties: Sequence[ClassProperty] = ()) -> "ClassNode":
return self._add_child(ClassNode, name, bases=bases,
properties=properties)
def add_function(self, name: str, arguments: Sequence[FunctionNode.Arg] = (),
return_type: Optional[FunctionNode.RetType] = None) -> FunctionNode:
return self._add_child(FunctionNode, name, arguments=arguments,
return_type=return_type)
def add_enumeration(self, name: str) -> EnumerationNode:
return self._add_child(EnumerationNode, name)
def add_constant(self, name: str, value: str) -> ConstantNode:
return self._add_child(ConstantNode, name, value=value)
def resolve_type_nodes(self, root: Optional[ASTNode] = None) -> None:
"""Resolves type nodes for all children nodes in 2 steps:
1. Resolve against `self` as a tree root
2. Resolve against `root` as a tree root
Type resolution errors are postponed until all children nodes are
examined.
Args:
root (Optional[ASTNode], optional): Root of the AST sub-tree.
Defaults to None.
"""
errors = []
for child in itertools.chain(self.functions.values(),
self.classes.values(),
self.namespaces.values()):
try:
try:
child.resolve_type_nodes(self) # type: ignore
except TypeResolutionError:
if root is not None:
child.resolve_type_nodes(root) # type: ignore
else:
raise
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve "{}" namespace against "{}". '
'Errors: {}'.format(
self.full_export_name,
root if root is None else root.full_export_name,
errors
)
)
@@ -0,0 +1,233 @@
import abc
import enum
import itertools
from typing import (Iterator, Type, TypeVar, Dict,
Optional, Tuple, DefaultDict)
from collections import defaultdict
import weakref
ASTNodeSubtype = TypeVar("ASTNodeSubtype", bound="ASTNode")
NodeType = Type["ASTNode"]
NameToNode = Dict[str, ASTNodeSubtype]
class ASTNodeType(enum.Enum):
Namespace = enum.auto()
Class = enum.auto()
Function = enum.auto()
Enumeration = enum.auto()
Constant = enum.auto()
class ASTNode:
"""Represents an element of the Abstract Syntax Tree produced by parsing
public C++ headers.
NOTE: Every node manages a lifetime of its children nodes. Children nodes
contain only weak references to their direct parents, so there are no
circular dependencies.
"""
def __init__(self, name: str, parent: Optional["ASTNode"] = None,
export_name: Optional[str] = None) -> None:
"""ASTNode initializer
Args:
name (str): name of the node, should be unique inside enclosing
context (There can't be 2 classes with the same name defined
in the same namespace).
parent (ASTNode, optional): parent node expressing node context.
None corresponds to globally defined object e.g. root namespace
or function without namespace. Defaults to None.
export_name (str, optional): export name of the node used to resolve
issues in languages without proper overload resolution and
provide more meaningful naming. Defaults to None.
"""
FORBIDDEN_SYMBOLS = ";,*&#/|\\@!()[]^% "
for forbidden_symbol in FORBIDDEN_SYMBOLS:
assert forbidden_symbol not in name, \
"Invalid node identifier '{}' - contains 1 or more "\
"forbidden symbols: ({})".format(name, FORBIDDEN_SYMBOLS)
assert ":" not in name, \
"Name '{}' contains C++ scope symbols (':'). Convert the name to "\
"Python style and create appropriate parent nodes".format(name)
assert "." not in name, \
"Trying to create a node with '.' symbols in its name ({}). " \
"Dots are supposed to be a scope delimiters, so create all nodes in ('{}') " \
"and add '{}' as a last child node".format(
name,
"->".join(name.split('.')[:-1]),
name.rsplit('.', maxsplit=1)[-1]
)
self.__name = name
self.export_name = name if export_name is None else export_name
self._parent: Optional["ASTNode"] = None
self.parent = parent
self.is_exported = True
self._children: DefaultDict[ASTNodeType, NameToNode] = defaultdict(dict)
def __str__(self) -> str:
return "{}('{}' exported as '{}')".format(
self.node_type.name, self.name, self.export_name
)
def __repr__(self) -> str:
return str(self)
@abc.abstractproperty
def children_types(self) -> Tuple[ASTNodeType, ...]:
"""Set of ASTNode types that are allowed to be children of this node
Returns:
Tuple[ASTNodeType, ...]: Types of children nodes
"""
pass
@abc.abstractproperty
def node_type(self) -> ASTNodeType:
"""Type of the ASTNode that can be used to distinguish nodes without
importing all subclasses of ASTNode
Returns:
ASTNodeType: Current node type
"""
pass
def node_type_name(self) -> str:
return f"{self.node_type.name}::{self.name}"
@property
def name(self) -> str:
return self.__name
@property
def native_name(self) -> str:
return self.full_name.replace(".", "::")
@property
def full_name(self) -> str:
return self._construct_full_name("name")
@property
def full_export_name(self) -> str:
return self._construct_full_name("export_name")
@property
def parent(self) -> Optional["ASTNode"]:
return self._parent
@parent.setter
def parent(self, value: Optional["ASTNode"]) -> None:
assert value is None or isinstance(value, ASTNode), \
"ASTNode.parent should be None or another ASTNode, " \
"but got: {}".format(type(value))
if value is not None:
value.__check_child_before_add(self, self.name)
# Detach from previous parent
if self._parent is not None:
self._parent._children[self.node_type].pop(self.name)
if value is None:
self._parent = None
return
# Set a weak reference to a new parent and add self to its children
self._parent = weakref.proxy(value)
value._children[self.node_type][self.name] = self
def __check_child_before_add(self, child: ASTNodeSubtype,
name: str) -> None:
assert len(self.children_types) > 0, (
f"Trying to add child node '{child.node_type_name}' to node "
f"'{self.node_type_name}' that can't have children nodes"
)
assert child.node_type in self.children_types, \
"Trying to add child node '{}' to node '{}' " \
"that supports only ({}) as its children types".format(
child.node_type_name, self.node_type_name,
",".join(t.name for t in self.children_types)
)
if self._find_child(child.node_type, name) is not None:
raise ValueError(
f"Node '{self.node_type_name}' already has a "
f"child '{child.node_type_name}'"
)
def _add_child(self, child_type: Type[ASTNodeSubtype], name: str,
**kwargs) -> ASTNodeSubtype:
"""Creates a child of the node with the given type and performs common
validation checks:
- Node can have children of the provided type
- Node doesn't have child with the same name
NOTE: Shouldn't be used directly by a user.
Args:
child_type (Type[ASTNodeSubtype]): Type of the child to create.
name (str): Name of the child.
**kwargs: Extra keyword arguments supplied to child_type.__init__
method.
Returns:
ASTNodeSubtype: Created ASTNode
"""
return child_type(name, parent=self, **kwargs)
def _find_child(self, child_type: ASTNodeType,
name: str) -> Optional[ASTNodeSubtype]:
"""Looks for child node with the given type and name.
Args:
child_type (ASTNodeType): Type of the child node.
name (str): Name of the child node.
Returns:
Optional[ASTNodeSubtype]: child node if it can be found, None
otherwise.
"""
if child_type not in self._children:
return None
return self._children[child_type].get(name, None)
def _construct_full_name(self, property_name: str) -> str:
"""Traverses nodes hierarchy upright to the root node and constructs a
full name of the node using original or export names depending on the
provided `property_name` argument.
Args:
property_name (str): Name of the property to quire from node to get
its name. Should be `name` or `export_name`.
Returns:
str: full node name where each node part is divided with a dot.
"""
def get_name(node: ASTNode) -> str:
return getattr(node, property_name)
assert property_name in ('name', 'export_name'), 'Invalid name property'
name_parts = [get_name(self), ]
parent = self.parent
while parent is not None:
name_parts.append(get_name(parent))
parent = parent.parent
return ".".join(reversed(name_parts))
def __iter__(self) -> Iterator["ASTNode"]:
return iter(itertools.chain.from_iterable(
node
# Iterate over mapping between node type and nodes dict
for children_nodes in self._children.values()
# Iterate over mapping between node name and node
for node in children_nodes.values()
))
@@ -0,0 +1,991 @@
from typing import Sequence, Generator, Tuple, Optional, Union
import weakref
import abc
from itertools import chain
from .node import ASTNode, ASTNodeType
class TypeResolutionError(Exception):
pass
class TypeNode(abc.ABC):
"""This class and its derivatives used for construction parts of AST that
otherwise can't be constructed from the information provided by header
parser, because this information is either not available at that moment of
time or not available at all:
- There is no possible way to derive correspondence between C++ type
and its Python equivalent if it is not exposed from library
e.g. `cv::Rect`.
- There is no information about types visibility (see `ASTNodeTypeNode`).
"""
compatible_to_runtime_usage = False
"""Class-wide property that switches exported type names for several nodes.
Example:
>>> node = OptionalTypeNode(ASTNodeTypeNode("Size"))
>>> node.typename # TypeNode.compatible_to_runtime_usage == False
"Size | None"
>>> TypeNode.compatible_to_runtime_usage = True
>>> node.typename
"typing.Optional[Size]"
"""
def __init__(self, ctype_name: str, required_modules: Tuple[str, ...] = ()) -> None:
self.ctype_name = ctype_name
self._required_modules = required_modules
@abc.abstractproperty
def typename(self) -> str:
"""Short name of the type node used that should be used in the same
module (or a file) where type is defined.
Returns:
str: short name of the type node.
"""
return ""
@property
def full_typename(self) -> str:
"""Full name of the type node including full module name starting from
the package.
Example: 'cv2.Algorithm', 'cv2.gapi.ie.PyParams'.
Returns:
str: full name of the type node.
"""
return self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
"""Generator filled with import statements required for type
node definition (especially used by `AliasTypeNode`).
Example:
```python
# Alias defined in the `cv2.typing.__init__.pyi`
Callback = typing.Callable[[cv2.GMat, float], None]
# alias definition
callback_alias = AliasTypeNode.callable_(
'Callback',
arg_types=(ASTNodeTypeNode('GMat'), PrimitiveTypeNode.float_())
)
# Required definition imports
for required_import in callback_alias.required_definition_imports:
print(required_import)
# Outputs:
# 'import typing'
# 'import cv2'
```
Yields:
Generator[str, None, None]: generator filled with import statements
required for type node definition.
"""
yield from ()
@property
def required_usage_imports(self) -> Generator[str, None, None]:
"""Generator filled with import statements required for type node
usage.
Example:
```python
# Alias defined in the `cv2.typing.__init__.pyi`
Callback = typing.Callable[[cv2.GMat, float], None]
# alias definition
callback_alias = AliasTypeNode.callable_(
'Callback',
arg_types=(ASTNodeTypeNode('GMat'), PrimitiveTypeNode.float_())
)
# Required usage imports
for required_import in callback_alias.required_usage_imports:
print(required_import)
# Outputs:
# 'import cv2.typing'
```
Yields:
Generator[str, None, None]: generator filled with import statements
required for type node definition.
"""
yield from ()
@property
def required_modules(self) -> Tuple[str, ...]:
return self._required_modules
@property
def is_resolved(self) -> bool:
return True
def relative_typename(self, module: str) -> str:
"""Type name relative to the provided module.
Args:
module (str): Full export name of the module to get relative name to.
Returns:
str: If module name of the type node doesn't match `module`, then
returns class scopes + `self.typename`, otherwise
`self.full_typename`.
"""
return self.full_typename
def resolve(self, root: ASTNode) -> None:
"""Resolves all references to AST nodes using a top-down search
for nodes with corresponding export names. See `_resolve_symbol` for
more details.
Args:
root (ASTNode): Node pointing to the root of a subtree in AST
representing search scope of the symbol.
Most of the symbols don't have full paths in their names, so
scopes should be examined in bottom-up manner starting
with narrowest one.
Raises:
TypeResolutionError: if at least 1 reference to AST node can't
be resolved in the subtree pointed by the root.
"""
pass
class NoneTypeNode(TypeNode):
"""Type node representing a None (or `void` in C++) type.
"""
@property
def typename(self) -> str:
return "None"
class AnyTypeNode(TypeNode):
"""Type node representing any type (most of the time it means unknown).
"""
@property
def typename(self) -> str:
return "_typing.Any"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
class PrimitiveTypeNode(TypeNode):
"""Type node representing a primitive built-in types e.g. int, float, str.
"""
def __init__(self, ctype_name: str,
typename: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self._typename = typename if typename is not None else ctype_name
@property
def typename(self) -> str:
return self._typename
@classmethod
def int_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "int"
return PrimitiveTypeNode(ctype_name, typename="int", required_modules=required_modules)
@classmethod
def float_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "float"
return PrimitiveTypeNode(ctype_name, typename="float", required_modules=required_modules)
@classmethod
def bool_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "bool"
return PrimitiveTypeNode(ctype_name, typename="bool", required_modules=required_modules)
@classmethod
def str_(cls, ctype_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
if ctype_name is None:
ctype_name = "string"
return PrimitiveTypeNode(ctype_name, "str", required_modules=required_modules)
class AliasRefTypeNode(TypeNode):
"""Type node representing an alias referencing another alias. Example:
```python
Point2i = tuple[int, int]
Point = Point2i
```
During typing stubs generation procedure above code section might be defined
as follows
```python
AliasTypeNode.tuple_("Point2i",
items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_()
))
AliasTypeNode.ref_("Point", "Point2i")
```
"""
def __init__(self, alias_ctype_name: str,
alias_export_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
super().__init__(alias_ctype_name, required_modules)
if alias_export_name is None:
self.alias_export_name = alias_ctype_name
else:
self.alias_export_name = alias_export_name
@property
def typename(self) -> str:
return self.alias_export_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
class AliasTypeNode(TypeNode):
"""Type node representing an alias to another type.
Example:
```python
Point2i = tuple[int, int]
```
can be defined as
```python
AliasTypeNode.tuple_("Point2i",
items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.int_()
))
```
Under the hood it is implemented as a container of another type node.
"""
def __init__(self, ctype_name: str, value: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self.value = value
# If alias is exported as is - use its ctype_name
if export_name is None:
forbidden_symbols = (":", "*", "&")
assert all(symbol not in ctype_name for symbol in forbidden_symbols), (
"Failed to create AliasTypeNode without export_name. "
f"'{ctype_name}' should not contain any of {forbidden_symbols}"
)
self._export_name = ctype_name
else:
self._export_name = export_name
self.doc = doc
@property
def typename(self) -> str:
return self._export_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
return self.value.required_usage_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import cv2.typing"
@property
def is_resolved(self) -> bool:
return self.value.is_resolved
def resolve(self, root: ASTNode):
try:
self.value.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve alias "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
)
) from e
@classmethod
def int_(cls, ctype_name: str, export_name: Optional[str] = None,
doc: Optional[str] = None, required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, PrimitiveTypeNode.int_(), export_name, doc, required_modules)
@classmethod
def float_(cls, ctype_name: str, export_name: Optional[str] = None,
doc: Optional[str] = None, required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, PrimitiveTypeNode.float_(), export_name, doc, required_modules)
@classmethod
def array_ref_(cls, ctype_name: str, array_ref_name: str,
shape: Optional[Tuple[int, ...]],
dtype: Optional[str] = None,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
"""Create alias to array reference alias `array_ref_name`.
This is required to preserve backward compatibility with Python < 3.9
and NumPy 1.20, when NumPy module introduces generics support.
Args:
ctype_name (str): Name of the alias.
array_ref_name (str): Name of the conditional array alias.
shape (Optional[Tuple[int, ...]]): Array shape.
dtype (Optional[str], optional): Array type. Defaults to None.
export_name (Optional[str], optional): Alias export name.
Defaults to None.
doc (Optional[str], optional): Documentation string for alias.
Defaults to None.
"""
if doc is None:
doc = f"NDArray(shape={shape}, dtype={dtype})"
else:
doc += f". NDArray(shape={shape}, dtype={dtype})"
return cls(ctype_name, AliasRefTypeNode(array_ref_name),
export_name, doc, required_modules)
@classmethod
def union_(cls, ctype_name: str, items: Tuple[TypeNode, ...],
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, UnionTypeNode(ctype_name, items),
export_name, doc, required_modules)
@classmethod
def optional_(cls, ctype_name: str, item: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, OptionalTypeNode(item), export_name, doc, required_modules)
@classmethod
def sequence_(cls, ctype_name: str, item: TypeNode,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, SequenceTypeNode(ctype_name, item),
export_name, doc, required_modules)
@classmethod
def tuple_(cls, ctype_name: str, items: Tuple[TypeNode, ...],
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, TupleTypeNode(ctype_name, items),
export_name, doc, required_modules)
@classmethod
def class_(cls, ctype_name: str, class_name: str,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, ASTNodeTypeNode(class_name),
export_name, doc, required_modules)
@classmethod
def callable_(cls, ctype_name: str,
arg_types: Union[TypeNode, Sequence[TypeNode]],
ret_type: TypeNode = NoneTypeNode("void"),
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name,
CallableTypeNode(ctype_name, arg_types, ret_type),
export_name, doc, required_modules)
@classmethod
def ref_(cls, ctype_name: str, alias_ctype_name: str,
alias_export_name: Optional[str] = None,
export_name: Optional[str] = None,
doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name,
AliasRefTypeNode(alias_ctype_name, alias_export_name),
export_name, doc, required_modules)
@classmethod
def dict_(cls, ctype_name: str, key_type: TypeNode, value_type: TypeNode,
export_name: Optional[str] = None, doc: Optional[str] = None,
required_modules: Tuple[str, ...] = ()):
return cls(ctype_name, DictTypeNode(ctype_name, key_type, value_type),
export_name, doc, required_modules)
class ConditionalAliasTypeNode(TypeNode):
"""Type node representing an alias protected by condition checked in runtime.
For typing-related conditions, prefer using typing.TYPE_CHECKING. For a full explanation, see:
https://github.com/opencv/opencv/pull/23927#discussion_r1256326835
Example:
```python
if typing.TYPE_CHECKING
NumPyArray = numpy.ndarray[typing.Any, numpy.dtype[numpy.generic]]
else:
NumPyArray = numpy.ndarray
```
is defined as follows:
```python
ConditionalAliasTypeNode(
"NumPyArray",
'typing.TYPE_CHECKING',
NDArrayTypeNode("NumPyArray"),
NDArrayTypeNode("NumPyArray", use_numpy_generics=False),
condition_required_imports=("import typing",)
)
```
"""
def __init__(self, ctype_name: str, condition: str,
positive_branch_type: TypeNode,
negative_branch_type: TypeNode,
export_name: Optional[str] = None,
condition_required_imports: Sequence[str] = ()) -> None:
super().__init__(ctype_name)
self.condition = condition
self.positive_branch_type = positive_branch_type
self.positive_branch_type.ctype_name = self.ctype_name
self.negative_branch_type = negative_branch_type
self.negative_branch_type.ctype_name = self.ctype_name
self._export_name = export_name
self._condition_required_imports = condition_required_imports
@property
def typename(self) -> str:
if self._export_name is not None:
return self._export_name
return self.ctype_name
@property
def full_typename(self) -> str:
return "cv2.typing." + self.typename
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield from self.positive_branch_type.required_usage_imports
yield from self.negative_branch_type.required_usage_imports
yield from self._condition_required_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import cv2.typing"
@property
def required_modules(self) -> Tuple[str, ...]:
return (*self.positive_branch_type.required_modules,
*self.negative_branch_type.required_modules)
@property
def is_resolved(self) -> bool:
return self.positive_branch_type.is_resolved \
and self.negative_branch_type.is_resolved
def resolve(self, root: ASTNode):
try:
self.positive_branch_type.resolve(root)
self.negative_branch_type.resolve(root)
except TypeResolutionError as e:
raise TypeResolutionError(
'Failed to resolve alias "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
)
) from e
@classmethod
def numpy_array_(cls, ctype_name: str, export_name: Optional[str] = None,
shape: Optional[Tuple[int, ...]] = None,
dtype: Optional[str] = None):
"""Type subscription is not possible in python 3.8 and older numpy versions."""
return cls(
ctype_name,
"_typing.TYPE_CHECKING",
NDArrayTypeNode(ctype_name, shape, dtype),
NDArrayTypeNode(ctype_name, shape, dtype,
use_numpy_generics=False),
condition_required_imports=("import typing as _typing",)
)
class NDArrayTypeNode(TypeNode):
"""Type node representing NumPy ndarray.
"""
def __init__(self, ctype_name: str,
shape: Optional[Tuple[int, ...]] = None,
dtype: Optional[str] = None,
use_numpy_generics: bool = True) -> None:
super().__init__(ctype_name)
self.shape = shape
self.dtype = dtype
self._use_numpy_generics = use_numpy_generics
@property
def typename(self) -> str:
if self._use_numpy_generics:
# NOTE: Shape is not fully supported yet
dtype = self.dtype if self.dtype is not None else "numpy.generic"
return f"numpy.ndarray[_typing.Any, numpy.dtype[{dtype}]]"
return "numpy.ndarray"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import numpy"
# if self.shape is None:
yield "import typing as _typing"
class ASTNodeTypeNode(TypeNode):
"""Type node representing a lazy ASTNode corresponding to type of
function argument or its return type or type of class property.
Introduced laziness nature resolves the types visibility issue - all types
should be known during function declaration to select an appropriate node
from the AST. Such knowledge leads to evaluation of all preprocessor
directives (`#include` particularly) for each processed header and might be
too expensive and error prone.
"""
def __init__(self, ctype_name: str, typename: Optional[str] = None,
module_name: Optional[str] = None,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self._typename = typename if typename is not None else ctype_name
self._module_name = module_name
self._ast_node: Optional[weakref.ProxyType[ASTNode]] = None
@property
def ast_node(self):
return self._ast_node
@property
def typename(self) -> str:
if self._ast_node is None:
return self._typename
typename = self._ast_node.export_name
if self._ast_node.node_type is not ASTNodeType.Enumeration:
return typename
# NOTE: Special handling for enums
parent = self._ast_node.parent
while parent.node_type is ASTNodeType.Class:
typename = parent.export_name + "_" + typename
parent = parent.parent
return typename
@property
def full_typename(self) -> str:
if self._ast_node is not None:
if self._ast_node.node_type is not ASTNodeType.Enumeration:
return self._ast_node.full_export_name
# NOTE: enumerations are exported to module scope
typename = self._ast_node.export_name
parent = self._ast_node.parent
while parent.node_type is ASTNodeType.Class:
typename = parent.export_name + "_" + typename
parent = parent.parent
return parent.full_export_name + "." + typename
if self._module_name is not None:
return self._module_name + "." + self._typename
return self._typename
@property
def required_usage_imports(self) -> Generator[str, None, None]:
if self._module_name is None:
assert self._ast_node is not None, \
"Can't find a module for class '{}' exported as '{}'".format(
self.ctype_name, self.typename,
)
module = self._ast_node.parent
while module.node_type is not ASTNodeType.Namespace:
module = module.parent
yield "import " + module.full_export_name
else:
yield "import " + self._module_name
@property
def is_resolved(self) -> bool:
return self._ast_node is not None or self._module_name is not None
def resolve(self, root: ASTNode):
if self.is_resolved:
return
node = _resolve_symbol(root, self.typename)
if node is None:
raise TypeResolutionError('Failed to resolve "{}" exposed as "{}"'.format(
self.ctype_name, self.typename
))
self._ast_node = weakref.proxy(node)
def relative_typename(self, module: str) -> str:
assert self._ast_node is not None or self._module_name is not None, \
"'{}' exported as '{}' is not resolved yet".format(self.ctype_name,
self.typename)
if self._module_name is None:
type_module = self._ast_node.parent # type: ignore
while type_module.node_type is not ASTNodeType.Namespace:
type_module = type_module.parent
module_name = type_module.full_export_name
else:
module_name = self._module_name
if module_name != module:
return self.full_typename
return self.full_typename[len(module_name) + 1:]
class AggregatedTypeNode(TypeNode):
"""Base type node for type nodes representing an aggregation of another
type nodes e.g. tuple, sequence or callable."""
def __init__(self, ctype_name: str, items: Sequence[TypeNode],
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, required_modules)
self.items = list(items)
@property
def is_resolved(self) -> bool:
return all(item.is_resolved for item in self.items)
@property
def required_modules(self) -> Tuple[str, ...]:
return (*chain.from_iterable(item.required_modules for item in self.items),
*self._required_modules)
def resolve(self, root: ASTNode) -> None:
errors = []
for item in filter(lambda item: not item.is_resolved, self):
try:
item.resolve(root)
except TypeResolutionError as e:
errors.append(str(e))
if len(errors) > 0:
raise TypeResolutionError(
'Failed to resolve one of "{}" items. Errors: {}'.format(
self.full_typename, errors
)
)
def __iter__(self):
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
@property
def required_definition_imports(self) -> Generator[str, None, None]:
for item in self:
yield from item.required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
for item in self:
yield from item.required_usage_imports
class ContainerTypeNode(AggregatedTypeNode):
"""Base type node for all type nodes representing a container type.
"""
@property
def typename(self) -> str:
return self.type_format.format(self.types_separator.join(
item.typename for item in self
))
@property
def full_typename(self) -> str:
return self.type_format.format(self.types_separator.join(
item.full_typename for item in self
))
def relative_typename(self, module: str) -> str:
return self.type_format.format(self.types_separator.join(
item.relative_typename(module) for item in self
))
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
yield from super().required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
if TypeNode.compatible_to_runtime_usage:
yield "import typing as _typing"
yield from super().required_usage_imports
@abc.abstractproperty
def type_format(self) -> str:
return ""
@abc.abstractproperty
def types_separator(self) -> str:
return ""
class SequenceTypeNode(ContainerTypeNode):
"""Type node representing a homogeneous collection of elements with
possible unknown length.
"""
def __init__(self, ctype_name: str, item: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, (item, ), required_modules)
@property
def type_format(self) -> str:
return "_typing.Sequence[{}]"
@property
def types_separator(self) -> str:
return ", "
class TupleTypeNode(ContainerTypeNode):
"""Type node representing possibly heterogeneous collection of types with
possibly unspecified length.
"""
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Tuple[{}]"
return "tuple[{}]"
@property
def types_separator(self) -> str:
return ", "
class UnionTypeNode(ContainerTypeNode):
"""Type node representing type that can be one of the predefined set of types.
"""
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Union[{}]"
return "{}"
@property
def types_separator(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return ", "
return " | "
class OptionalTypeNode(ContainerTypeNode):
"""Type node representing optional type which is effectively is a union
of value type node and None.
"""
def __init__(self, value: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(value.ctype_name, (value,), required_modules)
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Optional[{}]"
return "{} | None"
@property
def types_separator(self) -> str:
return ", "
class DictTypeNode(ContainerTypeNode):
"""Type node representing a homogeneous key-value mapping.
"""
def __init__(self, ctype_name: str, key_type: TypeNode,
value_type: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(ctype_name, (key_type, value_type), required_modules)
@property
def key_type(self) -> TypeNode:
return self.items[0]
@property
def value_type(self) -> TypeNode:
return self.items[1]
@property
def type_format(self) -> str:
if TypeNode.compatible_to_runtime_usage:
return "_typing.Dict[{}]"
return "dict[{}]"
@property
def types_separator(self) -> str:
return ", "
class CallableTypeNode(AggregatedTypeNode):
"""Type node representing a callable type (most probably a function).
```python
CallableTypeNode(
'image_reading_callback',
arg_types=(ASTNodeTypeNode('Image'), PrimitiveTypeNode.float_())
)
```
defines a callable type node representing a function with the same
interface as the following
```python
def image_reading_callback(image: Image, timestamp: float) -> None: ...
```
"""
def __init__(self, ctype_name: str,
arg_types: Union[TypeNode, Sequence[TypeNode]],
ret_type: TypeNode = NoneTypeNode("void"),
required_modules: Tuple[str, ...] = ()) -> None:
if isinstance(arg_types, TypeNode):
super().__init__(ctype_name, (arg_types, ret_type), required_modules)
else:
super().__init__(ctype_name, (*arg_types, ret_type), required_modules)
@property
def arg_types(self) -> Sequence[TypeNode]:
return self.items[:-1]
@property
def ret_type(self) -> TypeNode:
return self.items[-1]
@property
def typename(self) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.typename for arg in self.arg_types),
self.ret_type.typename
)
@property
def full_typename(self) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.full_typename for arg in self.arg_types),
self.ret_type.full_typename
)
def relative_typename(self, module: str) -> str:
return '_typing.Callable[[{}], {}]'.format(
', '.join(arg.relative_typename(module) for arg in self.arg_types),
self.ret_type.relative_typename(module)
)
@property
def required_definition_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
yield from super().required_definition_imports
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import typing as _typing"
yield from super().required_usage_imports
class ClassTypeNode(ContainerTypeNode):
"""Type node representing types themselves (refer to typing.Type)
"""
def __init__(self, value: TypeNode,
required_modules: Tuple[str, ...] = ()) -> None:
super().__init__(value.ctype_name, (value,), required_modules)
@property
def type_format(self) -> str:
return "_typing.Type[{}]"
@property
def types_separator(self) -> str:
return ", "
class PathLikeTypeNode(TypeNode):
"""Type node representing a PathLike object.
"""
def __init__(self, ctype_name: str) -> None:
super().__init__(ctype_name)
@property
def typename(self) -> str:
return "os.PathLike[str]"
@property
def required_usage_imports(self) -> Generator[str, None, None]:
yield "import os"
@staticmethod
def string_or_pathlike_(ctype_name: str = "string") -> UnionTypeNode:
return UnionTypeNode(
ctype_name,
items=(
PrimitiveTypeNode.str_(ctype_name),
PathLikeTypeNode(ctype_name)
)
)
def _resolve_symbol(root: Optional[ASTNode], full_symbol_name: str) -> Optional[ASTNode]:
"""Searches for a symbol with the given full export name in the AST
starting from the `root`.
Args:
root (Optional[ASTNode]): Root of the examining AST.
full_symbol_name (str): Full export name of the symbol to find. Path
components can be divided by '.' or '_'.
Returns:
Optional[ASTNode]: ASTNode with full export name equal to
`full_symbol_name`, None otherwise.
>>> root = NamespaceNode('cv')
>>> cls = root.add_class('Algorithm').add_class('Params')
>>> _resolve_symbol(root, 'cv.Algorithm.Params') == cls
True
>>> root = NamespaceNode('cv')
>>> enum = root.add_namespace('detail').add_enumeration('AlgorithmType')
>>> _resolve_symbol(root, 'cv_detail_AlgorithmType') == enum
True
>>> root = NamespaceNode('cv')
>>> _resolve_symbol(root, 'cv.detail.Algorithm')
None
>>> root = NamespaceNode('cv')
>>> enum = root.add_namespace('detail').add_enumeration('AlgorithmType')
>>> _resolve_symbol(root, 'AlgorithmType')
None
"""
def search_down_symbol(scope: Optional[ASTNode],
scope_sep: str) -> Optional[ASTNode]:
parts = full_symbol_name.split(scope_sep, maxsplit=1)
while len(parts) == 2:
# Try to find narrow scope
scope = _resolve_symbol(scope, parts[0])
if scope is None:
return None
# and resolve symbol in it
node = _resolve_symbol(scope, parts[1])
if node is not None:
return node
# symbol is not found, but narrowed scope is valid - diving further
parts = parts[1].split(scope_sep, maxsplit=1)
return None
assert root is not None, \
"Can't resolve symbol '{}' from NONE root".format(full_symbol_name)
# Looking for exact symbol match
for attr in filter(lambda attr: hasattr(root, attr),
("namespaces", "classes", "enumerations")):
nodes_dict = getattr(root, attr)
node = nodes_dict.get(full_symbol_name, None)
if node is not None:
return node
# Symbol is not found, looking for more fine-grained scope if possible
for scope_sep in ("_", "."):
node = search_down_symbol(root, scope_sep)
if node is not None:
return node
return None
@@ -0,0 +1,273 @@
from .nodes.type_node import (
AliasTypeNode, AliasRefTypeNode, PrimitiveTypeNode,
ASTNodeTypeNode, NDArrayTypeNode, NoneTypeNode, SequenceTypeNode,
TupleTypeNode, UnionTypeNode, AnyTypeNode, ConditionalAliasTypeNode
)
# Set of predefined types used to cover cases when library doesn't
# directly exports a type and equivalent one should be used instead.
# Example: Instead of C++ `cv::Rect(1, 1, 5, 6)` in Python any sequence type
# with length 4 can be used: tuple `(1, 1, 5, 6)` or list `[1, 1, 5, 6]`.
# Predefined type might be:
# - alias - defines a Python synonym for a native type name.
# Example: `cv::Rect` and `cv::Size` are both `Sequence[int]` in Python, but
# with different length constraints (4 and 2 accordingly).
# - direct substitution - just a plain type replacement without any credits to
# native type. Example:
# * `std::vector<uchar>` is `np.ndarray` with `dtype == np.uint8` in Python
# * `double` is a Python `float`
# * `std::string` is a Python `str`
_PREDEFINED_TYPES = (
PrimitiveTypeNode.int_("int"),
PrimitiveTypeNode.int_("uchar"),
PrimitiveTypeNode.int_("unsigned"),
PrimitiveTypeNode.int_("int64"),
PrimitiveTypeNode.int_("uint8_t"),
PrimitiveTypeNode.int_("int8_t"),
PrimitiveTypeNode.int_("int32_t"),
PrimitiveTypeNode.int_("uint32_t"),
PrimitiveTypeNode.int_("size_t"),
PrimitiveTypeNode.int_("int64_t"),
PrimitiveTypeNode.int_("long long"),
PrimitiveTypeNode.float_("float"),
PrimitiveTypeNode.float_("double"),
PrimitiveTypeNode.bool_("bool"),
PrimitiveTypeNode.str_("string"),
PrimitiveTypeNode.str_("char"),
PrimitiveTypeNode.str_("String"),
PrimitiveTypeNode.str_("c_string"),
ConditionalAliasTypeNode.numpy_array_(
"NumPyArrayNumeric",
dtype="numpy.integer[_typing.Any] | numpy.floating[_typing.Any]"
),
ConditionalAliasTypeNode.numpy_array_("NumPyArrayFloat32", dtype="numpy.float32"),
ConditionalAliasTypeNode.numpy_array_("NumPyArrayFloat64", dtype="numpy.float64"),
NoneTypeNode("void"),
AliasTypeNode.int_("void*", "IntPointer", "Represents an arbitrary pointer"),
AliasTypeNode.union_(
"Mat",
items=(ASTNodeTypeNode("Mat", module_name="cv2.mat_wrapper"),
AliasRefTypeNode("NumPyArrayNumeric")),
export_name="MatLike"
),
AliasTypeNode.sequence_("MatShape", PrimitiveTypeNode.int_()),
AliasTypeNode.sequence_("Size", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Size2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.union_(
"Scalar",
items=(SequenceTypeNode("Scalar", PrimitiveTypeNode.float_()),
PrimitiveTypeNode.float_()),
doc="Max sequence length is at most 4"
),
AliasTypeNode.sequence_("Point", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.ref_("Point2i", "Point"),
AliasTypeNode.sequence_("Point2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Point2d", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Point3i", PrimitiveTypeNode.int_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Point3f", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Point3d", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Range", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Rect", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2i", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2f", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Rect2d", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.dict_("Moments", PrimitiveTypeNode.str_("Moments::key"),
PrimitiveTypeNode.float_("Moments::value")),
AliasTypeNode.tuple_("RotatedRect",
items=(AliasRefTypeNode("Point2f"),
AliasRefTypeNode("Size2f"),
PrimitiveTypeNode.float_()),
doc="Any type providing sequence protocol is supported"),
AliasTypeNode.tuple_("TermCriteria",
items=(
ASTNodeTypeNode("TermCriteria.Type"),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_()),
doc="Any type providing sequence protocol is supported"),
AliasTypeNode.sequence_("Vec2i", PrimitiveTypeNode.int_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec2f", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec2d", PrimitiveTypeNode.float_(),
doc="Required length is 2"),
AliasTypeNode.sequence_("Vec3i", PrimitiveTypeNode.int_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec3f", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec3d", PrimitiveTypeNode.float_(),
doc="Required length is 3"),
AliasTypeNode.sequence_("Vec4i", PrimitiveTypeNode.int_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec4f", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec4d", PrimitiveTypeNode.float_(),
doc="Required length is 4"),
AliasTypeNode.sequence_("Vec6f", PrimitiveTypeNode.float_(),
doc="Required length is 6"),
AliasTypeNode.class_("FeatureDetector", "Feature2D",
export_name="FeatureDetector"),
AliasTypeNode.class_("DescriptorExtractor", "Feature2D",
export_name="DescriptorExtractor"),
AliasTypeNode.class_("FeatureExtractor", "Feature2D",
export_name="FeatureExtractor"),
AliasTypeNode.array_ref_("Matx33f",
array_ref_name="NumPyArrayFloat32",
shape=(3, 3),
dtype="numpy.float32"),
AliasTypeNode.array_ref_("Matx33d",
array_ref_name="NumPyArrayFloat64",
shape=(3, 3),
dtype="numpy.float64"),
AliasTypeNode.array_ref_("Matx44f",
array_ref_name="NumPyArrayFloat32",
shape=(4, 4),
dtype="numpy.float32"),
AliasTypeNode.array_ref_("Matx44d",
array_ref_name="NumPyArrayFloat64",
shape=(4, 4),
dtype="numpy.float64"),
NDArrayTypeNode("vector<uchar>", dtype="numpy.uint8"),
NDArrayTypeNode("vector_uchar", dtype="numpy.uint8"),
# DNN, optional
AliasTypeNode.class_("LayerId", "DictValue", required_modules=("dnn",)),
AliasTypeNode.dict_("LayerParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("DictValue", items=(
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
required_modules=("dnn",)),
# Flann, optional
PrimitiveTypeNode.int_("cvflann_flann_distance_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("flann_flann_distance_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("cvflann_flann_algorithm_t", required_modules=("flann",)),
PrimitiveTypeNode.int_("flann_flann_algorithm_t", required_modules=("flann",)),
AliasTypeNode.dict_("flann_IndexParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("flann_IndexParams::value", items=(
PrimitiveTypeNode.bool_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
export_name="IndexParams",
required_modules=("flann",)),
AliasTypeNode.dict_("flann_SearchParams",
key_type=PrimitiveTypeNode.str_(),
value_type=UnionTypeNode("flann_IndexParams::value", items=(
PrimitiveTypeNode.bool_(),
PrimitiveTypeNode.int_(),
PrimitiveTypeNode.float_(),
PrimitiveTypeNode.str_())
),
export_name="SearchParams",
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_string",
PrimitiveTypeNode.str_("map_string_and_string::key"),
PrimitiveTypeNode.str_("map_string_and_string::value"),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_int",
PrimitiveTypeNode.str_("map_string_and_int::key"),
PrimitiveTypeNode.int_("map_string_and_int::value"),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_vector_size_t",
PrimitiveTypeNode.str_("map_string_and_vector_size_t::key"),
SequenceTypeNode("map_string_and_vector_size_t::value", PrimitiveTypeNode.int_("size_t")),
required_modules=("flann",)),
AliasTypeNode.dict_("map_string_and_vector_float",
PrimitiveTypeNode.str_("map_string_and_vector_float::key"),
SequenceTypeNode("map_string_and_vector_float::value", PrimitiveTypeNode.float_()),
required_modules=("flann",)),
AliasTypeNode.dict_("map_int_and_double",
PrimitiveTypeNode.int_("map_int_and_double::key"),
PrimitiveTypeNode.float_("map_int_and_double::value"),
required_modules=("flann",)),
# G-API from opencv_contrib
AliasTypeNode.union_("GProtoArg",
items=(AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GMat"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
SequenceTypeNode("GProtoArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.sequence_("GProtoInputArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.sequence_("GProtoOutputArgs", AliasRefTypeNode("GProtoArg"), required_modules=("gapi",)),
AliasTypeNode.union_(
"GRunArg",
items=(AliasRefTypeNode("Mat", "MatLike"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT"),
SequenceTypeNode("GRunArg", AnyTypeNode("GRunArg")),
NoneTypeNode("GRunArg")),
required_modules=("gapi",)
),
AliasTypeNode.optional_("GOptRunArg", AliasRefTypeNode("GRunArg"), required_modules=("gapi",)),
AliasTypeNode.union_("GMetaArg",
items=(ASTNodeTypeNode("GMat"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
AliasTypeNode.union_("Prim",
items=(ASTNodeTypeNode("gapi.wip.draw.Text"),
ASTNodeTypeNode("gapi.wip.draw.Circle"),
ASTNodeTypeNode("gapi.wip.draw.Image"),
ASTNodeTypeNode("gapi.wip.draw.Line"),
ASTNodeTypeNode("gapi.wip.draw.Rect"),
ASTNodeTypeNode("gapi.wip.draw.Mosaic"),
ASTNodeTypeNode("gapi.wip.draw.Poly")),
required_modules=("gapi",)),
SequenceTypeNode("Prims", AliasRefTypeNode("Prim"), required_modules=("gapi",)),
TupleTypeNode("GMat2", items=(ASTNodeTypeNode("GMat"),
ASTNodeTypeNode("GMat")), required_modules=("gapi",)),
ASTNodeTypeNode("GOpaque", "GOpaqueT", required_modules=("gapi",)),
ASTNodeTypeNode("GArray", "GArrayT", required_modules=("gapi",)),
AliasTypeNode.union_("GTypeInfo",
items=(ASTNodeTypeNode("GMat"),
AliasRefTypeNode("Scalar"),
ASTNodeTypeNode("GOpaqueT"),
ASTNodeTypeNode("GArrayT")),
required_modules=("gapi",)),
SequenceTypeNode("GCompileArgs", ASTNodeTypeNode("GCompileArg"), required_modules=("gapi",)),
SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo"), required_modules=("gapi",)),
SequenceTypeNode("GRunArgs", AliasRefTypeNode("GRunArg"), required_modules=("gapi",)),
SequenceTypeNode("GMetaArgs", AliasRefTypeNode("GMetaArg"), required_modules=("gapi",)),
SequenceTypeNode("GOptRunArgs", AliasRefTypeNode("GOptRunArg"), required_modules=("gapi",)),
AliasTypeNode.callable_(
"detail_ExtractArgsCallback",
arg_types=SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo")),
ret_type=SequenceTypeNode("GRunArgs", AliasRefTypeNode("GRunArg")),
export_name="ExtractArgsCallback",
required_modules=("gapi",)
),
AliasTypeNode.callable_(
"detail_ExtractMetaCallback",
arg_types=SequenceTypeNode("GTypesInfo", AliasRefTypeNode("GTypeInfo")),
ret_type=SequenceTypeNode("GMetaArgs", AliasRefTypeNode("GMetaArg")),
export_name="ExtractMetaCallback",
required_modules=("gapi",)
),
PrimitiveTypeNode("NativeByteArray", "bytes"),
)
PREDEFINED_TYPES = dict(
zip((t.ctype_name for t in _PREDEFINED_TYPES), _PREDEFINED_TYPES)
)
@@ -0,0 +1,333 @@
from typing import Tuple, List, Optional
from .predefined_types import PREDEFINED_TYPES
from .nodes.type_node import (
TypeNode, UnionTypeNode, SequenceTypeNode, ASTNodeTypeNode, TupleTypeNode
)
def replace_template_parameters_with_placeholders(string: str) \
-> Tuple[str, Tuple[str, ...]]:
"""Replaces template parameters with `format` placeholders for all template
instantiations in provided string.
Only outermost template parameters are replaced.
Args:
string (str): input string containing C++ template instantiations
Returns:
tuple[str, tuple[str, ...]]: string with '{}' placeholders template
instead of instantiation types and a tuple of extracted types.
>>> template_string, args = replace_template_parameters_with_placeholders(
... "std::vector<cv::Point<int>>, test<int>"
... )
>>> template_string.format(*args) == "std::vector<cv::Point<int>>, test<int>"
True
>>> replace_template_parameters_with_placeholders(
... "cv::util::variant<cv::GRunArgs, cv::GOptRunArgs>"
... )
('cv::util::variant<{}>', ('cv::GRunArgs, cv::GOptRunArgs',))
>>> replace_template_parameters_with_placeholders("vector<Point<int>>")
('vector<{}>', ('Point<int>',))
>>> replace_template_parameters_with_placeholders(
... "vector<Point<int>>, vector<float>"
... )
('vector<{}>, vector<{}>', ('Point<int>', 'float'))
>>> replace_template_parameters_with_placeholders("string without templates")
('string without templates', ())
"""
template_brackets_indices = []
template_instantiations_count = 0
template_start_index = 0
for i, c in enumerate(string):
if c == "<":
template_instantiations_count += 1
if template_instantiations_count == 1:
# + 1 - because left bound is included in substring range
template_start_index = i + 1
elif c == ">":
template_instantiations_count -= 1
assert template_instantiations_count >= 0, \
"Provided string is ill-formed. There are more '>' than '<'."
if template_instantiations_count == 0:
template_brackets_indices.append((template_start_index, i))
assert template_instantiations_count == 0, \
"Provided string is ill-formed. There are more '<' than '>'."
template_args: List[str] = []
# Reversed loop is required to preserve template start/end indices
for i, j in reversed(template_brackets_indices):
template_args.insert(0, string[i:j])
string = string[:i] + "{}" + string[j:]
return string, tuple(template_args)
def get_template_instantiation_type(typename: str) -> str:
"""Extracts outermost template instantiation type from provided string
Args:
typename (str): String containing C++ template instantiation.
Returns:
str: String containing template instantiation type
>>> get_template_instantiation_type("std::vector<cv::Point<int>>")
'cv::Point<int>'
>>> get_template_instantiation_type("std::vector<uchar>")
'uchar'
>>> get_template_instantiation_type("std::map<int, float>")
'int, float'
>>> get_template_instantiation_type("uchar")
Traceback (most recent call last):
...
ValueError: typename ('uchar') doesn't contain template instantiations
>>> get_template_instantiation_type("std::vector<int>, std::vector<float>")
Traceback (most recent call last):
...
ValueError: typename ('std::vector<int>, std::vector<float>') contains more than 1 template instantiation
"""
_, args = replace_template_parameters_with_placeholders(typename)
if len(args) == 0:
raise ValueError(
"typename ('{}') doesn't contain template instantiations".format(typename)
)
if len(args) > 1:
raise ValueError(
"typename ('{}') contains more than 1 template instantiation".format(typename)
)
return args[0]
def normalize_ctype_name(typename: str) -> str:
"""Normalizes C++ name by removing unnecessary namespace prefixes and possible
pointer/reference qualification. '::' are replaced with '_'.
NOTE: Pointer decay for 'void*' is not performed.
Args:
typename (str): Name of the C++ type for normalization
Returns:
str: Normalized C++ type name.
>>> normalize_ctype_name('std::vector<cv::Point2f>&')
'vector<cv_Point2f>'
>>> normalize_ctype_name('AKAZE::DescriptorType')
'AKAZE_DescriptorType'
>>> normalize_ctype_name('std::vector<Mat>')
'vector<Mat>'
>>> normalize_ctype_name('std::string')
'string'
>>> normalize_ctype_name('void*') # keep void* as is - special case
'void*'
>>> normalize_ctype_name('Ptr<AKAZE>')
'AKAZE'
>>> normalize_ctype_name('Algorithm_Ptr')
'Algorithm'
"""
for prefix_to_remove in ("cv", "std"):
if typename.startswith(prefix_to_remove):
typename = typename[len(prefix_to_remove):]
typename = typename.replace("::", "_").lstrip("_")
if typename.endswith('&'):
typename = typename[:-1]
typename = typename.strip()
if typename == 'void*':
return typename
if is_pointer_type(typename):
# Case for "type*", "type_Ptr", "typePtr"
for suffix in ("*", "_Ptr", "Ptr"):
if typename.endswith(suffix):
return typename[:-len(suffix)]
# Case Ptr<Type>
if _is_template_instantiation(typename):
return normalize_ctype_name(
get_template_instantiation_type(typename)
)
# Case Ptr_Type
return typename.split("_", maxsplit=1)[-1]
# special normalization for several G-API Types
if typename.startswith("GArray_") or typename.startswith("GArray<"):
return "GArrayT"
if typename.startswith("GOpaque_") or typename.startswith("GOpaque<"):
return "GOpaqueT"
if typename == "GStreamerPipeline" or typename.startswith("GStreamerSource"):
return "gst_" + typename
return typename
def is_tuple_type(typename: str) -> bool:
return typename.startswith("tuple") or typename.startswith("pair")
def is_sequence_type(typename: str) -> bool:
return typename.startswith("vector")
def is_pointer_type(typename: str) -> bool:
return typename.endswith("Ptr") or typename.endswith("*") \
or typename.startswith("Ptr")
def is_union_type(typename: str) -> bool:
return typename.startswith('util_variant')
def _is_template_instantiation(typename: str) -> bool:
"""Fast, but unreliable check whenever provided typename is a template
instantiation.
Args:
typename (str): typename to check against template instantiation.
Returns:
bool: True if provided `typename` contains template instantiation,
False otherwise
"""
if "<" in typename:
assert ">" in typename, \
"Wrong template class instantiation: {}. '>' is missing".format(typename)
return True
return False
def create_type_nodes_from_template_arguments(template_args_str: str) \
-> List[TypeNode]:
"""Creates a list of type nodes corresponding to the argument types
used for template instantiation.
This method correctly addresses the situation when arguments of the input
template are also templates.
Example:
if `create_type_node` is called with
`std::tuple<std::variant<int, Point2i>, int, std::vector<int>>`
this function will be called with
`std::variant<int, Point<int>>, int, std::vector<int>`
that produces the following order of types resolution
`std::variant` ~ `Union`
`std::variant<int, Point2i>` -> `int` ~ `int` -> `Union[int, Point2i]`
`Point2i` ~ `Point2i`
`int` -> `int`
`std::vector<int>` -> `std::vector` ~ `Sequence` -> `Sequence[int]`
`int` ~ `int`
Returns:
List[TypeNode]: set of type nodes used for template instantiation.
List is empty if input string doesn't contain template instantiation.
"""
type_nodes = []
template_args_str, templated_args_types = replace_template_parameters_with_placeholders(
template_args_str
)
template_index = 0
# For each template argument
for template_arg in template_args_str.split(","):
template_arg = template_arg.strip()
# Check if argument requires type substitution
if _is_template_instantiation(template_arg):
# Reconstruct the original type
template_arg = template_arg.format(templated_args_types[template_index])
template_index += 1
# create corresponding type node
type_nodes.append(create_type_node(template_arg))
return type_nodes
def create_type_node(typename: str,
original_ctype_name: Optional[str] = None) -> TypeNode:
"""Converts C++ type name to appropriate type used in Python library API.
Conversion procedure:
1. Normalize typename: remove redundant prefixes, unify name
components delimiters, remove reference qualifications.
2. Check whenever typename has a known predefined conversion or exported
as alias e.g.
- C++ `double` -> Python `float`
- C++ `cv::Rect` -> Python `Sequence[int]`
- C++ `std::vector<char>` -> Python `np.ndarray`
return TypeNode corresponding to the appropriate type.
3. Check whenever typename is a container of types e.g. variant,
sequence or tuple. If so, select appropriate Python container type
and perform arguments conversion.
4. Create a type node corresponding to the AST node passing normalized
typename as its name.
Args:
typename (str): C++ type name to convert.
original_ctype_name (Optional[str]): Original C++ name of the type.
`original_ctype_name` == `typename` if provided argument is None.
Default is None.
Returns:
TypeNode: type node that wraps C++ type exposed to Python
>>> create_type_node('Ptr<AKAZE>').typename
'AKAZE'
>>> create_type_node('std::vector<Ptr<cv::Algorithm>>').typename
'typing.Sequence[Algorithm]'
"""
if original_ctype_name is None:
original_ctype_name = typename
typename = normalize_ctype_name(typename.strip())
# if typename is a known alias or has explicitly defined substitution
type_node = PREDEFINED_TYPES.get(typename)
if type_node is not None:
type_node.ctype_name = original_ctype_name
return type_node
# If typename is a known exported alias name (e.g. IndexParams or SearchParams)
for alias in PREDEFINED_TYPES.values():
if alias.typename == typename:
return alias
if is_union_type(typename):
union_types = get_template_instantiation_type(typename)
return UnionTypeNode(
original_ctype_name,
items=create_type_nodes_from_template_arguments(union_types)
)
# if typename refers to a sequence type e.g. vector<int>
if is_sequence_type(typename):
# Recursively convert sequence element type
if _is_template_instantiation(typename):
inner_sequence_type = create_type_node(
get_template_instantiation_type(typename)
)
else:
# Handle vector_Type cases
# maxsplit=1 is required to handle sequence of sequence e.g:
# vector_vector_Mat -> Sequence[Sequence[Mat]]
inner_sequence_type = create_type_node(typename.split("_", 1)[-1])
return SequenceTypeNode(original_ctype_name, inner_sequence_type)
# If typename refers to a heterogeneous container
# (can contain elements of different types)
if is_tuple_type(typename):
tuple_types = get_template_instantiation_type(typename)
return TupleTypeNode(
original_ctype_name,
items=create_type_nodes_from_template_arguments(tuple_types)
)
# If everything else is False, it means that input typename refers to a
# class or enum of the library.
return ASTNodeTypeNode(original_ctype_name, typename)
if __name__ == "__main__":
import doctest
doctest.testmod()
@@ -0,0 +1,180 @@
"""Contains a class used to resolve compatibility issues with old Python versions.
Typing stubs generation is available starting from Python 3.6 only.
For other versions all calls to functions are noop.
"""
import sys
import warnings
if sys.version_info >= (3, 6):
from contextlib import contextmanager
from typing import Dict, Set, Any, Sequence, Generator, Union
import traceback
from pathlib import Path
from typing_stubs_generation import (
generate_typing_stubs,
NamespaceNode,
EnumerationNode,
SymbolName,
ClassNode,
create_function_node,
create_class_node,
find_class_node,
resolve_enum_scopes
)
import functools
class FailuresWrapper:
def __init__(self, exceptions_as_warnings=True):
self.has_failure = False
self.exceptions_as_warnings = exceptions_as_warnings
def wrap_exceptions_as_warnings(self, original_func=None,
ret_type_on_failure=None):
def parametrized_wrapper(func):
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
if self.has_failure:
if ret_type_on_failure is None:
return None
return ret_type_on_failure()
try:
ret_type = func(*args, **kwargs)
except Exception:
self.has_failure = True
warnings.warn(
"Typing stubs generation has failed.\n{}".format(
traceback.format_exc()
)
)
if ret_type_on_failure is None:
return None
return ret_type_on_failure()
return ret_type
if self.exceptions_as_warnings:
return wrapped_func
else:
return original_func
if original_func:
return parametrized_wrapper(original_func)
return parametrized_wrapper
@contextmanager
def delete_on_failure(self, file_path):
# type: (Path) -> Generator[None, None, None]
# There is no errors during stubs generation and file doesn't exist
if not self.has_failure and not file_path.is_file():
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.touch()
try:
# continue execution
yield
finally:
# If failure is occurred - delete file if exists
if self.has_failure and file_path.is_file():
file_path.unlink()
failures_wrapper = FailuresWrapper(exceptions_as_warnings=True)
class ClassNodeStub:
def add_base(self, base_node):
pass
class TypingStubsGenerator:
def __init__(self):
self.cv_root = NamespaceNode("cv", export_name="cv2")
self.exported_enums = {} # type: Dict[SymbolName, EnumerationNode]
self.type_hints_ignored_functions = set() # type: Set[str]
@failures_wrapper.wrap_exceptions_as_warnings
def add_enum(self, symbol_name, is_scoped_enum, entries):
# type: (SymbolName, bool, Dict[str, str]) -> None
if symbol_name in self.exported_enums:
assert symbol_name.name == "<unnamed>", \
"Trying to export 2 enums with same symbol " \
"name: {}".format(symbol_name)
enumeration_node = self.exported_enums[symbol_name]
else:
enumeration_node = EnumerationNode(symbol_name.name,
is_scoped_enum)
self.exported_enums[symbol_name] = enumeration_node
for entry_name, entry_value in entries.items():
enumeration_node.add_constant(entry_name, entry_value)
@failures_wrapper.wrap_exceptions_as_warnings
def add_ignored_function_name(self, function_name):
# type: (str) -> None
self.type_hints_ignored_functions.add(function_name)
@failures_wrapper.wrap_exceptions_as_warnings
def create_function_node(self, func_info):
# type: (Any) -> None
create_function_node(self.cv_root, func_info)
@failures_wrapper.wrap_exceptions_as_warnings(ret_type_on_failure=ClassNodeStub)
def find_class_node(self, class_info, namespaces):
# type: (Any, Sequence[str]) -> ClassNode
return find_class_node(
self.cv_root,
SymbolName.parse(class_info.full_original_name, namespaces),
create_missing_namespaces=True
)
@failures_wrapper.wrap_exceptions_as_warnings(ret_type_on_failure=ClassNodeStub)
def create_class_node(self, class_info, namespaces):
# type: (Any, Sequence[str]) -> ClassNode
return create_class_node(self.cv_root, class_info, namespaces)
def generate(self, output_path):
# type: (Union[str, Path]) -> None
output_path = Path(output_path)
py_typed_path = output_path / self.cv_root.export_name / 'py.typed'
with failures_wrapper.delete_on_failure(py_typed_path):
self._generate(output_path)
@failures_wrapper.wrap_exceptions_as_warnings
def _generate(self, output_path):
# type: (Path) -> None
resolve_enum_scopes(self.cv_root, self.exported_enums)
generate_typing_stubs(self.cv_root, output_path)
else:
class ClassNode:
def add_base(self, base_node):
pass
class TypingStubsGenerator:
def __init__(self):
self.type_hints_ignored_functions = set() # type: Set[str]
print(
'WARNING! Typing stubs can be generated only with Python 3.6 or higher. '
'Current version {}'.format(sys.version_info)
)
def add_enum(self, symbol_name, is_scoped_enum, entries):
pass
def add_ignored_function_name(self, function_name):
pass
def create_function_node(self, func_info):
pass
def create_class_node(self, class_info, namespaces):
return ClassNode()
def find_class_node(self, class_info, namespaces):
return ClassNode()
def generate(self, output_path):
pass
+65
View File
@@ -0,0 +1,65 @@
if(NOT DEFINED OpenCV_BINARY_DIR)
message(FATAL_ERROR "Define OpenCV_BINARY_DIR")
endif()
include("${OpenCV_BINARY_DIR}/opencv_python_config.cmake")
if(NOT DEFINED OpenCV_SOURCE_DIR)
message(FATAL_ERROR "Missing OpenCV_SOURCE_DIR")
endif()
if(DEFINED OPENCV_PYTHON_STANDALONE_INSTALL_PATH)
set(OPENCV_PYTHON_INSTALL_PATH "${OPENCV_PYTHON_STANDALONE_INSTALL_PATH}")
elseif(NOT OPENCV_PYTHON_INSTALL_PATH)
message(FATAL_ERROR "Missing OPENCV_PYTHON_STANDALONE_INSTALL_PATH / OPENCV_PYTHON_INSTALL_PATH")
endif()
include("${OpenCV_SOURCE_DIR}/cmake/OpenCVUtils.cmake")
set(OPENCV_PYTHON_SKIP_DETECTION ON)
include("${OpenCV_SOURCE_DIR}/cmake/OpenCVDetectPython.cmake")
find_python("${OPENCV_PYTHON_VERSION}" "${OPENCV_PYTHON_VERSION}" PYTHON_LIBRARY PYTHON_INCLUDE_DIR
PYTHONINTERP_FOUND PYTHON_EXECUTABLE PYTHON_VERSION_STRING
PYTHON_VERSION_MAJOR PYTHON_VERSION_MINOR PYTHONLIBS_FOUND
PYTHONLIBS_VERSION_STRING PYTHON_LIBRARIES PYTHON_LIBRARY
PYTHON_DEBUG_LIBRARIES PYTHON_LIBRARY_DEBUG PYTHON_INCLUDE_PATH
PYTHON_INCLUDE_DIR PYTHON_INCLUDE_DIR2 PYTHON_PACKAGES_PATH
PYTHON_NUMPY_INCLUDE_DIRS PYTHON_NUMPY_VERSION)
if(NOT PYTHON_EXECUTABLE OR NOT PYTHON_INCLUDE_DIR)
message(FATAL_ERROR "Can't find Python development files")
endif()
if(NOT PYTHON_NUMPY_INCLUDE_DIRS)
message(FATAL_ERROR "Can't find Python 'numpy' development files")
endif()
include("${OpenCV_SOURCE_DIR}/cmake/OpenCVInstallLayout.cmake")
include("${OpenCV_SOURCE_DIR}/cmake/OpenCVDetectDLPack.cmake")
status("-----------------------------------------------------------------")
status(" Python:")
status(" Interpreter:" "${PYTHON_EXECUTABLE} (ver ${PYTHON_VERSION_STRING})")
status(" Libraries:" "${PYTHON_LIBRARIES} (ver ${PYTHONLIBS_VERSION_STRING})")
status(" numpy:" "${PYTHON_NUMPY_INCLUDE_DIRS} (ver ${PYTHON_NUMPY_VERSION})")
status("")
status(" Install to:" "${CMAKE_INSTALL_PREFIX}")
status("-----------------------------------------------------------------")
set(OpenCV_DIR "${OpenCV_BINARY_DIR}")
find_package(OpenCV REQUIRED)
set(PYTHON PYTHON)
macro(ocv_add_module module_name)
set(the_module opencv_${module_name})
project(${the_module} CXX)
endmacro()
macro(ocv_module_include_directories module)
include_directories(${ARGN})
endmacro()
set(MODULE_NAME python)
set(MODULE_INSTALL_SUBDIR "")
set(LIBRARY_OUTPUT_PATH "${CMAKE_BINARY_DIR}/lib")
set(deps ${OpenCV_LIBRARIES})
include("${CMAKE_CURRENT_LIST_DIR}/common.cmake") # generate python target
# done, cleanup
unset(OPENCV_BUILD_INFO_STR CACHE) # remove from cache
+32
View File
@@ -0,0 +1,32 @@
set(MODULE_NAME "python_tests")
set(OPENCV_MODULE_IS_PART_OF_WORLD FALSE)
ocv_add_module(${MODULE_NAME} INTERNAL)
set(OPENCV_PYTHON_TESTS_CONFIG_FILE_DIR "${OpenCV_BINARY_DIR}" CACHE INTERNAL "")
set(OPENCV_PYTHON_TESTS_CONFIG_FILE "${OPENCV_PYTHON_TESTS_CONFIG_FILE_DIR}/opencv_python_tests.cfg" CACHE INTERNAL "")
# get list of modules to wrap
set(OPENCV_PYTHON_MODULES)
foreach(m ${OPENCV_MODULES_BUILD})
if(";${OPENCV_MODULE_${m}_WRAPPERS};" MATCHES ";python.*;" AND HAVE_${m})
list(APPEND OPENCV_PYTHON_MODULES ${m})
#message(STATUS "\t${m}")
endif()
endforeach()
file(RELATIVE_PATH __loc_relative "${OPENCV_PYTHON_TESTS_CONFIG_FILE_DIR}" "${CMAKE_CURRENT_LIST_DIR}")
set(opencv_tests_locations "${__loc_relative}")
foreach(m ${OPENCV_PYTHON_MODULES})
set(__loc "${OPENCV_MODULE_${m}_LOCATION}/misc/python/test")
if(EXISTS "${__loc}")
file(RELATIVE_PATH __loc_relative "${OPENCV_PYTHON_TESTS_CONFIG_FILE_DIR}" "${__loc}")
list(APPEND opencv_tests_locations "${__loc_relative}")
endif()
endforeach(m)
string(REPLACE ";" "\n" opencv_tests_locations_ "${opencv_tests_locations}")
ocv_update_file("${OPENCV_PYTHON_TESTS_CONFIG_FILE}" "${opencv_tests_locations_}")
#
# TODO: Install rules (with test data?)
#
+2
View File
@@ -0,0 +1,2 @@
pyyml
numpy
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python
'''
Location of tests:
- <opencv_src>/modules/python/test
- <opencv_src>/modules/<module>/misc/python/test/
'''
from __future__ import print_function
import sys
sys.dont_write_bytecode = True # Don't generate .pyc files / __pycache__ directories
import os
import unittest
# Python 3 moved urlopen to urllib.requests
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
from tests_common import NewOpenCVTests
basedir = os.path.abspath(os.path.dirname(__file__))
def load_tests(loader, tests, pattern):
cwd = os.getcwd()
config_file = 'opencv_python_tests.cfg'
locations = [cwd, basedir]
if os.path.exists(config_file):
with open(config_file, 'r') as f:
locations += [str(s).strip() for s in f.readlines()]
else:
print('WARNING: OpenCV tests config file ({}) is missing, running subset of tests'.format(config_file))
tests_pattern = os.environ.get('OPENCV_PYTEST_FILTER', 'test_*') + '.py'
if tests_pattern != 'test_*.py':
print('Tests filter: {}'.format(tests_pattern))
processed = set()
for l in locations:
if not os.path.isabs(l):
l = os.path.normpath(os.path.join(cwd, l))
if l in processed:
continue
processed.add(l)
print('Discovering python tests from: {}'.format(l))
sys_path_modify = l not in sys.path
if sys_path_modify:
sys.path.append(l) # Hack python loader
discovered_tests = loader.discover(l, pattern=tests_pattern, top_level_dir=l)
print(' found {} tests'.format(discovered_tests.countTestCases()))
tests.addTests(loader.discover(l, pattern=tests_pattern))
if sys_path_modify:
sys.path.remove(l)
return tests
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python
"""Algorithm serialization test."""
import tempfile
import os
import cv2 as cv
from tests_common import NewOpenCVTests
class algorithm_rw_test(NewOpenCVTests):
def test_algorithm_rw(self):
fd, fname = tempfile.mkstemp(prefix="opencv_python_algorithm_", suffix=".yml")
os.close(fd)
# some arbitrary non-default parameters
gold = cv.ORB_create(nfeatures=200, scaleFactor=1.3, nlevels=5, edgeThreshold=28)
gold.write(cv.FileStorage(fname, cv.FILE_STORAGE_WRITE), "ORB")
fs = cv.FileStorage(fname, cv.FILE_STORAGE_READ)
algorithm = cv.ORB_create()
algorithm.read(fs.getNode("ORB"))
self.assertEqual(algorithm.getMaxFeatures(), 200)
self.assertAlmostEqual(algorithm.getScaleFactor(), 1.3, places=6)
self.assertEqual(algorithm.getNLevels(), 5)
self.assertEqual(algorithm.getEdgeThreshold(), 28)
os.remove(fname)
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class AsyncTest(NewOpenCVTests):
def test_async_simple(self):
m = np.array([[1,2],[3,4],[5,6]])
async_result = cv.utils.testAsyncArray(m)
self.assertTrue(async_result.valid())
ret, result = async_result.get(timeoutNs=10**6) # 1ms
self.assertTrue(ret)
self.assertFalse(async_result.valid())
self.assertEqual(cv.norm(m, result, cv.NORM_INF), 0)
def test_async_exception(self):
async_result = cv.utils.testAsyncException()
self.assertTrue(async_result.valid())
try:
_ret, _result = async_result.get(timeoutNs=10**6) # 1ms
self.fail("Exception expected")
except cv.error as e:
self.assertEqual(cv.Error.StsOk, e.code)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env python
'''
Camshift tracker
================
This is a demo that shows mean-shift based tracking
You select a color objects such as your face and it tracks it.
This reads from video camera (0 by default, or the camera number the user enters)
http://www.robinhewitt.com/research/track/camshift.html
'''
# Python 2/3 compatibility
from __future__ import print_function
import sys
PY3 = sys.version_info[0] == 3
if PY3:
xrange = range
import numpy as np
import cv2 as cv
from tst_scene_render import TestSceneRender
from tests_common import NewOpenCVTests, intersectionRate
class camshift_test(NewOpenCVTests):
framesNum = 300
frame = None
selection = None
drag_start = None
show_backproj = False
track_window = None
render = None
errors = 0
def prepareRender(self):
self.render = TestSceneRender(self.get_sample('samples/data/pca_test1.jpg'), deformation = True)
def runTracker(self):
framesCounter = 0
self.selection = True
xmin, ymin, xmax, ymax = self.render.getCurrentRect()
self.track_window = (xmin, ymin, xmax - xmin, ymax - ymin)
while True:
framesCounter += 1
self.frame = self.render.getNextFrame()
hsv = cv.cvtColor(self.frame, cv.COLOR_BGR2HSV)
mask = cv.inRange(hsv, np.array((0., 60., 32.)), np.array((180., 255., 255.)))
if self.selection:
x0, y0, x1, y1 = self.render.getCurrentRect() + 50
x0 -= 100
y0 -= 100
hsv_roi = hsv[y0:y1, x0:x1]
mask_roi = mask[y0:y1, x0:x1]
hist = cv.calcHist( [hsv_roi], [0], mask_roi, [16], [0, 180] )
cv.normalize(hist, hist, 0, 255, cv.NORM_MINMAX)
self.hist = hist.reshape(-1)
self.selection = False
if self.track_window and self.track_window[2] > 0 and self.track_window[3] > 0:
self.selection = None
prob = cv.calcBackProject([hsv], [0], self.hist, [0, 180], 1)
prob &= mask
term_crit = ( cv.TERM_CRITERIA_EPS | cv.TERM_CRITERIA_COUNT, 10, 1 )
_track_box, self.track_window = cv.CamShift(prob, self.track_window, term_crit)
trackingRect = np.array(self.track_window)
trackingRect[2] += trackingRect[0]
trackingRect[3] += trackingRect[1]
if intersectionRate(self.render.getCurrentRect(), trackingRect) < 0.4:
self.errors += 1
if framesCounter > self.framesNum:
break
self.assertLess(float(self.errors) / self.framesNum, 0.4)
def test_camshift(self):
self.prepareRender()
self.runTracker()
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
import tempfile
from tests_common import NewOpenCVTests
class photo_test(NewOpenCVTests):
def setUp(self):
super(photo_test, self).setUp()
self.image_cache = {}
def test_model(self):
s = np.array([
[214.11, 98.67, 37.97],
[231.94, 153.1, 85.27],
[204.08, 143.71, 78.46],
[190.58, 122.99, 30.84],
[230.93, 148.46, 100.84],
[228.64, 206.97, 97.5],
[229.09, 137.07, 55.29],
[189.21, 111.22, 92.66],
[223.5, 96.42, 75.45],
[201.82, 69.71, 50.9],
[240.52, 196.47, 59.3],
[235.73, 172.13, 54.],
[131.6, 75.04, 68.86],
[189.04, 170.43, 42.05],
[222.23, 74., 71.95],
[241.01, 199.1, 61.15],
[224.99, 101.4, 100.24],
[174.58, 152.63, 91.52],
[248.06, 227.69, 140.5],
[241.15, 201.38, 115.58],
[236.49, 175.87, 88.86],
[212.19, 133.49, 54.79],
[181.17, 102.94, 36.18],
[115.1, 53.77, 15.23]
], dtype=np.float64)
src = (s / 255.).astype(np.float64).reshape(-1, 1, 3)
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
colorCorrectionMat = model.compute()
src_rgbl = np.array([
[0.68078957, 0.12382801, 0.01514889],
[0.81177942, 0.32550452, 0.089818],
[0.61259378, 0.2831933, 0.07478902],
[0.52696493, 0.20105976, 0.00958657],
[0.80402284, 0.30419523, 0.12989841],
[0.78658646, 0.63184111, 0.12062068],
[0.78999637, 0.25520249, 0.03462853],
[0.51866697, 0.16114393, 0.1078387],
[0.74820768, 0.11770076, 0.06862177],
[0.59776825, 0.05765816, 0.02886627],
[0.8793145, 0.56346033, 0.0403954],
[0.84124847, 0.42120746, 0.03287592],
[0.23333214, 0.06780408, 0.05612276],
[0.5176423, 0.41210976, 0.01896255],
[0.73888613, 0.06575388, 0.06181293],
[0.88326036, 0.58018751, 0.04321991],
[0.75922531, 0.13149072, 0.1282041],
[0.4345097, 0.32331019, 0.10494139],
[0.94110142, 0.77941419, 0.26946323],
[0.88438952, 0.5949049, 0.17536928],
[0.84722687, 0.44160449, 0.09834799],
[0.66743106, 0.24076803, 0.03394333],
[0.47141286, 0.13592419, 0.01362205],
[0.17377101, 0.03256864, 0.00203026]
], dtype=np.float64)
np.testing.assert_allclose(src_rgbl, model.getSrcLinearRGB().reshape(-1, 3), rtol=1e-4, atol=1e-4)
dst_rgbl = np.array([
[0.17303173, 0.08211037, 0.05672686],
[0.56832031, 0.29269488, 0.21835529],
[0.10365019, 0.19588357, 0.33140475],
[0.10159676, 0.14892193, 0.05188294],
[0.22159627, 0.21584476, 0.43461196],
[0.10806379, 0.51437196, 0.41264213],
[0.74736423, 0.20062878, 0.02807988],
[0.05757947, 0.10516793, 0.40296109],
[0.56676218, 0.08424805, 0.11969461],
[0.11099515, 0.04230796, 0.14292554],
[0.34546869, 0.50872001, 0.04944204],
[0.79461323, 0.35942459, 0.02051968],
[0.01710416, 0.05022043, 0.29220674],
[0.05598012, 0.30021149, 0.06871162],
[0.45585457, 0.03033727, 0.04085654],
[0.85737614, 0.56757335, 0.0068503],
[0.53348585, 0.08861148, 0.30750446],
[-0.0374061, 0.24699498, 0.40041217],
[0.91262695, 0.91493909, 0.89367049],
[0.57981916, 0.59200418, 0.59328881],
[0.35490581, 0.36544831, 0.36755375],
[0.19007357, 0.19186587, 0.19308397],
[0.08529188, 0.08887994, 0.09257601],
[0.0303193, 0.03113818, 0.03274845]
], dtype=np.float64)
np.testing.assert_allclose(dst_rgbl, model.getRefLinearRGB().reshape(-1, 3), rtol=1e-4, atol=1e-4)
mask = np.ones((24, 1), dtype=np.uint8)
np.testing.assert_allclose(model.getMask(), mask, rtol=0.0, atol=0.0)
# Test reference color matrix
refColorMat = np.array([
[0.37406520, 0.02066507, 0.05804047],
[0.12719672, 0.77389268, -0.01569404],
[-0.27627010, 0.00603427, 2.74272981]
], dtype=np.float64)
np.testing.assert_allclose(colorCorrectionMat, refColorMat, rtol=1e-4, atol=1e-4)
def test_masks_weights_1(self):
s = np.array([
[214.11, 98.67, 37.97],
[231.94, 153.1, 85.27],
[204.08, 143.71, 78.46],
[190.58, 122.99, 30.84],
[230.93, 148.46, 100.84],
[228.64, 206.97, 97.5],
[229.09, 137.07, 55.29],
[189.21, 111.22, 92.66],
[223.5, 96.42, 75.45],
[201.82, 69.71, 50.9],
[240.52, 196.47, 59.3],
[235.73, 172.13, 54.],
[131.6, 75.04, 68.86],
[189.04, 170.43, 42.05],
[222.23, 74., 71.95],
[241.01, 199.1, 61.15],
[224.99, 101.4, 100.24],
[174.58, 152.63, 91.52],
[248.06, 227.69, 140.5],
[241.15, 201.38, 115.58],
[236.49, 175.87, 88.86],
[212.19, 133.49, 54.79],
[181.17, 102.94, 36.18],
[115.1, 53.77, 15.23]
], dtype=np.float64)
weightsList = np.array([1.1, 0, 0, 1.2, 0, 0, 1.3, 0, 0, 1.4, 0, 0,
0.5, 0, 0, 0.6, 0, 0, 0.7, 0, 0, 0.8, 0, 0], dtype=np.float64)
weightsList = weightsList.reshape(-1, 1)
src = (s / 255.).astype(np.float64).reshape(-1, 1, 3)
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
model.setColorSpace(cv.ccm.COLOR_SPACE_SRGB)
model.setCcmType(cv.ccm.CCM_LINEAR)
model.setDistance(cv.ccm.DISTANCE_CIE2000)
model.setLinearization(cv.ccm.LINEARIZATION_GAMMA)
model.setLinearizationGamma(2.2)
model.setLinearizationDegree(3)
model.setSaturatedThreshold(0, 0.98)
model.setWeightsList(weightsList)
model.setWeightCoeff(1.5)
_ = model.compute()
weights = np.array([1.15789474, 1.26315789, 1.36842105, 1.47368421,
0.52631579, 0.63157895, 0.73684211, 0.84210526], dtype=np.float64)
np.testing.assert_allclose(model.getWeights(), weights.reshape(-1, 1), rtol=1e-4, atol=1e-4)
mask = np.array([True, False, False, True, False, False,
True, False, False, True, False, False,
True, False, False, True, False, False,
True, False, False, True, False, False], dtype=np.uint8)
np.testing.assert_allclose(model.getMask(), mask.reshape(-1, 1), rtol=0.0, atol=0.0)
def test_masks_weights_2(self):
s = np.array([
[214.11, 98.67, 37.97],
[231.94, 153.1, 85.27],
[204.08, 143.71, 78.46],
[190.58, 122.99, 30.84],
[230.93, 148.46, 100.84],
[228.64, 206.97, 97.5],
[229.09, 137.07, 55.29],
[189.21, 111.22, 92.66],
[223.5, 96.42, 75.45],
[201.82, 69.71, 50.9],
[240.52, 196.47, 59.3],
[235.73, 172.13, 54.],
[131.6, 75.04, 68.86],
[189.04, 170.43, 42.05],
[222.23, 74., 71.95],
[241.01, 199.1, 61.15],
[224.99, 101.4, 100.24],
[174.58, 152.63, 91.52],
[248.06, 227.69, 140.5],
[241.15, 201.38, 115.58],
[236.49, 175.87, 88.86],
[212.19, 133.49, 54.79],
[181.17, 102.94, 36.18],
[115.1, 53.77, 15.23]
], dtype=np.float64)
src = (s / 255.).astype(np.float64).reshape(-1, 1, 3)
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
model.setCcmType(cv.ccm.CCM_LINEAR)
model.setDistance(cv.ccm.DISTANCE_CIE2000)
model.setLinearization(cv.ccm.LINEARIZATION_GAMMA)
model.setLinearizationGamma(2.2)
model.setLinearizationDegree(3)
model.setSaturatedThreshold(0.05, 0.93)
model.setWeightsList(np.array([]))
model.setWeightCoeff(1.5)
_ = model.compute()
weights = np.array([
0.65554256, 1.49454705, 1.00499244, 0.79735434, 1.16327759,
1.68623868, 1.37973155, 0.73213388, 1.0169629, 0.47430246,
1.70312161, 0.45414218, 1.15910007, 0.7540434, 1.05049802,
1.04551645, 1.54082353, 1.02453421, 0.6015915, 0.26154558
], dtype=np.float64)
np.testing.assert_allclose(model.getWeights(), weights.reshape(-1, 1), rtol=1e-4, atol=1e-4)
# Test mask
mask = np.array([True, True, True, True, True, True,
True, True, True, True, False, True,
True, True, True, False, True, True,
False, False, True, True, True, True], dtype=np.uint8)
np.testing.assert_allclose(model.getMask(), mask.reshape(-1, 1), rtol=0.0, atol=0.0)
def test_compute_color_correction_matrix(self):
path = self.find_file('cv/mcc/mcc_ccm_test.yml')
fs = cv.FileStorage(path, cv.FileStorage_READ)
chartsRGB = fs.getNode("chartsRGB").mat()
src = (chartsRGB[:, 1].reshape(-1, 1, 3) / 255.).astype(np.float64)
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
colorCorrectionMat = model.compute()
gold_ccm = fs.getNode("ccm").mat()
fs.release()
np.testing.assert_allclose(gold_ccm, colorCorrectionMat, rtol=1e-8, atol=1e-8)
gold_loss = 4.6386569120323129
loss = model.getLoss()
self.assertAlmostEqual(gold_loss, loss, places=8)
def test_correctImage(self):
img = self.get_sample('cv/mcc/mcc_ccm_test.jpg')
self.assertIsNotNone(img, "Test image can't be loaded: ")
gold_img = self.get_sample('cv/mcc/mcc_ccm_test_res.png')
self.assertIsNotNone(gold_img, "Ground truth for test image can't be loaded: ")
path = self.find_file("cv/mcc/mcc_ccm_test.yml")
fs = cv.FileStorage(path, cv.FileStorage_READ)
chartsRGB = fs.getNode("chartsRGB").mat()
fs.release()
src = (chartsRGB[:, 1].reshape(-1, 1, 3) / 255.).astype(np.float64)
np.savetxt('src_test_correct.txt',src.reshape(-1,3),fmt="%.2f")
model = cv.ccm.ColorCorrectionModel(src, cv.ccm.COLORCHECKER_MACBETH)
_ = model.compute()
calibratedImage = np.zeros_like(img)
model.correctImage(img, calibratedImage)
np.testing.assert_allclose(gold_img, calibratedImage, rtol=0.1, atol=0.1)
def test_mcc_ccm_combined(self):
detector = cv.mcc_CCheckerDetector.create()
img = self.get_sample('cv/mcc/mcc_ccm_test.jpg')
self.assertIsNotNone(img, "Test image can't be loaded: ")
gold_img = self.get_sample('cv/mcc/mcc_ccm_test_res.png')
self.assertIsNotNone(gold_img, "Ground truth for test image can't be loaded: ")
detector.setColorChartType(cv.mcc.MCC24)
self.assertTrue(detector.process(img))
checkers = detector.getListColorChecker()
# Get colors from detector and save for debugging
src = checkers[0].getChartsRGB(False).reshape(-1, 1, 3) / 255.
src = src.astype(np.float64)
# Load reference colors from file for comparison
path = self.find_file('cv/mcc/mcc_ccm_test.yml')
fs = cv.FileStorage(path, cv.FileStorage_READ)
chartsRGB = fs.getNode("chartsRGB").mat()
ref_src = (chartsRGB[:, 1].reshape(-1, 1, 3) / 255.).astype(np.float64)
fs.release()
# Verify that detected colors are close to reference colors
np.testing.assert_allclose(src, ref_src, rtol=0.01, atol=0.01)
# Use reference colors for model computation
model = cv.ccm.ColorCorrectionModel(ref_src, cv.ccm.COLORCHECKER_MACBETH)
_ = model.compute()
calibratedImage = np.zeros_like(img)
model.correctImage(img, calibratedImage)
np.testing.assert_allclose(gold_img, calibratedImage, rtol=0.1, atol=0.1)
def test_serialization(self):
path1 = self.find_file("cv/mcc/mcc_ccm_test.yml")
fs = cv.FileStorage(path1, cv.FileStorage_READ)
chartsRGB = fs.getNode("chartsRGB").mat()
fs.release()
model = cv.ccm.ColorCorrectionModel(chartsRGB[:, 1].reshape(-1, 1, 3) / 255., cv.ccm.COLORCHECKER_MACBETH)
_ = model.compute()
path1 = tempfile.mktemp(suffix='.yaml')
fs1 = cv.FileStorage(path1, cv.FileStorage_WRITE)
model.write(fs1)
fs1.release()
model1 = cv.ccm.ColorCorrectionModel()
fs2 = cv.FileStorage(path1, cv.FileStorage_READ)
modelNode = fs2.getNode("ColorCorrectionModel")
model1.read(modelNode)
fs2.release()
path2 = tempfile.mktemp(suffix='.yaml')
fs3 = cv.FileStorage(path2, cv.FileStorage_WRITE)
model1.write(fs3)
fs3.release()
with open(path1, 'r') as file1:
str1 = file1.read()
with open(path2, 'r') as file2:
str2 = file2.read()
self.assertEqual(str1, str2)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html.
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
EXPECTED_COEFFS_SIZE = 78
class ChromaticAberrationTest(NewOpenCVTests):
def setUp(self):
super().setUp()
self.test_yaml_file = self.find_file(
"cv/cameracalibration/chromatic_aberration/ca_photo_calib.yaml"
)
self.test_image = self.get_sample(
"cv/cameracalibration/chromatic_aberration/ca_photo.png", 1
)
self.assertIsNotNone(self.test_image, "Failed to load test image")
self.assertFalse(self.test_image.size == 0, "Failed to load test image")
def test_load_calib_and_correct_image(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
coeffMat, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
self.assertIsInstance(coeffMat, np.ndarray)
self.assertEqual(coeffMat.dtype, np.float32)
self.assertEqual(coeffMat.shape[0], 4)
self.assertGreater(coeffMat.shape[1], 0)
self.assertGreater(degree, 0)
self.assertGreater(calib_size[0], 0)
self.assertGreater(calib_size[1], 0)
self.assertEqual(coeffMat.shape[1], EXPECTED_COEFFS_SIZE)
self.assertEqual(self.test_image.shape[1], calib_size[0])
self.assertEqual(self.test_image.shape[0], calib_size[1])
corrected = cv.correctChromaticAberration(self.test_image, coeffMat, calib_size, degree)
self.assertEqual(corrected.shape[:2], self.test_image.shape[:2])
self.assertEqual(corrected.dtype, self.test_image.dtype)
diff = cv.absdiff(self.test_image, corrected)
sum_diff = cv.sumElems(diff)
self.assertGreater(sum(sum_diff[:3]), 0.0)
def test_yaml_contents_as_expected(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
red_node = fs.getNode("red_channel")
blue_node = fs.getNode("blue_channel")
self.assertTrue(red_node.isMap())
self.assertTrue(blue_node.isMap())
coeffs_x = red_node.getNode("coeffs_x")
self.assertIsNotNone(coeffs_x)
self.assertEqual(coeffs_x.size(), EXPECTED_COEFFS_SIZE)
coeffs_x = blue_node.getNode("coeffs_x")
self.assertIsNotNone(coeffs_x)
self.assertEqual(coeffs_x.size(), EXPECTED_COEFFS_SIZE)
coeffs_y = red_node.getNode("coeffs_y")
self.assertIsNotNone(coeffs_y)
self.assertEqual(coeffs_y.size(), EXPECTED_COEFFS_SIZE)
coeffs_y = blue_node.getNode("coeffs_y")
self.assertIsNotNone(coeffs_y)
self.assertEqual(coeffs_y.size(), EXPECTED_COEFFS_SIZE)
fs.release()
def test_invalid_single_channel(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
coeffMat, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
gray = cv.cvtColor(self.test_image, cv.COLOR_BGR2GRAY)
with self.assertRaises(cv.error):
_ = cv.correctChromaticAberration(gray, coeffMat, calib_size, degree)
def test_empty_coeff_mat(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
_, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
emptyCoeff = np.empty((0, 0), dtype=np.float32)
with self.assertRaises(cv.error):
_ = cv.correctChromaticAberration(self.test_image, emptyCoeff, calib_size, degree)
def test_mismatched_image_size(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
coeffMat, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
resized = cv.resize(self.test_image, (self.test_image.shape[1] // 2, self.test_image.shape[0] // 2))
with self.assertRaises(cv.error):
_ = cv.correctChromaticAberration(resized, coeffMat, calib_size, degree)
def test_wrong_coeff_type(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
coeffMat, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
wrongType = coeffMat.astype(np.float64)
with self.assertRaises(cv.error):
_ = cv.correctChromaticAberration(self.test_image, wrongType, calib_size, degree)
def test_degree_does_not_match_coeff_cols(self):
fs = cv.FileStorage(self.test_yaml_file, cv.FileStorage_READ)
self.assertTrue(fs.isOpened())
coeffMat, calib_size, degree = cv.loadChromaticAberrationParams(fs.root())
wrongDegree = max(1, degree - 1)
self.assertNotEqual(wrongDegree, coeffMat.shape[1])
with self.assertRaises(cv.error):
_ = cv.correctChromaticAberration(self.test_image, coeffMat, calib_size, wrongDegree)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python
'''
Test for copyto with mask
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import sys
from tests_common import NewOpenCVTests
class copytomask_test(NewOpenCVTests):
def test_copytomask(self):
img = self.get_sample('python/images/baboon.png', cv.IMREAD_COLOR)
eps = 0.
#Create mask using inRange
valeurBGRinf = np.array([0,0,100])
valeurBGRSup = np.array([70, 70,255])
maskRed = cv.inRange(img, valeurBGRinf, valeurBGRSup)
#New binding
dstcv = np.ndarray(np.array((2, 2, 1))*img.shape, dtype=img.dtype)
dstcv.fill(255)
cv.copyTo(img, maskRed, dstcv[:img.shape[0],:img.shape[1],:])
#using numpy
dstnp = np.ndarray(np.array((2, 2, 1))*img.shape, dtype=img.dtype)
dstnp.fill(255)
mask2=maskRed.astype(bool)
_, mask_b = np.broadcast_arrays(img, mask2[..., None])
np.copyto(dstnp[:img.shape[0],:img.shape[1],:], img, where=mask_b)
self.assertEqual(cv.norm(dstnp ,dstcv), eps)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python
'''
CUDA-accelerated Computer Vision functions
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import os
from tests_common import NewOpenCVTests, unittest
class cuda_test(NewOpenCVTests):
def setUp(self):
super(cuda_test, self).setUp()
if not cv.cuda.getCudaEnabledDeviceCount():
self.skipTest("No CUDA-capable device is detected")
def test_cuda_upload_download(self):
npMat = (np.random.random((128, 128, 3)) * 255).astype(np.uint8)
cuMat = cv.cuda_GpuMat()
cuMat.upload(npMat)
self.assertTrue(np.allclose(cuMat.download(), npMat))
def test_cuda_upload_download_stream(self):
stream = cv.cuda_Stream()
npMat = (np.random.random((128, 128, 3)) * 255).astype(np.uint8)
cuMat = cv.cuda_GpuMat(128,128, cv.CV_8UC3)
cuMat.upload(npMat, stream)
npMat2 = cuMat.download(stream=stream)
stream.waitForCompletion()
self.assertTrue(np.allclose(npMat2, npMat))
def test_cuda_interop(self):
npMat = (np.random.random((128, 128, 3)) * 255).astype(np.uint8)
cuMat = cv.cuda_GpuMat()
cuMat.upload(npMat)
self.assertTrue(cuMat.cudaPtr() != 0)
cuMatFromPtrSz = cv.cuda.createGpuMatFromCudaMemory(cuMat.size(),cuMat.type(),cuMat.cudaPtr(), cuMat.step)
self.assertTrue(cuMat.cudaPtr() == cuMatFromPtrSz.cudaPtr())
cuMatFromPtrRc = cv.cuda.createGpuMatFromCudaMemory(cuMat.size()[1],cuMat.size()[0],cuMat.type(),cuMat.cudaPtr(), cuMat.step)
self.assertTrue(cuMat.cudaPtr() == cuMatFromPtrRc.cudaPtr())
stream = cv.cuda_Stream()
self.assertTrue(stream.cudaPtr() != 0)
streamFromPtr = cv.cuda.wrapStream(stream.cudaPtr())
self.assertTrue(stream.cudaPtr() == streamFromPtr.cudaPtr())
asyncstream = cv.cuda_Stream(1) # cudaStreamNonBlocking
self.assertTrue(asyncstream.cudaPtr() != 0)
def test_cuda_buffer_pool(self):
stream_a = cv.cuda.Stream()
cv.cuda.setBufferPoolUsage(True)
cv.cuda.setBufferPoolConfig(cv.cuda.getDevice(), 1024 * 1024 * 64, 2)
pool_a = cv.cuda.BufferPool(stream_a)
cuMat = pool_a.getBuffer(1024, 1024, cv.CV_8UC3)
cv.cuda.setBufferPoolUsage(False)
self.assertEqual(cuMat.size(), (1024, 1024))
self.assertEqual(cuMat.type(), cv.CV_8UC3)
def test_cuda_release(self):
npMat = (np.random.random((128, 128, 3)) * 255).astype(np.uint8)
cuMat = cv.cuda_GpuMat()
cuMat.upload(npMat)
cuMat.release()
self.assertTrue(cuMat.cudaPtr() == 0)
self.assertTrue(cuMat.step == 0)
self.assertTrue(cuMat.size() == (0, 0))
def test_cuda_convertTo(self):
# setup
npMat_8UC4 = (np.random.random((128, 128, 4)) * 255).astype(np.uint8)
npMat_32FC4 = npMat_8UC4.astype(np.single)
new_type = cv.CV_32FC4
# sync
# in/out
cuMat_8UC4 = cv.cuda_GpuMat(npMat_8UC4)
cuMat_32FC4 = cv.cuda_GpuMat(cuMat_8UC4.size(), new_type)
cuMat_32FC4_out = cuMat_8UC4.convertTo(new_type, cuMat_32FC4)
self.assertTrue(cuMat_32FC4.cudaPtr() == cuMat_32FC4_out.cudaPtr())
npMat_32FC4_out = cuMat_32FC4.download()
self.assertTrue(np.array_equal(npMat_32FC4, npMat_32FC4_out))
# out
cuMat_32FC4_out = cuMat_8UC4.convertTo(new_type)
npMat_32FC4_out = cuMat_32FC4.download()
self.assertTrue(np.array_equal(npMat_32FC4, npMat_32FC4_out))
# async
stream = cv.cuda.Stream()
cuMat_32FC4 = cv.cuda_GpuMat(cuMat_8UC4.size(), new_type)
cuMat_32FC4_out = cuMat_8UC4.convertTo(new_type, cuMat_32FC4)
# in/out
cuMat_32FC4_out = cuMat_8UC4.convertTo(new_type, 1, 0, stream, cuMat_32FC4)
self.assertTrue(cuMat_32FC4.cudaPtr() == cuMat_32FC4_out.cudaPtr())
npMat_32FC4_out = cuMat_32FC4.download(stream)
stream.waitForCompletion()
self.assertTrue(np.array_equal(npMat_32FC4, npMat_32FC4_out))
# out
cuMat_32FC4_out = cuMat_8UC4.convertTo(new_type, 1, 0, stream)
npMat_32FC4_out = cuMat_32FC4.download(stream)
stream.waitForCompletion()
self.assertTrue(np.array_equal(npMat_32FC4, npMat_32FC4_out))
def test_cuda_copyTo(self):
# setup
npMat_8UC4 = (np.random.random((128, 128, 4)) * 255).astype(np.uint8)
# sync
# in/out
cuMat_8UC4 = cv.cuda_GpuMat(npMat_8UC4)
cuMat_8UC4_dst = cv.cuda_GpuMat(cuMat_8UC4.size(), cuMat_8UC4.type())
cuMat_8UC4_out = cuMat_8UC4.copyTo(cuMat_8UC4_dst)
self.assertTrue(cuMat_8UC4_out.cudaPtr() == cuMat_8UC4_dst.cudaPtr())
npMat_8UC4_out = cuMat_8UC4_out.download()
self.assertTrue(np.array_equal(npMat_8UC4, npMat_8UC4_out))
# out
cuMat_8UC4_out = cuMat_8UC4.copyTo()
npMat_8UC4_out = cuMat_8UC4_out.download()
self.assertTrue(np.array_equal(npMat_8UC4, npMat_8UC4_out))
# async
stream = cv.cuda.Stream()
# in/out
cuMat_8UC4 = cv.cuda_GpuMat(npMat_8UC4)
cuMat_8UC4_dst = cv.cuda_GpuMat(cuMat_8UC4.size(), cuMat_8UC4.type())
cuMat_8UC4_out = cuMat_8UC4.copyTo(cuMat_8UC4_dst, stream)
self.assertTrue(cuMat_8UC4_out.cudaPtr() == cuMat_8UC4_out.cudaPtr())
npMat_8UC4_out = cuMat_8UC4_dst.download(stream)
stream.waitForCompletion()
self.assertTrue(np.array_equal(npMat_8UC4, npMat_8UC4_out))
# out
cuMat_8UC4_out = cuMat_8UC4.copyTo(stream)
npMat_8UC4_out = cuMat_8UC4_out.download(stream)
stream.waitForCompletion()
self.assertTrue(np.array_equal(npMat_8UC4, npMat_8UC4_out))
def test_cuda_denoising(self):
self.assertEqual(True, hasattr(cv.cuda, 'fastNlMeansDenoising'))
self.assertEqual(True, hasattr(cv.cuda, 'fastNlMeansDenoisingColored'))
self.assertEqual(True, hasattr(cv.cuda, 'nonLocalMeans'))
def test_dlpack_GpuMat(self):
for dtype in [np.int8, np.uint8, np.int16, np.uint16, np.float16, np.int32, np.float32, np.float64, np.int64, np.uint32, np.uint64, np.bool_]:
for channels in [2, 3, 5]:
ref = (np.random.random((64, 128, channels)) * 255).astype(dtype)
src = cv.cuda_GpuMat()
src.upload(ref)
# workaround int64/uint64 conversion to int32/uint32
if dtype == np.int64:
print("skip because of https://github.com/opencv/opencv/issues/27671")
continue
src = src.convertTo(cv.CV_64S)
elif dtype == np.uint64:
print("skip because of https://github.com/opencv/opencv/issues/27671")
continue
src = src.convertTo(cv.CV_64U)
dst = cv.cuda_GpuMat.from_dlpack(src)
test = dst.download()
self.assertEqual(ref.dtype, test.dtype)
equal = np.array_equal(ref, test)
if not equal:
print(f"Failed test with dtype {dtype} and {channels} channels")
self.assertTrue(equal)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python
'''
Test for disctrete fourier transform (dft)
'''
# Python 2/3 compatibility
from __future__ import print_function
import cv2 as cv
import numpy as np
import sys
from tests_common import NewOpenCVTests
class dft_test(NewOpenCVTests):
def test_dft(self):
img = self.get_sample('samples/data/rubberwhale1.png', 0)
eps = 0.001
#test direct transform
refDft = np.fft.fft2(img)
refDftShift = np.fft.fftshift(refDft)
refMagnitide = np.log(1.0 + np.abs(refDftShift))
testDft = cv.dft(np.float32(img),flags = cv.DFT_COMPLEX_OUTPUT)
testDftShift = np.fft.fftshift(testDft)
testMagnitude = np.log(1.0 + cv.magnitude(testDftShift[:,:,0], testDftShift[:,:,1]))
refMagnitide = cv.normalize(refMagnitide, 0.0, 1.0, cv.NORM_MINMAX)
testMagnitude = cv.normalize(testMagnitude, 0.0, 1.0, cv.NORM_MINMAX)
self.assertLess(cv.norm(refMagnitide - testMagnitude), eps)
#test inverse transform
img_back = np.fft.ifft2(refDft)
img_back = np.abs(img_back)
img_backTest = cv.idft(testDft)
img_backTest = cv.magnitude(img_backTest[:,:,0], img_backTest[:,:,1])
img_backTest = cv.normalize(img_backTest, 0.0, 1.0, cv.NORM_MINMAX)
img_back = cv.normalize(img_back, 0.0, 1.0, cv.NORM_MINMAX)
self.assertLess(cv.norm(img_back - img_backTest), eps)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python
'''
ECC multiscale alignment test
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import inspect
import math
from tests_common import NewOpenCVTests
class eccms_test(NewOpenCVTests):
def test_eccms(self):
expected_res = np.array([
[1.0225, 0.0606, -28.6452],
[-0.0475, 1.0314, 11.819],
[8.21e-06, -3.65e-07, 1.0]
], dtype=np.float32)
largeGray0 = self.get_sample('cv/shared/halmosh0.jpg', cv.IMREAD_GRAYSCALE)
largeGray1 = self.get_sample('cv/shared/halmosh2.jpg', cv.IMREAD_GRAYSCALE)
roiMask0 = self.get_sample('cv/shared/halmosh0mask.png', cv.IMREAD_GRAYSCALE)
roiMask1 = self.get_sample('cv/shared/halmosh2mask.png', cv.IMREAD_GRAYSCALE)
if largeGray0 is None or largeGray1 is None or roiMask0 is None or roiMask1 is None:
self.assertEqual(0, 1, 'Missing test data')
found = np.eye(3, 3, dtype=np.float32)
n_iters = 23
termination_eps = 1e-6
params = cv.ECCParameters()
params.criteria = (cv.TERM_CRITERIA_COUNT + cv.TERM_CRITERIA_EPS, n_iters, termination_eps)
params.motionType = cv.MOTION_HOMOGRAPHY
params.nlevels = 5
params.itersPerLevel = [5, 10, 300, 300, 1000]
_, found = cv.findTransformECCMultiScale(largeGray0,largeGray1, found, params, roiMask0, roiMask1)
self.assertLess(cv.norm(found - expected_res, cv.NORM_L1), 0.1)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class Features_Tests(NewOpenCVTests):
def test_issue_13406(self):
self.assertEqual(True, hasattr(cv, 'drawKeypoints'))
self.assertEqual(True, hasattr(cv, 'DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS'))
self.assertEqual(True, hasattr(cv, 'DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS'))
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python
"""Algorithm serialization test."""
from __future__ import print_function
import base64
import json
import tempfile
import os
import cv2 as cv
import numpy as np
from tests_common import NewOpenCVTests
class MyData:
def __init__(self):
self.A = 97
self.X = np.pi
self.name = 'mydata1234'
def write(self, fs, name):
fs.startWriteStruct(name, cv.FileNode_MAP|cv.FileNode_FLOW)
fs.write('A', self.A)
fs.write('X', self.X)
fs.write('name', self.name)
fs.endWriteStruct()
def read(self, node):
if (not node.empty()):
self.A = int(node.getNode('A').real())
self.X = node.getNode('X').real()
self.name = node.getNode('name').string()
else:
self.A = self.X = 0
self.name = ''
class filestorage_io_test(NewOpenCVTests):
strings_data = ['image1.jpg', 'Awesomeness', '../data/baboon.jpg']
R0 = np.eye(3,3)
T0 = np.zeros((3,1))
def write_data(self, fname):
fs = cv.FileStorage(fname, cv.FileStorage_WRITE)
R = self.R0
T = self.T0
m = MyData()
fs.write('iterationNr', 100)
fs.startWriteStruct('strings', cv.FileNode_SEQ)
for elem in self.strings_data:
fs.write('', elem)
fs.endWriteStruct()
fs.startWriteStruct('Mapping', cv.FileNode_MAP)
fs.write('One', 1)
fs.write('Two', 2)
fs.endWriteStruct()
fs.write('R_MAT', R)
fs.write('T_MAT', T)
m.write(fs, 'MyData')
fs.release()
def read_data_and_check(self, fname):
fs = cv.FileStorage(fname, cv.FileStorage_READ)
n = fs.getNode('iterationNr')
itNr = int(n.real())
self.assertEqual(itNr, 100)
n = fs.getNode('strings')
self.assertTrue(n.isSeq())
self.assertEqual(n.size(), len(self.strings_data))
for i in range(n.size()):
self.assertEqual(n.at(i).string(), self.strings_data[i])
n = fs.getNode('Mapping')
self.assertEqual(int(n.getNode('Two').real()), 2)
self.assertEqual(int(n.getNode('One').real()), 1)
R = fs.getNode('R_MAT').mat()
T = fs.getNode('T_MAT').mat()
self.assertEqual(cv.norm(R, self.R0, cv.NORM_INF), 0)
self.assertEqual(cv.norm(T, self.T0, cv.NORM_INF), 0)
m0 = MyData()
m = MyData()
m.read(fs.getNode('MyData'))
self.assertEqual(m.A, m0.A)
self.assertEqual(m.X, m0.X)
self.assertEqual(m.name, m0.name)
n = fs.getNode('NonExisting')
self.assertTrue(n.isNone())
fs.release()
def run_fs_test(self, ext):
fd, fname = tempfile.mkstemp(prefix="opencv_python_sample_filestorage", suffix=ext)
os.close(fd)
self.write_data(fname)
self.read_data_and_check(fname)
os.remove(fname)
def test_xml(self):
self.run_fs_test(".xml")
def test_yml(self):
self.run_fs_test(".yml")
def test_json(self):
self.run_fs_test(".json")
def test_base64(self):
fd, fname = tempfile.mkstemp(prefix="opencv_python_sample_filestorage_base64", suffix=".json")
os.close(fd)
np.random.seed(42)
self.write_base64_json(fname)
os.remove(fname)
@staticmethod
def get_normal_2d_mat(dtype):
rows = 10
cols = 20
cn = 3
image = np.zeros((rows, cols, cn), dtype)
if dtype != bool:
image[:] = (1, 2, 127)
else:
image[:] = (False, True, False)
for i in range(rows):
for j in range(cols):
if dtype != bool:
image[i, j, 1] = (i + j) % 256
else:
image[i, j, 1] = (i + j) % 2 != 0
return image
@staticmethod
def get_normal_nd_mat():
shape = (2, 2, 1, 2)
cn = 4
image = np.zeros(shape + (cn,), np.float64)
image[:] = (0.888, 0.111, 0.666, 0.444)
return image
@staticmethod
def get_empty_2d_mat():
shape = (0, 0)
cn = 1
image = np.zeros(shape + (cn,), np.uint8)
return image
@staticmethod
def get_random_mat():
rows = 8
cols = 16
cn = 1
image = np.random.rand(rows, cols, cn)
return image
@staticmethod
def decode(data):
# strip $base64$
encoded = data[8:]
if len(encoded) == 0:
return b''
# strip info about datatype and padding
return base64.b64decode(encoded)[24:]
def write_base64_json(self, fname):
fs = cv.FileStorage(fname, cv.FileStorage_WRITE_BASE64)
mats = {'normal_2d_mat_u8': self.get_normal_2d_mat(np.uint8),
'normal_2d_mat_u32': self.get_normal_2d_mat(np.uint32),
'normal_2d_mat_bool': self.get_normal_2d_mat(bool),
'normal_nd_mat': self.get_normal_nd_mat(),
'empty_2d_mat': self.get_empty_2d_mat(),
'random_mat': self.get_random_mat()}
for name, mat in mats.items():
fs.write(name, mat)
fs.release()
data = {}
with open(fname) as file:
data = json.load(file)
for name, mat in mats.items():
buffer = b''
if mat.size != 0:
if hasattr(mat, 'tobytes'):
buffer = mat.tobytes()
else:
buffer = mat.tostring()
self.assertEqual(buffer, self.decode(data[name]['data']))
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python
'''
Robust line fitting.
==================
Example of using cv.fitLine function for fitting line
to points in presence of outliers.
Switch through different M-estimator functions and see,
how well the robust functions fit the line even
in case of ~50% of outliers.
'''
# Python 2/3 compatibility
from __future__ import print_function
import sys
PY3 = sys.version_info[0] == 3
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
w, h = 512, 256
def toint(p):
return tuple(map(int, p))
def sample_line(p1, p2, n, noise=0.0):
np.random.seed(10)
p1 = np.float32(p1)
t = np.random.rand(n,1)
return p1 + (p2-p1)*t + np.random.normal(size=(n, 2))*noise
dist_func_names = ['DIST_L2', 'DIST_L1', 'DIST_L12', 'DIST_FAIR', 'DIST_WELSCH', 'DIST_HUBER']
class fitline_test(NewOpenCVTests):
def test_fitline(self):
noise = 5
n = 200
r = 5 / 100.0
outn = int(n*r)
p0, p1 = (90, 80), (w-90, h-80)
line_points = sample_line(p0, p1, n-outn, noise)
outliers = np.random.rand(outn, 2) * (w, h)
points = np.vstack([line_points, outliers])
lines = []
for name in dist_func_names:
func = getattr(cv, name)
vx, vy, cx, cy = cv.fitLine(np.float32(points), func, 0, 0.01, 0.01)
line = [vx[0], vy[0], cx[0], cy[0]]
lines.append(line)
eps = 0.05
refVec = (np.float32(p1) - p0) / cv.norm(np.float32(p1) - p0)
for i in range(len(lines)):
self.assertLessEqual(cv.norm(refVec - lines[i][0:2], cv.NORM_L2), eps)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+41
View File
@@ -0,0 +1,41 @@
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import os
import datetime
from tests_common import NewOpenCVTests
class get_cache_dir_test(NewOpenCVTests):
def test_get_cache_dir(self):
#New binding
path = cv.utils.fs.getCacheDirectoryForDownloads()
self.assertTrue(os.path.exists(path))
self.assertTrue(os.path.isdir(path))
def get_cache_dir_imread_interop(self, ext):
path = cv.utils.fs.getCacheDirectoryForDownloads()
gold_image = np.ones((16, 16, 3), np.uint8)
read_from_file = np.zeros((16, 16, 3), np.uint8)
test_file_name = os.path.join(path, "test." + ext)
try:
cv.imwrite(test_file_name, gold_image)
read_from_file = cv.imread(test_file_name)
finally:
os.remove(test_file_name)
self.assertEqual(cv.norm(gold_image, read_from_file), 0)
def test_get_cache_dir_imread_interop_png(self):
self.get_cache_dir_imread_interop("png")
def test_get_cache_dir_imread_interop_jpeg(self):
self.get_cache_dir_imread_interop("jpg")
def test_get_cache_dir_imread_interop_tiff(self):
self.get_cache_dir_imread_interop("tif")
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python
'''
===============================================================================
Interactive Image Segmentation using GrabCut algorithm.
===============================================================================
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import sys
from tests_common import NewOpenCVTests
class grabcut_test(NewOpenCVTests):
def verify(self, mask, exp):
maxDiffRatio = 0.02
expArea = np.count_nonzero(exp)
nonIntersectArea = np.count_nonzero(mask != exp)
curRatio = float(nonIntersectArea) / expArea
return curRatio < maxDiffRatio
def scaleMask(self, mask):
return np.where((mask==cv.GC_FGD) + (mask==cv.GC_PR_FGD),255,0).astype('uint8')
def test_grabcut(self):
img = self.get_sample('cv/shared/airplane.png')
mask_prob = self.get_sample("cv/grabcut/mask_probpy.png", 0)
exp_mask1 = self.get_sample("cv/grabcut/exp_mask1py.png", 0)
exp_mask2 = self.get_sample("cv/grabcut/exp_mask2py.png", 0)
if img is None:
self.assertTrue(False, 'Missing test data')
rect = (24, 126, 459, 168)
mask = np.zeros(img.shape[:2], dtype = np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
cv.grabCut(img, mask, rect, bgdModel, fgdModel, 0, cv.GC_INIT_WITH_RECT)
cv.grabCut(img, mask, rect, bgdModel, fgdModel, 2, cv.GC_EVAL)
if mask_prob is None:
mask_prob = mask.copy()
cv.imwrite(self.extraTestDataPath + '/cv/grabcut/mask_probpy.png', mask_prob)
if exp_mask1 is None:
exp_mask1 = self.scaleMask(mask)
cv.imwrite(self.extraTestDataPath + '/cv/grabcut/exp_mask1py.png', exp_mask1)
self.assertEqual(self.verify(self.scaleMask(mask), exp_mask1), True)
mask = mask_prob
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
cv.grabCut(img, mask, rect, bgdModel, fgdModel, 0, cv.GC_INIT_WITH_MASK)
cv.grabCut(img, mask, rect, bgdModel, fgdModel, 1, cv.GC_EVAL)
if exp_mask2 is None:
exp_mask2 = self.scaleMask(mask)
cv.imwrite(self.extraTestDataPath + '/cv/grabcut/exp_mask2py.png', exp_mask2)
self.assertEqual(self.verify(self.scaleMask(mask), exp_mask2), True)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/python
'''
This example illustrates how to use cv.HoughCircles() function.
'''
# Python 2/3 compatibility
from __future__ import print_function
import cv2 as cv
import numpy as np
import sys
from numpy import pi, sin, cos
from tests_common import NewOpenCVTests
def circleApproximation(circle):
nPoints = 30
dPhi = 2*pi / nPoints
contour = []
for i in range(nPoints):
contour.append(([circle[0] + circle[2]*cos(i*dPhi),
circle[1] + circle[2]*sin(i*dPhi)]))
return np.array(contour).astype(int)
def convContoursIntersectiponRate(c1, c2):
s1 = cv.contourArea(c1)
s2 = cv.contourArea(c2)
s, _ = cv.intersectConvexConvex(c1, c2)
return 2*s/(s1+s2)
class houghcircles_test(NewOpenCVTests):
def test_houghcircles(self):
fn = "samples/data/board.jpg"
src = self.get_sample(fn, 1)
img = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
img = cv.medianBlur(img, 5)
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT, 1, 10, np.array([]), 100, 30, 1, 30)[0]
testCircles = [[38, 181, 17.6],
[99.7, 166, 13.12],
[142.7, 160, 13.52],
[223.6, 110, 8.62],
[79.1, 206.7, 8.62],
[47.5, 351.6, 11.64],
[189.5, 354.4, 11.64],
[189.8, 298.9, 10.64],
[189.5, 252.4, 14.62],
[252.5, 393.4, 15.62],
[602.9, 467.5, 11.42],
[222, 210.4, 9.12],
[263.1, 216.7, 9.12],
[359.8, 222.6, 9.12],
[518.9, 120.9, 9.12],
[413.8, 113.4, 9.12],
[489, 127.2, 9.12],
[448.4, 121.3, 9.12],
[384.6, 128.9, 8.62]]
matches_counter = 0
for i in range(len(testCircles)):
for j in range(len(circles)):
tstCircle = circleApproximation(testCircles[i])
circle = circleApproximation(circles[j])
if convContoursIntersectiponRate(tstCircle, circle) > 0.6:
matches_counter += 1
self.assertGreater(float(matches_counter) / len(testCircles), .5)
self.assertLess(float(len(circles) - matches_counter) / len(circles), .75)
circles_acc = cv.HoughCirclesWithAccumulator(
image=img,
method=cv.HOUGH_GRADIENT,
dp=1,
minDist=10,
circles=np.array([]),
param1=150,
param2=45,
minRadius=1,
maxRadius=30)
self.assertEqual(circles_acc.shape, (1, 2, 4))
self.assertEqual(circles_acc[0, 0, 3], 66.)
self.assertEqual(circles_acc[0, 1, 3], 62.)
def test_houghcircles_alt(self):
fn = "samples/data/board.jpg"
src = self.get_sample(fn, 1)
img = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
img = cv.medianBlur(img, 5)
circles = cv.HoughCircles(img, cv.HOUGH_GRADIENT_ALT, 1, 10, np.array([]), 300, 0.9, 1, 30)
self.assertEqual(circles.shape, (1, 18, 3))
circles = circles[0]
testCircles = [[38, 181, 17.6],
[99.7, 166, 13.12],
[142.7, 160, 13.52],
[223.6, 110, 8.62],
[79.1, 206.7, 8.62],
[47.5, 351.6, 11.64],
[189.5, 354.4, 11.64],
[189.8, 298.9, 10.64],
[189.5, 252.4, 14.62],
[252.5, 393.4, 15.62],
[602.9, 467.5, 11.42],
[222, 210.4, 9.12],
[263.1, 216.7, 9.12],
[359.8, 222.6, 9.12],
[518.9, 120.9, 9.12],
[413.8, 113.4, 9.12],
[489, 127.2, 9.12],
[448.4, 121.3, 9.12],
[384.6, 128.9, 8.62]]
matches_counter = 0
for i in range(len(testCircles)):
for j in range(len(circles)):
tstCircle = circleApproximation(testCircles[i])
circle = circleApproximation(circles[j])
if convContoursIntersectiponRate(tstCircle, circle) > 0.6:
matches_counter += 1
self.assertGreater(float(matches_counter) / len(testCircles), .5)
self.assertLess(float(len(circles) - matches_counter) / len(circles), .75)
circles_acc = cv.HoughCirclesWithAccumulator(
image=img,
method=cv.HOUGH_GRADIENT_ALT,
dp=1,
minDist=10,
circles=np.array([]),
param1=300,
param2=0.9,
minRadius=13,
maxRadius=15)
self.assertEqual(circles_acc.shape, (1, 3, 4))
self.assertEqual(circles_acc[0, 0, 3], 62.)
self.assertEqual(circles_acc[0, 1, 3], 59.)
self.assertEqual(circles_acc[0, 2, 3], 47.)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/python
'''
This example illustrates how to use Hough Transform to find lines
'''
# Python 2/3 compatibility
from __future__ import print_function
import cv2 as cv
import numpy as np
import sys
import math
from tests_common import NewOpenCVTests
def linesDiff(line1, line2):
norm1 = cv.norm(line1 - line2, cv.NORM_L2)
line3 = line1[2:4] + line1[0:2]
norm2 = cv.norm(line3 - line2, cv.NORM_L2)
return min(norm1, norm2)
class houghlines_test(NewOpenCVTests):
def test_houghlines(self):
fn = "/samples/data/pic1.png"
src = self.get_sample(fn)
dst = cv.Canny(src, 50, 200)
lines = cv.HoughLinesP(dst, 1, math.pi/180.0, 40, np.array([]), 50, 10)[:,:]
eps = 5
testLines = [
#rect1
[ 232, 25, 43, 25],
[ 43, 129, 232, 129],
[ 43, 129, 43, 25],
[232, 129, 232, 25],
#rect2
[251, 86, 314, 183],
[252, 86, 323, 40],
[315, 183, 386, 137],
[324, 40, 386, 136],
#triangle
[245, 205, 377, 205],
[244, 206, 305, 278],
[306, 279, 377, 205],
#rect3
[153, 177, 196, 177],
[153, 277, 153, 179],
[153, 277, 196, 277],
[196, 177, 196, 277]]
matches_counter = 0
for i in range(len(testLines)):
for j in range(len(lines)):
if linesDiff(testLines[i], lines[j]) < eps:
matches_counter += 1
self.assertGreater(float(matches_counter) / len(testLines), .7)
lines_acc = cv.HoughLinesWithAccumulator(dst, rho=1, theta=np.pi / 180, threshold=150, srn=0, stn=0)
self.assertEqual(lines_acc[0,2], 192.0)
self.assertEqual(lines_acc[1,2], 187.0)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python
'''
Test for imread
'''
# Python 2/3 compatibility
from __future__ import print_function
import cv2 as cv
import numpy as np
import sys
from tests_common import NewOpenCVTests
class imread_test(NewOpenCVTests):
def test_imread_to_buffer(self):
path = self.extraTestDataPath + '/cv/shared/lena.png'
ref = cv.imread(path)
img = np.zeros_like(ref)
cv.imread(path, img)
self.assertEqual(cv.norm(ref, img, cv.NORM_INF), 0.0)
def test_imread_with_meta(self):
path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.jpg'
img, meta_types, meta_data = cv.imreadWithMetadata(path, flags=cv.IMREAD_ANYCOLOR)
self.assertTrue(img is not None)
self.assertTrue(meta_types is not None)
self.assertTrue(meta_data is not None)
path = self.extraTestDataPath + '/highgui/readwrite/testExifOrientation_1.png'
img, meta_types, meta_data = cv.imreadWithMetadata(path, flags=cv.IMREAD_ANYCOLOR)
self.assertTrue(img is not None)
self.assertTrue(meta_types is not None)
self.assertTrue(meta_data is not None)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python
'''
K-means clusterization test
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
from numpy import random
import sys
PY3 = sys.version_info[0] == 3
if PY3:
xrange = range
from tests_common import NewOpenCVTests
def make_gaussians(cluster_n, img_size):
points = []
ref_distrs = []
sizes = []
for _ in xrange(cluster_n):
mean = (0.1 + 0.8*random.rand(2)) * img_size
a = (random.rand(2, 2)-0.5)*img_size*0.1
cov = np.dot(a.T, a) + img_size*0.05*np.eye(2)
n = 100 + random.randint(900)
pts = random.multivariate_normal(mean, cov, n)
points.append( pts )
ref_distrs.append( (mean, cov) )
sizes.append(n)
points = np.float32( np.vstack(points) )
return points, ref_distrs, sizes
def getMainLabelConfidence(labels, nLabels):
n = len(labels)
labelsDict = dict.fromkeys(range(nLabels), 0)
labelsConfDict = dict.fromkeys(range(nLabels))
for i in range(n):
labelsDict[labels[i][0]] += 1
for i in range(nLabels):
labelsConfDict[i] = float(labelsDict[i]) / n
return max(labelsConfDict.values())
class kmeans_test(NewOpenCVTests):
def test_kmeans(self):
np.random.seed(10)
cluster_n = 5
img_size = 512
points, _, clusterSizes = make_gaussians(cluster_n, img_size)
term_crit = (cv.TERM_CRITERIA_EPS, 30, 0.1)
_ret, labels, centers = cv.kmeans(points, cluster_n, None, term_crit, 10, 0)
self.assertEqual(len(centers), cluster_n)
offset = 0
for i in range(cluster_n):
confidence = getMainLabelConfidence(labels[offset : (offset + clusterSizes[i])], cluster_n)
offset += clusterSizes[i]
self.assertGreater(confidence, 0.9)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class Hackathon244Tests(NewOpenCVTests):
def test_int_array(self):
a = np.array([-1, 2, -3, 4, -5])
absa0 = np.abs(a)
self.assertTrue(cv.norm(a, cv.NORM_L1) == 15)
absa1 = cv.absdiff(a, 0)
self.assertEqual(cv.norm(absa1, absa0, cv.NORM_INF), 0)
def test_imencode(self):
a = np.zeros((480, 640), dtype=np.uint8)
flag, ajpg = cv.imencode("img_q90.jpg", a, [cv.IMWRITE_JPEG_QUALITY, 90])
self.assertEqual(flag, True)
self.assertEqual(ajpg.dtype, np.uint8)
self.assertTrue(isinstance(ajpg, np.ndarray), "imencode returned buffer of wrong type: {}".format(type(ajpg)))
self.assertEqual(len(ajpg.shape), 1, "imencode returned buffer with wrong shape: {}".format(ajpg.shape))
self.assertGreaterEqual(len(ajpg), 1, "imencode length of the returned buffer should be at least 1")
self.assertLessEqual(
len(ajpg), a.size,
"imencode length of the returned buffer shouldn't exceed number of elements in original image"
)
def test_projectPoints(self):
objpt = np.float64([[1,2,3]])
imgpt0, jac0 = cv.projectPoints(objpt, np.zeros(3), np.zeros(3), np.eye(3), np.float64([]))
imgpt1, jac1 = cv.projectPoints(objpt, np.zeros(3), np.zeros(3), np.eye(3), None)
self.assertEqual(imgpt0.shape, (objpt.shape[0], 1, 2))
self.assertEqual(imgpt1.shape, imgpt0.shape)
self.assertEqual(jac0.shape, jac1.shape)
self.assertEqual(jac0.shape[0], 2*objpt.shape[0])
def test_estimateAffine3D(self):
pattern_size = (11, 8)
pattern_points = np.zeros((np.prod(pattern_size), 3), np.float32)
pattern_points[:,:2] = np.indices(pattern_size).T.reshape(-1, 2)
pattern_points *= 10
(retval, out, inliers) = cv.estimateAffine3D(pattern_points, pattern_points)
self.assertEqual(retval, 1)
if cv.norm(out[2,:]) < 1e-3:
out[2,2]=1
self.assertLess(cv.norm(out, np.float64([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0]])), 1e-3)
self.assertEqual(cv.countNonZero(inliers), pattern_size[0]*pattern_size[1])
def test_fast(self):
fd = cv.FastFeatureDetector_create(30, True)
img = self.get_sample("samples/data/right02.jpg", 0)
img = cv.medianBlur(img, 3)
keypoints = fd.detect(img)
self.assertTrue(600 <= len(keypoints) <= 700)
for kpt in keypoints:
self.assertNotEqual(kpt.response, 0)
def check_close_angles(self, a, b, angle_delta):
self.assertTrue(abs(a - b) <= angle_delta or
abs(360 - abs(a - b)) <= angle_delta)
def check_close_pairs(self, a, b, delta):
self.assertLessEqual(abs(a[0] - b[0]), delta)
self.assertLessEqual(abs(a[1] - b[1]), delta)
def check_close_boxes(self, a, b, delta, angle_delta):
self.check_close_pairs(a[0], b[0], delta)
self.check_close_pairs(a[1], b[1], delta)
self.check_close_angles(a[2], b[2], angle_delta)
def test_geometry(self):
npt = 100
np.random.seed(244)
a = np.random.randn(npt,2).astype('float32')*50 + 150
be = cv.fitEllipse(a)
br = cv.minAreaRect(a)
mc, mr = cv.minEnclosingCircle(a)
be0 = ((150.2511749267578, 150.77322387695312), (158.024658203125, 197.57696533203125), 37.57804489135742)
br0 = ((161.2974090576172, 154.41793823242188), (199.2301483154297, 207.7177734375), -9.164555549621582)
mc0, mr0 = (160.41790771484375, 144.55152893066406), 136.713500977
self.check_close_boxes(be, be0, 5, 15)
self.check_close_boxes(br, br0, 5, 15)
self.check_close_pairs(mc, mc0, 5)
self.assertLessEqual(abs(mr - mr0), 5)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
import os
import sys
import unittest
from tests_common import NewOpenCVTests
try:
if sys.version_info[:2] < (3, 0):
raise unittest.SkipTest('Python 2.x is not supported')
class MatTest(NewOpenCVTests):
def test_mat_construct(self):
data = np.random.random([10, 10, 3])
#print(np.ndarray.__dictoffset__) # 0
#print(cv.Mat.__dictoffset__) # 88 (> 0)
#print(cv.Mat) # <class cv2.Mat>
#print(cv.Mat.__base__) # <class 'numpy.ndarray'>
mat_data0 = cv.Mat(data)
assert isinstance(mat_data0, cv.Mat)
assert isinstance(mat_data0, np.ndarray)
self.assertEqual(mat_data0.wrap_channels, False)
res0 = cv.utils.dumpInputArray(mat_data0)
self.assertEqual(res0, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=300 dims(-1)=3 size(-1)=[10 10 3] type(-1)=CV_64FC1")
mat_data1 = cv.Mat(data, wrap_channels=True)
assert isinstance(mat_data1, cv.Mat)
assert isinstance(mat_data1, np.ndarray)
self.assertEqual(mat_data1.wrap_channels, True)
res1 = cv.utils.dumpInputArray(mat_data1)
self.assertEqual(res1, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=100 dims(-1)=2 size(-1)=10x10 type(-1)=CV_64FC3")
mat_data2 = cv.Mat(mat_data1)
assert isinstance(mat_data2, cv.Mat)
assert isinstance(mat_data2, np.ndarray)
self.assertEqual(mat_data2.wrap_channels, True) # fail if __array_finalize__ doesn't work
res2 = cv.utils.dumpInputArray(mat_data2)
self.assertEqual(res2, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=100 dims(-1)=2 size(-1)=10x10 type(-1)=CV_64FC3")
def test_mat_construct_4d(self):
data = np.random.random([5, 10, 10, 3])
mat_data0 = cv.Mat(data)
assert isinstance(mat_data0, cv.Mat)
assert isinstance(mat_data0, np.ndarray)
self.assertEqual(mat_data0.wrap_channels, False)
res0 = cv.utils.dumpInputArray(mat_data0)
self.assertEqual(res0, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=1500 dims(-1)=4 size(-1)=[5 10 10 3] type(-1)=CV_64FC1")
mat_data1 = cv.Mat(data, wrap_channels=True)
assert isinstance(mat_data1, cv.Mat)
assert isinstance(mat_data1, np.ndarray)
self.assertEqual(mat_data1.wrap_channels, True)
res1 = cv.utils.dumpInputArray(mat_data1)
self.assertEqual(res1, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=500 dims(-1)=3 size(-1)=[5 10 10] type(-1)=CV_64FC3")
mat_data2 = cv.Mat(mat_data1)
assert isinstance(mat_data2, cv.Mat)
assert isinstance(mat_data2, np.ndarray)
self.assertEqual(mat_data2.wrap_channels, True) # __array_finalize__ doesn't work
res2 = cv.utils.dumpInputArray(mat_data2)
self.assertEqual(res2, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=500 dims(-1)=3 size(-1)=[5 10 10] type(-1)=CV_64FC3")
def test_mat_wrap_channels_fail(self):
data = np.random.random([2, 3, 4, 520])
mat_data0 = cv.Mat(data)
assert isinstance(mat_data0, cv.Mat)
assert isinstance(mat_data0, np.ndarray)
self.assertEqual(mat_data0.wrap_channels, False)
res0 = cv.utils.dumpInputArray(mat_data0)
self.assertEqual(res0, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=12480 dims(-1)=4 size(-1)=[2 3 4 520] type(-1)=CV_64FC1")
with self.assertRaises(cv.error):
mat_data1 = cv.Mat(data, wrap_channels=True) # argument unable to wrap channels, too high (520 > CV_CN_MAX=512)
res1 = cv.utils.dumpInputArray(mat_data1)
print(mat_data1.__dict__)
print(res1)
def test_mat_wrap_channels_zero(self):
# Passing a 0-channel array must raise cv.error, not segfault.
data = np.zeros((100, 100, 0), dtype=np.uint8)
with self.assertRaises(cv.error):
cv.resize(data, (200, 200)) # channels=0 -> invalid, must not segfault
with self.assertRaises(cv.error):
mat_data = cv.Mat(data, wrap_channels=True) # unable to wrap channels, invalid count (0)
cv.utils.dumpInputArray(mat_data)
# Verify that channels=1 (lower bound) still works correctly
data_1ch = np.zeros((100, 100, 1), dtype=np.uint8)
mat_1ch = cv.Mat(data_1ch, wrap_channels=True)
res = cv.utils.dumpInputArray(mat_1ch)
self.assertIn("CV_8UC1", res)
def test_ufuncs(self):
data = np.arange(10)
mat_data = cv.Mat(data)
mat_data2 = 2 * mat_data
self.assertEqual(type(mat_data2), cv.Mat)
np.testing.assert_equal(2 * data, 2 * mat_data)
def test_comparison(self):
# Undefined behavior, do NOT use that.
# Behavior may be changed in the future
data = np.ones((10, 10, 3))
mat_wrapped = cv.Mat(data, wrap_channels=True)
mat_simple = cv.Mat(data)
np.testing.assert_equal(mat_wrapped, mat_simple) # ???: wrap_channels is not checked for now
np.testing.assert_equal(data, mat_simple)
np.testing.assert_equal(data, mat_wrapped)
#self.assertEqual(mat_wrapped, mat_simple) # ???
#self.assertTrue(mat_wrapped == mat_simple) # ???
#self.assertTrue((mat_wrapped == mat_simple).all())
except unittest.SkipTest as e:
message = str(e)
class TestSkip(unittest.TestCase):
def setUp(self):
self.skipTest('Skip tests: ' + message)
def test_skip():
pass
pass
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+988
View File
@@ -0,0 +1,988 @@
#!/usr/bin/env python
from __future__ import print_function
import sys
import ctypes
from functools import partial
from collections import namedtuple
import sys
if sys.version_info[0] < 3:
from collections import Sequence
else:
from collections.abc import Sequence
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests, unittest
def is_numeric(dtype):
return np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.floating)
def get_limits(dtype):
if not is_numeric(dtype):
return None, None
if np.issubdtype(dtype, np.integer):
info = np.iinfo(dtype)
else:
info = np.finfo(dtype)
return info.min, info.max
def get_conversion_error_msg(value, expected, actual):
return 'Conversion "{}" of type "{}" failed\nExpected: "{}" vs Actual "{}"'.format(
value, type(value).__name__, expected, actual
)
def get_no_exception_msg(value):
return 'Exception is not risen for {} of type {}'.format(value, type(value).__name__)
def rpad(src, dst_size, pad_value=0):
"""Extend `src` up to `dst_size` with given value.
Args:
src (np.ndarray | tuple | list): 1d array like object to pad.
dst_size (_type_): Desired `src` size after padding.
pad_value (int, optional): Padding value. Defaults to 0.
Returns:
np.ndarray: 1d array with len == `dst_size`.
"""
src = np.asarray(src)
if len(src.shape) != 1:
raise ValueError("Only 1d arrays are supported")
# Considering the meaning, it is desirable to use np.pad().
# However, the old numpy doesn't include the following fixes and cannot work as expected.
# So an alternative fix that combines np.append() and np.fill() is used.
# https://docs.scipy.org/doc/numpy-1.13.0/release.html#support-for-returning-arrays-of-arbitrary-dimensions-in-apply-along-axis
return np.append(src, np.full( dst_size - len(src), pad_value, dtype=src.dtype) )
def get_ocv_arithm_op_table(apply_saturation=False):
def saturate(func):
def wrapped_func(x, y):
dst_dtype = x.dtype
if apply_saturation:
if np.issubdtype(x.dtype, np.integer):
x = x.astype(np.int64)
# Apply padding or truncation for array-like `y` inputs
if not isinstance(y, (float, int)):
if len(y) > x.shape[-1]:
y = y[:x.shape[-1]]
else:
y = rpad(y, x.shape[-1], pad_value=0)
dst = func(x, y)
if apply_saturation:
min_val, max_val = get_limits(dst_dtype)
dst = np.clip(dst, min_val, max_val)
return dst.astype(dst_dtype)
return wrapped_func
@saturate
def subtract(x, y):
return x - y
@saturate
def add(x, y):
return x + y
@saturate
def divide(x, y):
if not isinstance(y, (int, float)):
dst_dtype = np.result_type(x, y)
y = np.array(y).astype(dst_dtype)
_, max_value = get_limits(dst_dtype)
y[y == 0] = max_value
# to compatible between python2 and python3, it calicurates with float.
# python2: int / int = int
# python3: int / int = float
dst = 1.0 * x / y
if np.issubdtype(x.dtype, np.integer):
dst = np.rint(dst)
return dst
@saturate
def multiply(x, y):
return x * y
@saturate
def absdiff(x, y):
res = np.abs(x - y)
return res
return {
cv.subtract: subtract,
cv.add: add,
cv.multiply: multiply,
cv.divide: divide,
cv.absdiff: absdiff
}
class Bindings(NewOpenCVTests):
def test_inheritance(self):
bm = cv.StereoBM_create()
bm.getPreFilterCap() # from StereoBM
bm.getBlockSize() # from SteroMatcher
def test_raiseGeneralException(self):
with self.assertRaises((cv.error,),
msg='C++ exception is not propagated to Python in the right way') as cm:
cv.utils.testRaiseGeneralException()
self.assertEqual(str(cm.exception), 'exception text')
def test_redirectError(self):
try:
cv.imshow("", None) # This causes an assert
self.assertEqual("Dead code", 0)
except cv.error as _e:
pass
handler_called = [False]
def test_error_handler(status, func_name, err_msg, file_name, line):
handler_called[0] = True
cv.redirectError(test_error_handler)
try:
cv.imshow("", None) # This causes an assert
self.assertEqual("Dead code", 0)
except cv.error as _e:
self.assertEqual(handler_called[0], True)
pass
cv.redirectError(None)
try:
cv.imshow("", None) # This causes an assert
self.assertEqual("Dead code", 0)
except cv.error as _e:
pass
def test_overload_resolution_can_choose_correct_overload(self):
val = 123
point = (51, 165)
self.assertEqual(cv.utils.testOverloadResolution(val, point),
'overload (int={}, point=(x={}, y={}))'.format(val, *point),
"Can't select first overload if all arguments are provided as positional")
self.assertEqual(cv.utils.testOverloadResolution(val, point=point),
'overload (int={}, point=(x={}, y={}))'.format(val, *point),
"Can't select first overload if one of the arguments are provided as keyword")
self.assertEqual(cv.utils.testOverloadResolution(val),
'overload (int={}, point=(x=42, y=24))'.format(val),
"Can't select first overload if one of the arguments has default value")
rect = (1, 5, 10, 23)
self.assertEqual(cv.utils.testOverloadResolution(rect),
'overload (rect=(x={}, y={}, w={}, h={}))'.format(*rect),
"Can't select second overload if all arguments are provided")
def test_overload_resolution_fails(self):
def test_overload_resolution(msg, *args, **kwargs):
no_exception_msg = 'Overload resolution failed without any exception for: "{}"'.format(msg)
wrong_exception_msg = 'Overload resolution failed with wrong exception type for: "{}"'.format(msg)
with self.assertRaises((cv.error, Exception), msg=no_exception_msg) as cm:
res = cv.utils.testOverloadResolution(*args, **kwargs)
self.fail("Unexpected result for {}: '{}'".format(msg, res))
self.assertEqual(type(cm.exception), cv.error, wrong_exception_msg)
test_overload_resolution('wrong second arg type (keyword arg)', 5, point=(1, 2, 3))
test_overload_resolution('wrong second arg type', 5, 2)
test_overload_resolution('wrong first arg', 3.4, (12, 21))
test_overload_resolution('wrong first arg, no second arg', 4.5)
test_overload_resolution('wrong args number for first overload', 3, (12, 21), 123)
test_overload_resolution('wrong args number for second overload', (3, 12, 12, 1), (12, 21))
# One of the common problems
test_overload_resolution('rect with float coordinates', (4.5, 4, 2, 1))
test_overload_resolution('rect with wrong number of coordinates', (4, 4, 1))
def test_properties_with_reserved_keywords_names_are_transformed(self):
obj = cv.utils.ClassWithKeywordProperties(except_arg=23)
self.assertTrue(hasattr(obj, "lambda_"),
msg="Class doesn't have RW property with converted name")
try:
obj.lambda_ = 32
except Exception as e:
self.fail("Failed to set value to RW property. Error: {}".format(e))
self.assertTrue(hasattr(obj, "except_"),
msg="Class doesn't have readonly property with converted name")
self.assertEqual(obj.except_, 23,
msg="Can't access readonly property value")
with self.assertRaises(AttributeError):
obj.except_ = 32
def test_maketype(self):
data = {
cv.CV_8UC3: [cv.CV_8U, 3, cv.CV_8UC],
cv.CV_16SC1: [cv.CV_16S, 1, cv.CV_16SC],
cv.CV_32FC4: [cv.CV_32F, 4, cv.CV_32FC],
cv.CV_64FC2: [cv.CV_64F, 2, cv.CV_64FC],
cv.CV_8SC4: [cv.CV_8S, 4, cv.CV_8SC],
cv.CV_16UC2: [cv.CV_16U, 2, cv.CV_16UC],
cv.CV_32SC1: [cv.CV_32S, 1, cv.CV_32SC],
cv.CV_16FC3: [cv.CV_16F, 3, cv.CV_16FC],
cv.CV_BoolC1: [cv.CV_Bool, 1, cv.CV_BoolC],
}
for ref, (depth, channels, func) in data.items():
self.assertEqual(ref, cv.CV_MAKETYPE(depth, channels))
self.assertEqual(ref, func(channels))
class Arguments(NewOpenCVTests):
def _try_to_convert(self, conversion, value):
try:
result = conversion(value).lower()
except Exception as e:
self.fail(
'{} "{}" is risen for conversion {} of type {}'.format(
type(e).__name__, e, value, type(value).__name__
)
)
else:
return result
def test_InputArray(self):
res1 = cv.utils.dumpInputArray(None)
# self.assertEqual(res1, "InputArray: noArray()") # not supported
self.assertEqual(res1, "InputArray: empty()=true kind=0x00010000 flags=0x01010000 total(-1)=0 dims(-1)=0 size(-1)=0x0 type(-1)=CV_8UC1")
res2_1 = cv.utils.dumpInputArray((1, 2))
self.assertEqual(res2_1, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=2 dims(-1)=2 size(-1)=1x2 type(-1)=CV_64FC1")
res2_2 = cv.utils.dumpInputArray(1.5) # Scalar(1.5, 1.5, 1.5, 1.5)
self.assertEqual(res2_2, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=4 dims(-1)=2 size(-1)=1x4 type(-1)=CV_64FC1")
a = np.array([[1, 2], [3, 4], [5, 6]])
res3 = cv.utils.dumpInputArray(a) # 32SC1
self.assertEqual(res3, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=6 dims(-1)=2 size(-1)=2x3 type(-1)=CV_32SC1")
a = np.array([[[1, 2], [3, 4], [5, 6]]], dtype='f')
res4 = cv.utils.dumpInputArray(a) # 32FC2
self.assertEqual(res4, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=3 dims(-1)=2 size(-1)=3x1 type(-1)=CV_32FC2")
a = np.array([[[1, 2]], [[3, 4]], [[5, 6]]], dtype=float)
res5 = cv.utils.dumpInputArray(a) # 64FC2
self.assertEqual(res5, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=3 dims(-1)=2 size(-1)=1x3 type(-1)=CV_64FC2")
a = np.zeros((2,3,4), dtype='f')
res6 = cv.utils.dumpInputArray(a)
self.assertEqual(res6, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=6 dims(-1)=2 size(-1)=3x2 type(-1)=CV_32FC4")
a = np.zeros((2,3,4,5), dtype='f')
res7 = cv.utils.dumpInputArray(a)
self.assertEqual(res7, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=120 dims(-1)=4 size(-1)=[2 3 4 5] type(-1)=CV_32FC1")
a = np.array([0, 1, 0, 1], dtype=bool)
res8 = cv.utils.dumpInputArray(a)
self.assertEqual(res8, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=4 dims(-1)=1 size(-1)=4x1 type(-1)=CV_BoolC1")
a = np.array(3.14, dtype=np.float32)
res9 = cv.utils.dumpInputArray(a)
self.assertEqual(res9, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=1 dims(-1)=0 size(-1)=1x1 type(-1)=CV_32FC1")
def test_InputArrayOfArrays(self):
res1 = cv.utils.dumpInputArrayOfArrays(None)
# self.assertEqual(res1, "InputArray: noArray()") # not supported
self.assertEqual(res1, "InputArrayOfArrays: empty()=true kind=0x00050000 flags=0x01050000 total(-1)=0 dims(-1)=1 size(-1)=0x0")
res2_1 = cv.utils.dumpInputArrayOfArrays((1, 2)) # { Scalar:all(1), Scalar::all(2) }
self.assertEqual(res2_1, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_64FC1 dims(0)=2 size(0)=1x4")
res2_2 = cv.utils.dumpInputArrayOfArrays([1.5])
self.assertEqual(res2_2, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=1 dims(-1)=1 size(-1)=1x1 type(0)=CV_64FC1 dims(0)=2 size(0)=1x4")
a = np.array([[1, 2], [3, 4], [5, 6]])
b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
res3 = cv.utils.dumpInputArrayOfArrays([a, b])
self.assertEqual(res3, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_32SC1 dims(0)=2 size(0)=2x3")
c = np.array([[[1, 2], [3, 4], [5, 6]]], dtype='f')
res4 = cv.utils.dumpInputArrayOfArrays([c, a, b])
self.assertEqual(res4, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=3 dims(-1)=1 size(-1)=3x1 type(0)=CV_32FC2 dims(0)=2 size(0)=3x1")
a = np.zeros((2,3,4), dtype='f')
res5 = cv.utils.dumpInputArrayOfArrays([a, b])
self.assertEqual(res5, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_32FC4 dims(0)=2 size(0)=3x2")
# TODO: fix conversion error
#a = np.zeros((2,3,4,5), dtype='f')
#res6 = cv.utils.dumpInputArray([a, b])
#self.assertEqual(res6, "InputArrayOfArrays: empty()=false kind=0x00050000 flags=0x01050000 total(-1)=2 dims(-1)=1 size(-1)=2x1 type(0)=CV_32FC1 dims(0)=4 size(0)=[2 3 4 5]")
def test_unsupported_numpy_data_types_string_description(self):
for dtype in (object, str, np.complex128):
test_array = np.zeros((4, 4, 3), dtype=dtype)
msg = ".*type = {} is not supported".format(test_array.dtype)
if sys.version_info[0] < 3:
self.assertRaisesRegexp(
Exception, msg, cv.utils.dumpInputArray, test_array
)
else:
self.assertRaisesRegex(
Exception, msg, cv.utils.dumpInputArray, test_array
)
def test_numpy_writeable_flag_is_preserved(self):
array = np.zeros((10, 10, 1), dtype=np.uint8)
array.setflags(write=False)
with self.assertRaises(Exception):
cv.rectangle(array, (0, 0), (5, 5), (255), 2)
def test_20968(self):
pixel = np.uint8([[[40, 50, 200]]])
_ = cv.cvtColor(pixel, cv.COLOR_RGB2BGR) # should not raise exception
def test_parse_to_bool_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpBool)
for convertible_true in (True, 1, 64, np.int8(123), np.int16(11), np.int32(2),
np.int64(1), np.bool_(12)):
actual = try_to_convert(convertible_true)
self.assertEqual('bool: true', actual,
msg=get_conversion_error_msg(convertible_true, 'bool: true', actual))
for convertible_false in (False, 0, np.uint8(0), np.bool_(0), np.int_(0)):
actual = try_to_convert(convertible_false)
self.assertEqual('bool: false', actual,
msg=get_conversion_error_msg(convertible_false, 'bool: false', actual))
def test_parse_to_bool_not_convertible(self):
for not_convertible in (1.2, np.float32(2.3), 's', 'str', (1, 2), [1, 2], complex(1, 1),
complex(imag=2), complex(1.1)):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpBool(not_convertible)
def test_parse_to_bool_convertible_extra(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpBool)
_, max_size_t = get_limits(ctypes.c_size_t)
for convertible_true in (-1, max_size_t):
actual = try_to_convert(convertible_true)
self.assertEqual('bool: true', actual,
msg=get_conversion_error_msg(convertible_true, 'bool: true', actual))
def test_parse_to_bool_not_convertible_extra(self):
for not_convertible in (np.array([False]), np.array([True])):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpBool(not_convertible)
def test_parse_to_int_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpInt)
min_int, max_int = get_limits(ctypes.c_int)
for convertible in (-10, -1, 2, int(43.2), np.uint8(15), np.int8(33), np.int16(-13),
np.int32(4), np.int64(345), (23), min_int, max_int, np.int_(33)):
expected = 'int: {0:d}'.format(convertible)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_int_not_convertible(self):
min_int, max_int = get_limits(ctypes.c_int)
for not_convertible in (1.2, float(3), np.float32(4), np.double(45), 's', 'str',
np.array([1, 2]), (1,), [1, 2], min_int - 1, max_int + 1,
complex(1, 1), complex(imag=2), complex(1.1)):
with self.assertRaises((TypeError, OverflowError, ValueError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpInt(not_convertible)
def test_parse_to_int_not_convertible_extra(self):
for not_convertible in (np.bool_(True), True, False, np.float32(2.3),
np.array([3, ], dtype=int), np.array([-2, ], dtype=np.int32),
np.array([11, ], dtype=np.uint8)):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpInt(not_convertible)
def test_parse_to_int64_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpInt64)
min_int64, max_int64 = get_limits(ctypes.c_longlong)
for convertible in (-10, -1, 2, int(43.2), np.uint8(15), np.int8(33), np.int16(-13),
np.int32(4), np.int64(345), (23), min_int64, max_int64, np.int_(33)):
expected = 'int64: {0:d}'.format(convertible)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_int64_not_convertible(self):
min_int64, max_int64 = get_limits(ctypes.c_longlong)
for not_convertible in (1.2, np.float32(4), float(3), np.double(45), 's', 'str',
np.array([1, 2]), (1,), [1, 2], min_int64 - 1, max_int64 + 1,
complex(1, 1), complex(imag=2), complex(1.1), np.bool_(True),
True, False, np.float32(2.3), np.array([3, ], dtype=int),
np.array([-2, ], dtype=np.int32), np.array([11, ], dtype=np.uint8)):
with self.assertRaises((TypeError, OverflowError, ValueError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpInt64(not_convertible)
def test_parse_to_size_t_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpSizeT)
_, max_uint = get_limits(ctypes.c_uint)
for convertible in (2, max_uint, (12), np.uint8(34), np.int8(12), np.int16(23),
np.int32(123), np.int64(344), np.uint64(3), np.uint16(2), np.uint32(5),
np.uint(44)):
expected = 'size_t: {0:d}'.format(convertible).lower()
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_size_t_not_convertible(self):
min_long, _ = get_limits(ctypes.c_long)
for not_convertible in (1.2, True, False, np.bool_(True), np.float32(4), float(3),
np.double(45), 's', 'str', np.array([1, 2]), (1,), [1, 2],
np.float64(6), complex(1, 1), complex(imag=2), complex(1.1),
-1, min_long, np.int8(-35)):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpSizeT(not_convertible)
def test_parse_to_size_t_convertible_extra(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpSizeT)
_, max_size_t = get_limits(ctypes.c_size_t)
for convertible in (max_size_t,):
expected = 'size_t: {0:d}'.format(convertible).lower()
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_size_t_not_convertible_extra(self):
for not_convertible in (np.bool_(True), True, False, np.array([123, ], dtype=np.uint8),):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpSizeT(not_convertible)
def test_parse_to_float_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpFloat)
min_float, max_float = get_limits(ctypes.c_float)
for convertible in (2, -13, 1.24, np.float32(32.45), float(32), np.double(12.23),
np.float32(-12.3), np.float64(3.22), min_float,
max_float, np.inf, -np.inf, float('Inf'), -float('Inf'),
np.double(np.inf), np.double(-np.inf), np.double(float('Inf')),
np.double(-float('Inf'))):
expected = 'Float: {0:.2f}'.format(convertible).lower()
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
# Workaround for Windows NaN tests due to Visual C runtime
# special floating point values (indefinite NaN)
for nan in (float('NaN'), np.nan, np.float32(np.nan), np.double(np.nan),
np.double(float('NaN'))):
actual = try_to_convert(nan)
self.assertIn('nan', actual, msg="Can't convert nan of type {} to float. "
"Actual: {}".format(type(nan).__name__, actual))
min_double, max_double = get_limits(ctypes.c_double)
for inf in (min_float * 10, max_float * 10, min_double, max_double):
expected = 'float: {}inf'.format('-' if inf < 0 else '')
actual = try_to_convert(inf)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(inf, expected, actual))
def test_parse_to_float_not_convertible(self):
for not_convertible in ('s', 'str', (12,), [1, 2], np.array([1, 2], dtype=float),
np.array([1, 2], dtype=np.double), complex(1, 1), complex(imag=2),
complex(1.1)):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpFloat(not_convertible)
def test_parse_to_float_not_convertible_extra(self):
for not_convertible in (np.bool_(False), True, False, np.array([123, ], dtype=int),
np.array([1., ]), np.array([False]),
np.array([True])):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpFloat(not_convertible)
def test_parse_to_double_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpDouble)
min_float, max_float = get_limits(ctypes.c_float)
min_double, max_double = get_limits(ctypes.c_double)
for convertible in (2, -13, 1.24, np.float32(32.45), float(2), np.double(12.23),
np.float32(-12.3), np.float64(3.22), min_float,
max_float, min_double, max_double, np.inf, -np.inf, float('Inf'),
-float('Inf'), np.double(np.inf), np.double(-np.inf),
np.double(float('Inf')), np.double(-float('Inf'))):
expected = 'Double: {0:.2f}'.format(convertible).lower()
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
# Workaround for Windows NaN tests due to Visual C runtime
# special floating point values (indefinite NaN)
for nan in (float('NaN'), np.nan, np.double(np.nan),
np.double(float('NaN'))):
actual = try_to_convert(nan)
self.assertIn('nan', actual, msg="Can't convert nan of type {} to double. "
"Actual: {}".format(type(nan).__name__, actual))
def test_parse_to_double_not_convertible(self):
for not_convertible in ('s', 'str', (12,), [1, 2], np.array([1, 2], dtype=np.float32),
np.array([1, 2], dtype=np.double), complex(1, 1), complex(imag=2),
complex(1.1)):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpDouble(not_convertible)
def test_parse_to_double_not_convertible_extra(self):
for not_convertible in (np.bool_(False), True, False, np.array([123, ], dtype=int),
np.array([1., ]), np.array([False]),
np.array([12.4], dtype=np.double), np.array([True])):
with self.assertRaises((TypeError, OverflowError),
msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpDouble(not_convertible)
def test_parse_to_cstring_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpCString)
for convertible in ('', 's', 'str', str(123), ('char'), np.str_('test2')):
expected = 'string: ' + convertible
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_cstring_not_convertible(self):
for not_convertible in ((12,), ('t', 'e', 's', 't'), np.array(['123', ]),
np.array(['t', 'e', 's', 't']), 1, -1.4, True, False, None):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpCString(not_convertible)
def test_parse_to_string_convertible(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpString)
for convertible in (None, '', 's', 'str', str(123), np.str_('test2')):
expected = 'string: ' + (convertible if convertible else '')
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_string_not_convertible(self):
for not_convertible in ((12,), ('t', 'e', 's', 't'), np.array(['123', ]),
np.array(['t', 'e', 's', 't']), 1, True, False):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpString(not_convertible)
def test_parse_to_rect_convertible(self):
Rect = namedtuple('Rect', ('x', 'y', 'w', 'h'))
try_to_convert = partial(self._try_to_convert, cv.utils.dumpRect)
for convertible in ((1, 2, 4, 5), [5, 3, 10, 20], np.array([10, 20, 23, 10]),
Rect(10, 30, 40, 55), tuple(np.array([40, 20, 24, 20])),
list(np.array([20, 40, 30, 35]))):
expected = 'rect: (x={}, y={}, w={}, h={})'.format(*convertible)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_rect_not_convertible(self):
for not_convertible in (np.empty(shape=(4, 1)), (), [], np.array([]), (12, ),
[3, 4, 5, 10, 123], {1: 2, 3:4, 5:10, 6:30},
'1234', np.array([1, 2, 3, 4], dtype=np.float32),
np.array([[1, 2], [3, 4], [5, 6], [6, 8]]), (1, 2, 5, 1.5)):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpRect(not_convertible)
def test_parse_to_rotated_rect_convertible(self):
RotatedRect = namedtuple('RotatedRect', ('center', 'size', 'angle'))
try_to_convert = partial(self._try_to_convert, cv.utils.dumpRotatedRect)
for convertible in (((2.5, 2.5), (10., 20.), 12.5), [[1.5, 10.5], (12.5, 51.5), 10],
RotatedRect((10, 40), np.array([10.5, 20.5]), 5),
np.array([[10, 6], [50, 50], 5.5], dtype=object)):
center, size, angle = convertible
expected = 'rotated_rect: (c_x={:.6f}, c_y={:.6f}, w={:.6f},' \
' h={:.6f}, a={:.6f})'.format(center[0], center[1],
size[0], size[1], angle)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_wrap_rotated_rect(self):
center = (34.5, 52.)
size = (565.0, 140.0)
angle = -177.5
rect1 = cv.RotatedRect(center, size, angle)
self.assertEqual(rect1.center, center)
self.assertEqual(rect1.size, size)
self.assertEqual(rect1.angle, angle)
pts = [[ 319.7845, -5.6109037],
[ 313.6778, 134.25586],
[-250.78448, 109.6109],
[-244.6778, -30.25586]]
self.assertLess(np.max(np.abs(rect1.points() - pts)), 1e-4)
rect2 = cv.RotatedRect(pts[0], pts[1], pts[2])
_, inter_pts = cv.rotatedRectangleIntersection(rect1, rect2)
self.assertLess(np.max(np.abs(inter_pts.reshape(-1, 2) - pts)), 1e-4)
def test_result_rotated_rect_boundingRect2f(self):
center = (0, 0)
size = (10, 10)
angle = 0
gold_box = (-5.0, -5.0, 10.0, 10.0)
rect1 = cv.RotatedRect(center, size, angle)
bbox = rect1.boundingRect2f()
self.assertEqual(gold_box, bbox)
def test_parse_to_rotated_rect_not_convertible(self):
for not_convertible in ([], (), np.array([]), (123, (45, 34), 1), {1: 2, 3: 4}, 123,
np.array([[123, 123, 14], [1, 3], 56], dtype=object), '123'):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpRotatedRect(not_convertible)
def test_parse_to_term_criteria_convertible(self):
TermCriteria = namedtuple('TermCriteria', ('type', 'max_count', 'epsilon'))
try_to_convert = partial(self._try_to_convert, cv.utils.dumpTermCriteria)
for convertible in ((1, 10, 1e-3), [2, 30, 1e-1], np.array([10, 20, 0.5], dtype=object),
TermCriteria(0, 5, 0.1)):
expected = 'term_criteria: (type={}, max_count={}, epsilon={:.6f}'.format(*convertible)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_term_criteria_not_convertible(self):
for not_convertible in ([], (), np.array([]), [1, 4], (10,), (1.5, 34, 0.1),
{1: 5, 3: 5, 10: 10}, '145'):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpTermCriteria(not_convertible)
def test_parse_to_range_convertible_to_all(self):
try_to_convert = partial(self._try_to_convert, cv.utils.dumpRange)
for convertible in ((), [], np.array([])):
expected = 'range: all'
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_range_convertible(self):
Range = namedtuple('Range', ('start', 'end'))
try_to_convert = partial(self._try_to_convert, cv.utils.dumpRange)
for convertible in ((10, 20), [-1, 3], np.array([10, 24]), Range(-4, 6)):
expected = 'range: (s={}, e={})'.format(*convertible)
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_to_range_not_convertible(self):
for not_convertible in ((1, ), [40, ], np.array([1, 4, 6]), {'a': 1, 'b': 40},
(1.5, 13.5), [3, 6.7], np.array([6.3, 2.1]), '14, 4'):
with self.assertRaises((TypeError), msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpRange(not_convertible)
def test_reserved_keywords_are_transformed(self):
default_lambda_value = 2
default_from_value = 3
format_str = "arg={}, lambda={}, from={}"
self.assertEqual(
cv.utils.testReservedKeywordConversion(20), format_str.format(20, default_lambda_value, default_from_value)
)
self.assertEqual(
cv.utils.testReservedKeywordConversion(10, lambda_=10), format_str.format(10, 10, default_from_value)
)
self.assertEqual(
cv.utils.testReservedKeywordConversion(10, from_=10), format_str.format(10, default_lambda_value, 10)
)
self.assertEqual(
cv.utils.testReservedKeywordConversion(20, lambda_=-4, from_=12), format_str.format(20, -4, 12)
)
def test_parse_vector_int_convertible(self):
np.random.seed(123098765)
try_to_convert = partial(self._try_to_convert, cv.utils.dumpVectorOfInt)
arr = np.random.randint(-20, 20, 40).astype(np.int32).reshape(10, 2, 2)
int_min, int_max = get_limits(ctypes.c_int)
for convertible in ((int_min, 1, 2, 3, int_max), [40, 50], tuple(),
np.array([int_min, -10, 24, int_max], dtype=np.int32),
np.array([10, 230, 12], dtype=np.uint8), arr[:, 0, 1],):
expected = "[" + ", ".join(map(str, convertible)) + "]"
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_vector_int_not_convertible(self):
np.random.seed(123098765)
arr = np.random.randint(-20, 20, 40).astype(np.float32).reshape(10, 2, 2)
int_min, int_max = get_limits(ctypes.c_int)
test_dict = {1: 2, 3: 10, 10: 20}
for not_convertible in ((int_min, 1, 2.5, 3, int_max), [True, 50], 'test', test_dict,
reversed([1, 2, 3]),
np.array([int_min, -10, 24, [1, 2]], dtype=object),
np.array([[1, 2], [3, 4]]), arr[:, 0, 1],):
with self.assertRaises(TypeError, msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpVectorOfInt(not_convertible)
def test_parse_vector_double_convertible(self):
np.random.seed(1230965)
try_to_convert = partial(self._try_to_convert, cv.utils.dumpVectorOfDouble)
arr = np.random.randint(-20, 20, 40).astype(np.int32).reshape(10, 2, 2)
for convertible in ((1, 2.12, 3.5), [40, 50], tuple(),
np.array([-10, 24], dtype=np.int32),
np.array([-12.5, 1.4], dtype=np.double),
np.array([10, 230, 12], dtype=np.float32), arr[:, 0, 1], ):
expected = "[" + ", ".join(map(lambda v: "{:.2f}".format(v), convertible)) + "]"
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_vector_double_not_convertible(self):
test_dict = {1: 2, 3: 10, 10: 20}
for not_convertible in (('t', 'e', 's', 't'), [True, 50.55], 'test', test_dict,
np.array([-10.1, 24.5, [1, 2]], dtype=object),
np.array([[1, 2], [3, 4]]),):
with self.assertRaises(TypeError, msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpVectorOfDouble(not_convertible)
def test_parse_vector_rect_convertible(self):
np.random.seed(1238765)
try_to_convert = partial(self._try_to_convert, cv.utils.dumpVectorOfRect)
arr_of_rect_int32 = np.random.randint(5, 20, 4 * 3).astype(np.int32).reshape(3, 4)
arr_of_rect_cast = np.random.randint(10, 40, 4 * 5).astype(np.uint8).reshape(5, 4)
for convertible in (((1, 2, 3, 4), (10, -20, 30, 10)), arr_of_rect_int32, arr_of_rect_cast,
arr_of_rect_int32.astype(np.int8), [[5, 3, 1, 4]],
((np.int8(4), np.uint8(10), int(32), np.int16(55)),)):
expected = "[" + ", ".join(map(lambda v: "[x={}, y={}, w={}, h={}]".format(*v), convertible)) + "]"
actual = try_to_convert(convertible)
self.assertEqual(expected, actual,
msg=get_conversion_error_msg(convertible, expected, actual))
def test_parse_vector_rect_not_convertible(self):
np.random.seed(1238765)
arr = np.random.randint(5, 20, 4 * 3).astype(np.float32).reshape(3, 4)
for not_convertible in (((1, 2, 3, 4), (10.5, -20, 30.1, 10)), arr,
[[5, 3, 1, 4], []],
((float(4), np.uint8(10), int(32), np.int16(55)),)):
with self.assertRaises(TypeError, msg=get_no_exception_msg(not_convertible)):
_ = cv.utils.dumpVectorOfRect(not_convertible)
def test_vector_general_return(self):
expected_number_of_mats = 5
expected_shape = (10, 10, 3)
expected_type = np.uint8
mats = cv.utils.generateVectorOfMat(5, 10, 10, cv.CV_8UC3)
self.assertTrue(isinstance(mats, tuple),
"Vector of Mats objects should be returned as tuple. Got: {}".format(type(mats)))
self.assertEqual(len(mats), expected_number_of_mats, "Returned array has wrong length")
for mat in mats:
self.assertEqual(mat.shape, expected_shape, "Returned Mat has wrong shape")
self.assertEqual(mat.dtype, expected_type, "Returned Mat has wrong elements type")
empty_mats = cv.utils.generateVectorOfMat(0, 10, 10, cv.CV_32FC1)
self.assertTrue(isinstance(empty_mats, tuple),
"Empty vector should be returned as empty tuple. Got: {}".format(type(mats)))
self.assertEqual(len(empty_mats), 0, "Vector of size 0 should be returned as tuple of length 0")
def test_vector_fast_return(self):
expected_shape = (5, 4)
rects = cv.utils.generateVectorOfRect(expected_shape[0])
self.assertTrue(isinstance(rects, np.ndarray),
"Vector of rectangles should be returned as numpy array. Got: {}".format(type(rects)))
self.assertEqual(rects.dtype, np.int32, "Vector of rectangles has wrong elements type")
self.assertEqual(rects.shape, expected_shape, "Vector of rectangles has wrong shape")
empty_rects = cv.utils.generateVectorOfRect(0)
self.assertTrue(isinstance(empty_rects, tuple),
"Empty vector should be returned as empty tuple. Got: {}".format(type(empty_rects)))
self.assertEqual(len(empty_rects), 0, "Vector of size 0 should be returned as tuple of length 0")
expected_shape = (10,)
ints = cv.utils.generateVectorOfInt(expected_shape[0])
self.assertTrue(isinstance(ints, np.ndarray),
"Vector of integers should be returned as numpy array. Got: {}".format(type(ints)))
self.assertEqual(ints.dtype, np.int32, "Vector of integers has wrong elements type")
self.assertEqual(ints.shape, expected_shape, "Vector of integers has wrong shape.")
def test_result_rotated_rect_issue_20930(self):
rr = cv.utils.testRotatedRect(10, 20, 100, 200, 45)
self.assertTrue(isinstance(rr, tuple), msg=type(rr))
self.assertEqual(len(rr), 3)
rrv = cv.utils.testRotatedRectVector(10, 20, 100, 200, 45)
self.assertTrue(isinstance(rrv, tuple), msg=type(rrv))
self.assertEqual(len(rrv), 10)
rr = rrv[0]
self.assertTrue(isinstance(rr, tuple), msg=type(rrv))
self.assertEqual(len(rr), 3)
def test_nested_function_availability(self):
self.assertTrue(hasattr(cv.utils, "nested"),
msg="Module is not generated for nested namespace")
self.assertTrue(hasattr(cv.utils.nested, "testEchoBooleanFunction"),
msg="Function in nested module is not available")
if sys.version_info[0] < 3:
# Nested submodule is managed only by the global submodules dictionary
# and parent native module
expected_ref_count = 2
else:
# Nested submodule is managed by the global submodules dictionary,
# parent native module and Python part of the submodule
expected_ref_count = 3
# `getrefcount` temporary increases reference counter by 1
actual_ref_count = sys.getrefcount(cv.utils.nested) - 1
self.assertEqual(actual_ref_count, expected_ref_count,
msg="Nested submodule reference counter has wrong value\n"
"Expected: {}. Actual: {}".format(expected_ref_count, actual_ref_count))
for flag in (True, False):
self.assertEqual(flag, cv.utils.nested.testEchoBooleanFunction(flag),
msg="Function in nested module returns wrong result")
def test_inner_class_has_global_alias(self):
self.assertTrue(hasattr(cv.SimpleBlobDetector, "Params"),
msg="Class is not registered as inner class")
self.assertTrue(hasattr(cv, "SimpleBlobDetector_Params"),
msg="Inner class doesn't have alias in the global module")
self.assertEqual(cv.SimpleBlobDetector.Params, cv.SimpleBlobDetector_Params,
msg="Inner class and class in global module don't refer "
"to the same type")
def test_export_class_with_different_name(self):
self.assertTrue(hasattr(cv.utils.nested, "ExportClassName"),
msg="Class with export alias is not registered in the submodule")
self.assertTrue(hasattr(cv, "utils_nested_ExportClassName"),
msg="Class with export alias doesn't have alias in the "
"global module")
self.assertEqual(cv.utils.nested.ExportClassName.originalName(), "OriginalClassName")
instance = cv.utils.nested.ExportClassName.create()
self.assertTrue(isinstance(instance, cv.utils.nested.ExportClassName),
msg="Factory function returns wrong class instance: {}".format(type(instance)))
self.assertTrue(hasattr(cv.utils.nested, "ExportClassName_create"),
msg="Factory function should have alias in the same module as the class")
# self.assertFalse(hasattr(cv.utils.nested, "OriginalClassName_create"),
# msg="Factory function should not be registered with original class name, "\
# "when class has different export name")
def test_export_inner_class_of_class_exported_with_different_name(self):
if not hasattr(cv.utils.nested, "ExportClassName"):
raise unittest.SkipTest(
"Outer class with export alias is not registered in the submodule")
self.assertTrue(hasattr(cv.utils.nested.ExportClassName, "Params"),
msg="Inner class with export alias is not registered in "
"the outer class")
self.assertTrue(hasattr(cv, "utils_nested_ExportClassName_Params"),
msg="Inner class with export alias is not registered in "
"global module")
params = cv.utils.nested.ExportClassName.Params()
params.int_value = 45
params.float_value = 4.5
instance = cv.utils.nested.ExportClassName.create(params)
self.assertTrue(isinstance(instance, cv.utils.nested.ExportClassName),
msg="Factory function returns wrong class instance: {}".format(type(instance)))
self.assertEqual(
params.int_value, instance.getIntParam(),
msg="Class initialized with wrong integer parameter. Expected: {}. Actual: {}".format(
params.int_value, instance.getIntParam()
)
)
self.assertEqual(
params.float_value, instance.getFloatParam(),
msg="Class initialized with wrong integer parameter. Expected: {}. Actual: {}".format(
params.float_value, instance.getFloatParam()
)
)
def test_named_arguments_without_parameters(self):
src = np.ones((5, 5, 3), dtype=np.uint8)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(src)
np.testing.assert_equal(src, src_copy)
self.assertEqual(arguments_dump, 'lambda=-1, sigma=0.0')
def test_named_arguments_without_output_argument(self):
src = np.zeros((2, 2, 3), dtype=np.uint8)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(
src, lambda_=15, sigma=3.5
)
np.testing.assert_equal(src, src_copy)
self.assertEqual(arguments_dump, 'lambda=15, sigma=3.5')
def test_named_arguments_with_output_argument(self):
src = np.zeros((3, 3, 3), dtype=np.uint8)
dst = np.ones_like(src)
arguments_dump, src_copy = cv.utils.copyMatAndDumpNamedArguments(
src, dst, lambda_=25, sigma=5.5
)
np.testing.assert_equal(src, src_copy)
np.testing.assert_equal(dst, src_copy)
self.assertEqual(arguments_dump, 'lambda=25, sigma=5.5')
def test_arithm_op_without_saturation(self):
np.random.seed(4231568)
src = np.random.randint(20, 40, 8 * 4 * 3).astype(np.uint8).reshape(8, 4, 3)
operations = get_ocv_arithm_op_table(apply_saturation=False)
for ocv_op, numpy_op in operations.items():
for val in (2, 4, (5, ), (6, 4), (2., 4., 1.),
np.uint8([1, 2, 2]), np.float64([5, 2, 6, 3]),):
dst = ocv_op(src, val)
expected = numpy_op(src, val)
# Temporarily allows a difference of 1 for arm64 workaround.
self.assertLess(np.max(np.abs(dst - expected)), 2,
msg="Operation '{}' is failed for {}".format(ocv_op.__name__, val ) )
def test_arithm_op_with_saturation(self):
np.random.seed(4231568)
src = np.random.randint(20, 40, 4 * 8 * 4).astype(np.uint8).reshape(4, 8, 4)
operations = get_ocv_arithm_op_table(apply_saturation=True)
for ocv_op, numpy_op in operations.items():
for val in (10, 4, (40, ), (15, 12), (25., 41., 15.),
np.uint8([1, 2, 20]), np.float64([50, 21, 64, 30]),):
dst = ocv_op(src, val)
expected = numpy_op(src, val)
# Temporarily allows a difference of 1 for arm64 workaround.
self.assertLess(np.max(np.abs(dst - expected)), 2,
msg="Saturated Operation '{}' is failed for {}".format(ocv_op.__name__, val ) )
class CanUsePurePythonModuleFunction(NewOpenCVTests):
def test_can_get_ocv_version(self):
import sys
if sys.version_info[0] < 3:
raise unittest.SkipTest('Python 2.x is not supported')
self.assertEqual(cv.misc.get_ocv_version(), cv.__version__,
"Can't get package version using Python misc module")
def test_native_method_can_be_patched(self):
import sys
if sys.version_info[0] < 3:
raise unittest.SkipTest('Python 2.x is not supported')
res = cv.utils.testOverwriteNativeMethod(10)
self.assertTrue(isinstance(res, Sequence),
msg="Overwritten method should return sequence. "
"Got: {} of type {}".format(res, type(res)))
self.assertSequenceEqual(res, (11, 10),
msg="Failed to overwrite native method")
res = cv.utils._native.testOverwriteNativeMethod(123)
self.assertEqual(res, 123, msg="Failed to call native method implementation")
def test_default_matx_argument(self):
res = cv.utils.dumpVec2i()
self.assertEqual(res, "Vec2i(42, 24)",
msg="Default argument is not properly handled")
res = cv.utils.dumpVec2i((12, 21))
self.assertEqual(res, "Vec2i(12, 21)")
class SamplesFindFile(NewOpenCVTests):
def test_ExistedFile(self):
res = cv.samples.findFile('HappyFish.jpg', False)
self.assertNotEqual(res, '')
def test_MissingFile(self):
res = cv.samples.findFile('non_existed.file', False)
self.assertEqual(res, '')
def test_MissingFileException(self):
try:
_res = cv.samples.findFile('non_existed.file', True)
self.assertEqual("Dead code", 0)
except cv.error as _e:
pass
class AlgorithmImplHit(NewOpenCVTests):
def test_callable(self):
res = cv.getDefaultAlgorithmHint()
self.assertTrue(res is not None)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python
'''
Morphology operations.
'''
# Python 2/3 compatibility
from __future__ import print_function
import sys
PY3 = sys.version_info[0] == 3
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class morphology_test(NewOpenCVTests):
def test_morphology(self):
fn = 'samples/data/rubberwhale1.png'
img = self.get_sample(fn)
modes = ['erode/dilate', 'open/close', 'blackhat/tophat', 'gradient']
str_modes = ['ellipse', 'rect', 'cross']
referenceHashes = { modes[0]: '071a526425b79e45b4d0d71ef51b0562', modes[1] : '071a526425b79e45b4d0d71ef51b0562',
modes[2] : '427e89f581b7df1b60a831b1ed4c8618', modes[3] : '0dd8ad251088a63d0dd022bcdc57361c'}
def update(cur_mode):
cur_str_mode = str_modes[0]
sz = 10
iters = 1
opers = cur_mode.split('/')
if len(opers) > 1:
sz = sz - 10
op = opers[sz > 0]
sz = abs(sz)
else:
op = opers[0]
sz = sz*2+1
str_name = 'MORPH_' + cur_str_mode.upper()
oper_name = 'MORPH_' + op.upper()
st = cv.getStructuringElement(getattr(cv, str_name), (sz, sz))
return cv.morphologyEx(img, getattr(cv, oper_name), st, iterations=iters)
for mode in modes:
res = update(mode)
self.assertEqual(self.hashimg(res), referenceHashes[mode])
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python
'''
MSER detector test
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import random
from tests_common import NewOpenCVTests
class mser_test(NewOpenCVTests):
def test_mser(self):
img = self.get_sample('cv/mser/puzzle.png', 0)
smallImg = [
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
[255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
]
kDelta = 5
mserExtractor = cv.MSER_create()
mserExtractor.setDelta(kDelta)
mserExtractor.setMinDiversity(0)
random.seed(10)
for _i in range(100):
use_big_image = random.choice([True, False])
invert = random.choice([True, False])
binarize = random.choice([True, False]) if use_big_image else False
blur = random.choice([True, False])
thresh = random.choice([0, 70, 120, 180, 255])
src0 = img if use_big_image else np.array(smallImg).astype('uint8')
src = src0.copy()
kMinArea = 256 if use_big_image else 10
kMaxArea = int(src.shape[0]*src.shape[1]/4)
mserExtractor.setMinArea(kMinArea)
mserExtractor.setMaxArea(kMaxArea)
if invert:
cv.bitwise_not(src, src)
if binarize:
_, src = cv.threshold(src, thresh, 255, cv.THRESH_BINARY)
if blur:
src = cv.GaussianBlur(src, (5, 5), 1.5, 1.5)
minRegs = 7 if use_big_image else 2
maxRegs = 1000 if use_big_image else 20
if binarize and (thresh == 0 or thresh == 255):
minRegs = maxRegs = 0
msers, boxes = mserExtractor.detectRegions(src)
nmsers = len(msers)
self.assertEqual(nmsers, len(boxes))
self.assertLessEqual(minRegs, nmsers)
self.assertGreaterEqual(maxRegs, nmsers)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python
from itertools import product
from functools import reduce
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
def norm_inf(x, y=None):
def norm(vec):
return np.linalg.norm(vec.flatten(), np.inf)
x = x.astype(np.float64)
return norm(x) if y is None else norm(x - y.astype(np.float64))
def norm_l1(x, y=None):
def norm(vec):
return np.linalg.norm(vec.flatten(), 1)
x = x.astype(np.float64)
return norm(x) if y is None else norm(x - y.astype(np.float64))
def norm_l2(x, y=None):
def norm(vec):
return np.linalg.norm(vec.flatten())
x = x.astype(np.float64)
return norm(x) if y is None else norm(x - y.astype(np.float64))
def norm_l2sqr(x, y=None):
def norm(vec):
return np.square(vec).sum()
x = x.astype(np.float64)
return norm(x) if y is None else norm(x - y.astype(np.float64))
def norm_hamming(x, y=None):
def norm(vec):
return sum(bin(i).count('1') for i in vec.flatten())
return norm(x) if y is None else norm(np.bitwise_xor(x, y))
def norm_hamming2(x, y=None):
def norm(vec):
def element_norm(element):
binary_str = bin(element).split('b')[-1]
if len(binary_str) % 2 == 1:
binary_str = '0' + binary_str
gen = filter(lambda p: p != '00',
(binary_str[i:i+2]
for i in range(0, len(binary_str), 2)))
return sum(1 for _ in gen)
return sum(element_norm(element) for element in vec.flatten())
return norm(x) if y is None else norm(np.bitwise_xor(x, y))
norm_type_under_test = {
cv.NORM_INF: norm_inf,
cv.NORM_L1: norm_l1,
cv.NORM_L2: norm_l2,
cv.NORM_L2SQR: norm_l2sqr,
cv.NORM_HAMMING: norm_hamming,
cv.NORM_HAMMING2: norm_hamming2
}
norm_name = {
cv.NORM_INF: 'inf',
cv.NORM_L1: 'L1',
cv.NORM_L2: 'L2',
cv.NORM_L2SQR: 'L2SQR',
cv.NORM_HAMMING: 'Hamming',
cv.NORM_HAMMING2: 'Hamming2'
}
def get_element_types(norm_type):
if norm_type in (cv.NORM_HAMMING, cv.NORM_HAMMING2):
return (np.uint8,)
else:
return (np.uint8, np.int8, np.uint16, np.int16, np.int32, np.float32,
np.float64, np.float16)
def generate_vector(shape, dtype):
if np.issubdtype(dtype, np.integer):
return np.random.randint(0, 100, shape).astype(dtype)
else:
return np.random.normal(10., 12.5, shape).astype(dtype)
shapes = (1, 2, 3, 5, 7, 16, (1, 1), (2, 2), (3, 5), (1, 7))
class norm_test(NewOpenCVTests):
def test_norm_for_one_array(self):
np.random.seed(123)
for norm_type, norm in norm_type_under_test.items():
element_types = get_element_types(norm_type)
for shape, element_type in product(shapes, element_types):
array = generate_vector(shape, element_type)
expected = norm(array)
actual = cv.norm(array, norm_type)
self.assertAlmostEqual(
expected, actual, places=2,
msg='Array {0} of {1} and norm {2}'.format(
array, element_type.__name__, norm_name[norm_type]
)
)
def test_norm_for_two_arrays(self):
np.random.seed(456)
for norm_type, norm in norm_type_under_test.items():
element_types = get_element_types(norm_type)
for shape, element_type in product(shapes, element_types):
first = generate_vector(shape, element_type)
second = generate_vector(shape, element_type)
expected = norm(first, second)
actual = cv.norm(first, second, norm_type)
self.assertAlmostEqual(
expected, actual, places=2,
msg='Arrays {0} {1} of type {2} and norm {3}'.format(
first, second, element_type.__name__,
norm_name[norm_type]
)
)
def test_norm_fails_for_wrong_type(self):
for norm_type in (cv.NORM_HAMMING, cv.NORM_HAMMING2):
with self.assertRaises(Exception,
msg='Type is not checked {0}'.format(
norm_name[norm_type]
)):
cv.norm(np.array([1, 2], dtype=np.int32), norm_type)
def test_norm_fails_for_array_and_scalar(self):
for norm_type in norm_type_under_test:
with self.assertRaises(Exception,
msg='Exception is not thrown for {0}'.format(
norm_name[norm_type]
)):
cv.norm(np.array([1, 2], dtype=np.uint8), 123, norm_type)
def test_norm_fails_for_scalar_and_array(self):
for norm_type in norm_type_under_test:
with self.assertRaises(Exception,
msg='Exception is not thrown for {0}'.format(
norm_name[norm_type]
)):
cv.norm(4, np.array([1, 2], dtype=np.uint8), norm_type)
def test_norm_fails_for_array_and_norm_type_as_scalar(self):
for norm_type in norm_type_under_test:
with self.assertRaises(Exception,
msg='Exception is not thrown for {0}'.format(
norm_name[norm_type]
)):
cv.norm(np.array([3, 4, 5], dtype=np.uint8),
norm_type, normType=norm_type)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+38
View File
@@ -0,0 +1,38 @@
from tests_common import NewOpenCVTests, unittest
import cv2 as cv
import os
def import_path():
import sys
if sys.version_info[0] < 3 or sys.version_info[1] < 6:
raise unittest.SkipTest('Python 3.6+ required')
from pathlib import Path
return Path
class CanPassPathLike(NewOpenCVTests):
def test_pathlib_path(self):
Path = import_path()
img_path = self.find_file('cv/imgproc/stuff.jpg', [os.environ.get('OPENCV_TEST_DATA_PATH')])
image_from_str = cv.imread(img_path)
self.assertIsNotNone(image_from_str)
image_from_path = cv.imread(Path(img_path))
self.assertIsNotNone(image_from_path)
def test_type_mismatch(self):
import_path() # checks python version
with self.assertRaises(cv.error) as context:
cv.imread(123)
self.assertTrue('str or path-like' in str(context.exception))
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python
""""Core serialization tests."""
import tempfile
import os
import cv2 as cv
import numpy as np
from tests_common import NewOpenCVTests, unittest
class persistence_test(NewOpenCVTests):
def test_yml_rw(self):
fd, fname = tempfile.mkstemp(prefix="opencv_python_persistence_", suffix=".yml")
os.close(fd)
# Writing ...
expected = np.array([[[0, 1, 2, 3, 4]]])
expected_str = ("Hello", "World", "!")
fs = cv.FileStorage(fname, cv.FILE_STORAGE_WRITE)
fs.write("test", expected)
fs.write("strings", expected_str)
fs.release()
# Reading ...
fs = cv.FileStorage(fname, cv.FILE_STORAGE_READ)
root = fs.getFirstTopLevelNode()
self.assertEqual(root.name(), "test")
test = fs.getNode("test")
self.assertEqual(test.empty(), False)
self.assertEqual(test.name(), "test")
self.assertEqual(test.type(), cv.FILE_NODE_MAP)
self.assertEqual(test.isMap(), True)
actual = test.mat()
self.assertEqual(actual.shape, expected.shape)
self.assertEqual(np.array_equal(expected, actual), True)
strings = fs.getNode("strings")
self.assertEqual(strings.isSeq(), True)
self.assertEqual(strings.size(), len(expected_str))
self.assertEqual(all(strings.at(i).isString() for i in range(strings.size())), True)
self.assertSequenceEqual([strings.at(i).string() for i in range(strings.size())], expected_str)
fs.release()
os.remove(fname)
def test_yml_python_interop(self):
try:
import yaml
except:
raise unittest.SkipTest('Pyyml is not available for interop test')
ref_data = {
'int_value': 42,
"bool_value": True,
"float_value": 3.1415926,
"int64_value": 2147483647 + 1024, # C++ INT_MAX + 1024
"string_value": "opencv"
}
fd, test_file_name = tempfile.mkstemp(prefix="opencv_python_persistence_", suffix=".yml")
os.close(fd)
with open(test_file_name, 'w') as ff:
yaml.dump(ref_data, ff)
# Notice: no cv.FileStorage_FORMAT_YAML flag needed now thanks to the C++ fix!
fs = cv.FileStorage(test_file_name, cv.FILE_STORAGE_READ)
self.assertTrue(fs.isOpened())
node = fs.getNode('int_value')
self.assertTrue(node.isInt())
self.assertEqual(42, int(node.real()))
node = fs.getNode('int64_value')
self.assertTrue(node.isInt())
self.assertEqual(2147483647 + 1024, int(node.real()))
node = fs.getNode('float_value')
self.assertTrue(node.isReal())
self.assertEqual(3.1415926, node.real())
node = fs.getNode('string_value')
self.assertTrue(node.isString())
self.assertEqual("opencv", node.string())
fs.release()
os.remove(test_file_name)
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python
'''
Simple "Square Detector" program.
Loads several images sequentially and tries to find squares in each image.
'''
# Python 2/3 compatibility
import sys
PY3 = sys.version_info[0] == 3
if PY3:
xrange = range
import numpy as np
import cv2 as cv
def angle_cos(p0, p1, p2):
d1, d2 = (p0-p1).astype('float'), (p2-p1).astype('float')
return abs( np.dot(d1, d2) / np.sqrt( np.dot(d1, d1)*np.dot(d2, d2) ) )
def find_squares(img):
img = cv.GaussianBlur(img, (5, 5), 0)
squares = []
for gray in cv.split(img):
for thrs in xrange(0, 255, 26):
if thrs == 0:
bin = cv.Canny(gray, 0, 50, apertureSize=5)
bin = cv.dilate(bin, None)
else:
_retval, bin = cv.threshold(gray, thrs, 255, cv.THRESH_BINARY)
contours, _hierarchy = cv.findContours(bin, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
for cnt in contours:
cnt_len = cv.arcLength(cnt, True)
cnt = cv.approxPolyDP(cnt, 0.02*cnt_len, True)
if len(cnt) == 4 and cv.contourArea(cnt) > 1000 and cv.isContourConvex(cnt):
cnt = cnt.reshape(-1, 2)
max_cos = np.max([angle_cos( cnt[i], cnt[(i+1) % 4], cnt[(i+2) % 4] ) for i in xrange(4)])
if max_cos < 0.1 and filterSquares(squares, cnt):
squares.append(cnt)
return squares
def intersectionRate(s1, s2):
area, _intersection = cv.intersectConvexConvex(np.array(s1), np.array(s2))
return 2 * area / (cv.contourArea(np.array(s1)) + cv.contourArea(np.array(s2)))
def filterSquares(squares, square):
for i in range(len(squares)):
if intersectionRate(squares[i], square) > 0.95:
return False
return True
from tests_common import NewOpenCVTests
class squares_test(NewOpenCVTests):
def test_squares(self):
img = self.get_sample('samples/data/pic1.png')
squares = find_squares(img)
testSquares = [
[[43, 25],
[43, 129],
[232, 129],
[232, 25]],
[[252, 87],
[324, 40],
[387, 137],
[315, 184]],
[[154, 178],
[196, 180],
[198, 278],
[154, 278]],
[[0, 0],
[400, 0],
[400, 300],
[0, 300]]
]
matches_counter = 0
for i in range(len(squares)):
for j in range(len(testSquares)):
if intersectionRate(squares[i], testSquares[j]) > 0.9:
matches_counter += 1
self.assertGreater(matches_counter / len(testSquares), 0.9)
self.assertLess( (len(squares) - matches_counter) / len(squares), 0.2)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python
'''
Texture flow direction estimation.
Sample shows how cv.cornerEigenValsAndVecs function can be used
to estimate image texture flow direction.
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
import sys
from tests_common import NewOpenCVTests
class texture_flow_test(NewOpenCVTests):
def test_texture_flow(self):
img = self.get_sample('samples/data/chessboard.png')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
h, w = img.shape[:2]
eigen = cv.cornerEigenValsAndVecs(gray, 5, 3)
eigen = eigen.reshape(h, w, 3, 2) # [[e1, e2], v1, v2]
flow = eigen[:,:,2]
d = 300
eps = d / 30
points = np.dstack( np.mgrid[d/2:w:d, d/2:h:d] ).reshape(-1, 2)
textureVectors = []
for x, y in np.int32(points):
textureVectors.append(np.int32(flow[y, x]*d))
for i in range(len(textureVectors)):
self.assertTrue(cv.norm(textureVectors[i], cv.NORM_L2) < eps
or abs(cv.norm(textureVectors[i], cv.NORM_L2) - d) < eps)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python
'''
Test for Tokenizer Python bindings
'''
from __future__ import print_function
import cv2 as cv
import os
import json
from tests_common import NewOpenCVTests
def _tf(filename=""):
base = os.environ.get("OPENCV_TEST_DATA_PATH") or os.getcwd()
path = os.path.join(base, "dnn", "llm", filename)
if not os.path.exists(path):
raise FileNotFoundError(
f"Missing test data: {path}. "
"Set OPENCV_TEST_DATA_PATH to the testdata root contains dnn/llm."
)
return path
class TokenizerBindingTest(NewOpenCVTests):
def test_tokenizer_binding(self):
try:
tokenizer = cv.dnn.Tokenizer
print("Tokenizer binding is available.", tokenizer)
gpt2_model = _tf("gpt2/config.json")
tokenizer = cv.dnn.Tokenizer.load(gpt2_model)
print("Tokenizer loaded from:", gpt2_model)
except AttributeError:
self.fail("Tokenizer binding is NOT available.")
def test_tokenizer_gpt2(self):
tok = cv.dnn.Tokenizer.load((_tf("gpt2/config.json")))
ids = tok.encode("hello world")
print(ids)
txt = tok.decode(ids)
self.assertEqual(txt, "hello world")
def test_tokenizer_gpt4(self):
tok = cv.dnn.Tokenizer.load(_tf("gpt4/config.json"))
tokens = tok.encode("hello world")
# expects {15339, 1917}
self.assertEqual(list(tokens), [15339, 1917])
sent = tok.decode([15339, 1917])
self.assertEqual(sent, "hello world")
def test_with_hf_tiktoken(self):
tok = cv.dnn.Tokenizer.load(_tf("gpt2/config.json"))
with open(_tf("gpt2/gpt2_hf_tik_testdata.json"), "r", encoding="utf-8") as f:
golden = json.load(f)
for s in golden["samples"]:
text = s["text"]
expected = s["ids"]
got = tok.encode(text).tolist()
self.assertEqual(
got, expected,
msg=f"Mismatch for sample '{s['name']}'"
)
self.assertEqual(tok.decode(expected), text)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
import os
from tests_common import NewOpenCVTests
def load_exposure_seq(path):
images = []
times = []
with open(os.path.join(path, 'list.txt'), 'r') as list_file:
for line in list_file.readlines():
name, time = line.split()
images.append(cv.imread(os.path.join(path, name)))
times.append(1. / float(time))
return images, times
class UMat(NewOpenCVTests):
def test_umat_construct(self):
data = np.random.random([512, 512])
# UMat constructors
data_um = cv.UMat(data) # from ndarray
data_sub_um = cv.UMat(data_um, (128, 256), (128, 256)) # from UMat
data_dst_um = cv.UMat(128, 128, cv.CV_64F) # from size/type
# test continuous and submatrix flags
assert data_um.isContinuous() and not data_um.isSubmatrix()
assert not data_sub_um.isContinuous() and data_sub_um.isSubmatrix()
# test operation on submatrix
cv.multiply(data_sub_um, 2., dst=data_dst_um)
assert np.allclose(2. * data[128:256, 128:256], data_dst_um.get())
def test_umat_handle(self):
a_um = cv.UMat(256, 256, cv.CV_32F)
_ctx_handle = cv.UMat.context() # obtain context handle
_queue_handle = cv.UMat.queue() # obtain queue handle
_a_handle = a_um.handle(cv.ACCESS_READ) # obtain buffer handle
_offset = a_um.offset # obtain buffer offset
def test_umat_matching(self):
img1 = self.get_sample("samples/data/right01.jpg")
img2 = self.get_sample("samples/data/right02.jpg")
orb = cv.ORB_create()
img1, img2 = cv.UMat(img1), cv.UMat(img2)
ps1, descs_umat1 = orb.detectAndCompute(img1, None)
ps2, descs_umat2 = orb.detectAndCompute(img2, None)
self.assertIsInstance(descs_umat1, cv.UMat)
self.assertIsInstance(descs_umat2, cv.UMat)
self.assertGreater(len(ps1), 0)
self.assertGreater(len(ps2), 0)
bf = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True)
res_umats = bf.match(descs_umat1, descs_umat2)
res = bf.match(descs_umat1.get(), descs_umat2.get())
self.assertGreater(len(res), 0)
self.assertEqual(len(res_umats), len(res))
def test_umat_optical_flow(self):
img1 = self.get_sample("samples/data/right01.jpg", cv.IMREAD_GRAYSCALE)
img2 = self.get_sample("samples/data/right02.jpg", cv.IMREAD_GRAYSCALE)
# Note, that if you want to see performance boost by OCL implementation - you need enough data
# For example you can increase maxCorners param to 10000 and increase img1 and img2 in such way:
# img = np.hstack([np.vstack([img] * 6)] * 6)
feature_params = dict(maxCorners=239,
qualityLevel=0.3,
minDistance=7,
blockSize=7)
p0 = cv.goodFeaturesToTrack(img1, mask=None, **feature_params)
p0_umat = cv.goodFeaturesToTrack(cv.UMat(img1), mask=None, **feature_params)
self.assertEqual(p0_umat.get().shape, p0.shape)
p0 = np.array(sorted(p0, key=lambda p: tuple(p[0])))
p0_umat = cv.UMat(np.array(sorted(p0_umat.get(), key=lambda p: tuple(p[0]))))
self.assertTrue(np.allclose(p0_umat.get(), p0))
_p1_mask_err = cv.calcOpticalFlowPyrLK(img1, img2, p0, None)
_p1_mask_err_umat0 = list(map(lambda umat: umat.get(), cv.calcOpticalFlowPyrLK(img1, img2, p0_umat, None)))
_p1_mask_err_umat1 = list(map(lambda umat: umat.get(), cv.calcOpticalFlowPyrLK(cv.UMat(img1), img2, p0_umat, None)))
_p1_mask_err_umat2 = list(map(lambda umat: umat.get(), cv.calcOpticalFlowPyrLK(img1, cv.UMat(img2), p0_umat, None)))
for _p1_mask_err_umat in [_p1_mask_err_umat0, _p1_mask_err_umat1, _p1_mask_err_umat2]:
for data, data_umat in zip(_p1_mask_err, _p1_mask_err_umat):
self.assertEqual(data.shape, data_umat.shape)
self.assertEqual(data.dtype, data_umat.dtype)
for _p1_mask_err_umat in [_p1_mask_err_umat1, _p1_mask_err_umat2]:
for data_umat0, data_umat in zip(_p1_mask_err_umat0[:2], _p1_mask_err_umat[:2]):
self.assertTrue(np.allclose(data_umat0, data_umat))
def test_umat_merge_mertens(self):
if self.extraTestDataPath is None:
self.fail('Test data is not available')
test_data_path = os.path.join(self.extraTestDataPath, 'cv', 'hdr')
images, _ = load_exposure_seq(os.path.join(test_data_path, 'exposures'))
# As we want to test mat vs. umat here, we temporarily set only one worker-thread to achieve
# deterministic summations inside mertens' parallelized process.
num_threads = cv.getNumThreads()
cv.setNumThreads(1)
merge = cv.createMergeMertens()
mat_result = merge.process(images)
umat_images = [cv.UMat(img) for img in images]
umat_result = merge.process(umat_images)
cv.setNumThreads(num_threads)
self.assertTrue(np.allclose(umat_result.get(), mat_result))
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python
'''
Watershed segmentation test
'''
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class watershed_test(NewOpenCVTests):
def test_watershed(self):
img = self.get_sample('cv/inpaint/orig.png')
markers = self.get_sample('cv/watershed/wshed_exp.png', 0)
refSegments = self.get_sample('cv/watershed/wshed_segments.png')
if img is None or markers is None:
self.assertEqual(0, 1, 'Missing test data')
colors = np.int32( list(np.ndindex(3, 3, 3)) ) * 122
cv.watershed(img, np.int32(markers))
segments = colors[np.maximum(markers, 0)]
if refSegments is None:
refSegments = segments.copy()
cv.imwrite(self.extraTestDataPath + '/cv/watershed/wshed_segments.png', refSegments)
self.assertLess(cv.norm(segments - refSegments, cv.NORM_L1) / 255.0, 50)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python
from __future__ import print_function
import os
import sys
import unittest
import hashlib
import random
import argparse
import numpy as np
#sys.OpenCV_LOADER_DEBUG = True
import cv2 as cv
# Python 3 moved urlopen to urllib.requests
try:
from urllib.request import urlopen
except ImportError:
from urllib import urlopen
class NewOpenCVTests(unittest.TestCase):
# path to local repository folder containing 'samples' folder
repoPath = None
extraTestDataPath = None
extraDnnTestDataPath = None
# github repository url
repoUrl = 'https://raw.github.com/opencv/opencv/5.x'
def find_file(self, filename, searchPaths=[], required=True):
searchPaths = searchPaths if searchPaths else [self.repoPath, self.extraTestDataPath, self.extraDnnTestDataPath]
for path in searchPaths:
if path is not None:
candidate = path + '/' + filename
if os.path.isfile(candidate):
return candidate
if required:
self.fail('File ' + filename + ' not found')
else:
self.skipTest('File ' + filename + ' not found')
return None
def get_sample(self, filename, iscolor = None):
if iscolor is None:
iscolor = cv.IMREAD_COLOR
if not filename in self.image_cache:
filepath = self.find_file(filename)
with open(filepath, 'rb') as f:
filedata = f.read()
self.image_cache[filename] = cv.imdecode(np.frombuffer(filedata, dtype=np.uint8), iscolor)
return self.image_cache[filename]
def setUp(self):
cv.setRNGSeed(10)
self.image_cache = {}
def hashimg(self, im):
""" Compute a hash for an image, useful for image comparisons """
return hashlib.md5(im.tobytes()).hexdigest()
if sys.version_info[:2] == (2, 6):
def assertLess(self, a, b, msg=None):
if not a < b:
self.fail('%s not less than %s' % (repr(a), repr(b)))
def assertLessEqual(self, a, b, msg=None):
if not a <= b:
self.fail('%s not less than or equal to %s' % (repr(a), repr(b)))
def assertGreater(self, a, b, msg=None):
if not a > b:
self.fail('%s not greater than %s' % (repr(a), repr(b)))
@staticmethod
def bootstrap():
parser = argparse.ArgumentParser(description='run OpenCV python tests')
parser.add_argument('--repo', help='use sample image files from local git repository (path to folder), '
'if not set, samples will be downloaded from github.com')
parser.add_argument('--data', help='<not used> use data files from local folder (path to folder), '
'if not set, data files will be downloaded from docs.opencv.org')
args, other = parser.parse_known_args()
print("Testing OpenCV", cv.__version__)
print("Local repo path:", args.repo)
NewOpenCVTests.repoPath = args.repo
try:
NewOpenCVTests.extraTestDataPath = os.environ['OPENCV_TEST_DATA_PATH']
except KeyError:
print('Missing opencv extra repository. Some of tests may fail.')
try:
NewOpenCVTests.extraDnnTestDataPath = os.environ['OPENCV_DNN_TEST_DATA_PATH']
except KeyError:
pass
random.seed(0)
unit_argv = [sys.argv[0]] + other
unittest.main(argv=unit_argv)
def intersectionRate(s1, s2):
x1, y1, x2, y2 = s1
s1 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]])
x1, y1, x2, y2 = s2
s2 = np.array([[x1, y1], [x2,y1], [x2, y2], [x1, y2]])
area, _intersection = cv.intersectConvexConvex(s1, s2)
return 2 * area / (cv.contourArea(s1) + cv.contourArea(s2))
def isPointInRect(p, rect):
if rect[0] <= p[0] and rect[1] <=p[1] and p[0] <= rect[2] and p[1] <= rect[3]:
return True
else:
return False
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python
# Python 2/3 compatibility
from __future__ import print_function
import numpy as np
from numpy import pi, sin, cos
import cv2 as cv
defaultSize = 512
class TestSceneRender():
def __init__(self, bgImg = None, fgImg = None, deformation = False, noise = 0.0, speed = 0.25, **params):
self.time = 0.0
self.timeStep = 1.0 / 30.0
self.foreground = fgImg
self.deformation = deformation
self.noise = noise
self.speed = speed
if bgImg is not None:
self.sceneBg = bgImg.copy()
else:
self.sceneBg = np.zeros(defaultSize, defaultSize, np.uint8)
self.w = self.sceneBg.shape[0]
self.h = self.sceneBg.shape[1]
if fgImg is not None:
self.foreground = fgImg.copy()
self.center = self.currentCenter = (int(self.w/2 - fgImg.shape[0]/2), int(self.h/2 - fgImg.shape[1]/2))
self.xAmpl = self.sceneBg.shape[0] - (self.center[0] + fgImg.shape[0])
self.yAmpl = self.sceneBg.shape[1] - (self.center[1] + fgImg.shape[1])
self.initialRect = np.array([ (self.h/2, self.w/2), (self.h/2, self.w/2 + self.w/10),
(self.h/2 + self.h/10, self.w/2 + self.w/10), (self.h/2 + self.h/10, self.w/2)]).astype(int)
self.currentRect = self.initialRect
np.random.seed(10)
def getXOffset(self, time):
return int(self.xAmpl*cos(time*self.speed))
def getYOffset(self, time):
return int(self.yAmpl*sin(time*self.speed))
def setInitialRect(self, rect):
self.initialRect = rect
def getRectInTime(self, time):
if self.foreground is not None:
tmp = np.array(self.center) + np.array((self.getXOffset(time), self.getYOffset(time)))
x0, y0 = tmp
x1, y1 = tmp + self.foreground.shape[0:2]
return np.array([y0, x0, y1, x1])
else:
x0, y0 = self.initialRect[0] + np.array((self.getXOffset(time), self.getYOffset(time)))
x1, y1 = self.initialRect[2] + np.array((self.getXOffset(time), self.getYOffset(time)))
return np.array([y0, x0, y1, x1])
def getCurrentRect(self):
if self.foreground is not None:
x0 = self.currentCenter[0]
y0 = self.currentCenter[1]
x1 = self.currentCenter[0] + self.foreground.shape[0]
y1 = self.currentCenter[1] + self.foreground.shape[1]
return np.array([y0, x0, y1, x1])
else:
x0, y0 = self.currentRect[0]
x1, y1 = self.currentRect[2]
return np.array([x0, y0, x1, y1])
def getNextFrame(self):
img = self.sceneBg.copy()
if self.foreground is not None:
self.currentCenter = (self.center[0] + self.getXOffset(self.time), self.center[1] + self.getYOffset(self.time))
img[self.currentCenter[0]:self.currentCenter[0]+self.foreground.shape[0],
self.currentCenter[1]:self.currentCenter[1]+self.foreground.shape[1]] = self.foreground
else:
self.currentRect = self.initialRect + int( 30*cos(self.time) + 50*sin(self.time/3))
if self.deformation:
self.currentRect[1:3] += int(self.h/20*cos(self.time))
cv.fillConvexPoly(img, self.currentRect, (0, 0, 255))
self.time += self.timeStep
if self.noise:
noise = np.zeros(self.sceneBg.shape, np.int8)
cv.randn(noise, np.zeros(3), np.ones(3)*255*self.noise)
img = cv.add(img, noise, dtype=cv.CV_8UC3)
return img
def resetTime(self):
self.time = 0.0
if __name__ == '__main__':
backGr = cv.imread('../../../samples/data/lena.jpg')
render = TestSceneRender(backGr, noise = 0.5)
while True:
img = render.getNextFrame()
cv.imshow('img', img)
ch = cv.waitKey(3)
if ch == 27:
break
cv.destroyAllWindows()