diff --git a/CMakeLists.txt b/CMakeLists.txt index d43fc01f..a6136e2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,24 @@ cmake_minimum_required(VERSION 3.24 FATAL_ERROR) -if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan_provider.cmake") - message(STATUS "Downloading conan_provider.cmake from https://github.com/conan-io/cmake-conan") - file( - DOWNLOAD - "https://raw.githubusercontent.com/conan-io/cmake-conan/develop2/conan_provider.cmake" - "${CMAKE_BINARY_DIR}/conan_provider.cmake" - ) -endif() +if(TARGET Zivid::Core) + message(STATUS "Building Python wrapper as part of Zivid SDK - using existing Zivid::Core target") + set(ZIVID_PYTHON_SDK_BUILD TRUE) +else() + message(STATUS "Building Python wrapper in standalone mode") + set(ZIVID_PYTHON_SDK_BUILD FALSE) -set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES ${CMAKE_BINARY_DIR}/conan_provider.cmake) + if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan_provider.cmake") + message(STATUS "Downloading conan_provider.cmake from https://github.com/conan-io/cmake-conan") + file( + DOWNLOAD + "https://raw.githubusercontent.com/conan-io/cmake-conan/develop2/conan_provider.cmake" + "${CMAKE_BINARY_DIR}/conan_provider.cmake" + ) + endif() + set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES ${CMAKE_BINARY_DIR}/conan_provider.cmake) +endif() -project(zivid LANGUAGES CXX) +project(ZividPython LANGUAGES CXX) list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) @@ -19,29 +26,36 @@ include(ReadZividVersion) include(CompilerWarningOptions) include(TargetUtilities) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(NOT ZIVID_PYTHON_SDK_BUILD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() option(ZIVID_PYTHON_VERSION "Version number to be compiled into the module" "UNKNOWN") -zivid_read_sdk_version(JSON_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sdk_version.json") -if("${ZIVID_SDK_VERSION}" STREQUAL "") - message(FATAL_ERROR "ZIVID_SDK_VERSION is not set. Please ensure sdk_version.json is present and valid.") +if(NOT ZIVID_PYTHON_SDK_BUILD) + zivid_read_sdk_version(JSON_FILE "${CMAKE_CURRENT_SOURCE_DIR}/sdk_version.json") + if("${ZIVID_SDK_VERSION}" STREQUAL "") + message(FATAL_ERROR "ZIVID_SDK_VERSION is not set. Please ensure sdk_version.json is present and valid.") + endif() + message(STATUS "Standalone build - Using Zivid SDK version: ${ZIVID_SDK_VERSION}") endif() if(MSVC) add_compile_options(/bigobj) endif() -find_package( - Python3 - "${PYTHON_INTERPRETER_VERSION}" - EXACT - REQUIRED - COMPONENTS - Interpreter - Development -) +if(NOT ZIVID_PYTHON_SDK_BUILD) + find_package( + Python3 + "${PYTHON_INTERPRETER_VERSION}" + EXACT + REQUIRED + COMPONENTS + Interpreter + Development + ) +endif() if(WIN32) # If "debug" libraries for Python are installed on Windows, `find_package(Python3 ...)` # above will find debug version `_d` of imported python library during linking. `pybind11` @@ -59,6 +73,38 @@ endif() find_package(Eigen3 3.4.0 CONFIG REQUIRED) find_package(pybind11 2.12.0 CONFIG REQUIRED) -find_package(Zivid ${ZIVID_SDK_VERSION} EXACT COMPONENTS Core REQUIRED) + +if(NOT ZIVID_PYTHON_SDK_BUILD) + find_package(Zivid ${ZIVID_SDK_VERSION} EXACT COMPONENTS Core REQUIRED) +endif() add_subdirectory(src) + +if(DEFINED ZIVID_PYTHON_OUTPUT_DIR AND ZIVID_PYTHON_OUTPUT_DIR) + include(Zivid_PythonStage) + + zivid_stage_python_sources( + TARGET ZividPythonStage_zivid + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/zivid" + DEST_DIR "${ZIVID_PYTHON_OUTPUT_DIR}/zivid" + ) + + zivid_stage_python_sources( + TARGET ZividPythonStage_zivid_init + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules/_zivid" + DEST_DIR "${ZIVID_PYTHON_OUTPUT_DIR}/_zivid" + ) + + add_dependencies( + _zividcore + ZividPythonStage_zivid + ZividPythonStage_zivid_init + ) + if(TARGET _zividvisualization) + add_dependencies( + _zividvisualization + ZividPythonStage_zivid + ZividPythonStage_zivid_init + ) + endif() +endif() diff --git a/CMakePresets.json b/CMakePresets.json index 5e1be42b..7d2e9e60 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,8 +26,8 @@ "description": "Build python wrapper with latest sdk on Linux", "inherits": "common_opts", "cacheVariables": { - "CMAKE_C_COMPILER": "clang-20", - "CMAKE_CXX_COMPILER": "clang++-20", + "CMAKE_C_COMPILER": "clang-21", + "CMAKE_CXX_COMPILER": "clang++-21", "CMAKE_EXE_LINKER_FLAGS": "-fuse-ld=mold", "CMAKE_MODULE_LINKER_FLAGS": "-fuse-ld=mold", "CMAKE_SHARED_LINKER_FLAGS": "-fuse-ld=mold" diff --git a/README.md b/README.md index 78ee1afc..2cae948e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ at [zivid.com](https://www.zivid.com/). ### Dependencies * [Python](https://www.python.org/) version 3.7 or higher -* [Zivid SDK][zivid-download-software-url] version 2.17.2 (see [docs][zivid-software-installation-url] for help) +* [Zivid SDK][zivid-download-software-url] version 2.18.0 (see [docs][zivid-software-installation-url] for help) * [Compiler](doc/CompilerInstallation.md) with C++17 support *Ubuntu users must install Python headers (`apt install python3-dev`) in addition to the regular `python3` package.* diff --git a/cmake/CompilerWarningOptions.cmake b/cmake/CompilerWarningOptions.cmake index 719440ec..8225445c 100644 --- a/cmake/CompilerWarningOptions.cmake +++ b/cmake/CompilerWarningOptions.cmake @@ -1,3 +1,5 @@ +include_guard() + option(WARNINGS "Enable compiler warnings" OFF) if(WARNINGS) @@ -34,6 +36,7 @@ elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(WARNINGS_THAT_SHOULD_BE_IGNORED # WHY it is ok to ignore c++98-compat # Code base should be modern c++98-compat-pedantic # Code base should be modern + nrvo # TODO(ZIVID-11540): Reenable when fixed - return value optimization warning ) foreach(WARNING ${WARNINGS_THAT_SHOULD_BE_FIXED}) diff --git a/cmake/ReadZividVersion.cmake b/cmake/ReadZividVersion.cmake index 8b577d6b..1e691a4b 100644 --- a/cmake/ReadZividVersion.cmake +++ b/cmake/ReadZividVersion.cmake @@ -1,3 +1,5 @@ +include_guard() + # Reads the zivid sdk version from a json file. Sets `ZIVID_SDK_VERSION` CMake variable in parent # scope to the version read from the file. function(zivid_read_sdk_version) diff --git a/cmake/TargetUtilities.cmake b/cmake/TargetUtilities.cmake index 2175f21d..b45d52d5 100644 --- a/cmake/TargetUtilities.cmake +++ b/cmake/TargetUtilities.cmake @@ -1,3 +1,5 @@ +include_guard() + function(_zivid_python_library) set(OPTIONS "") set(ONE_VALUE_ARGUMENTS @@ -37,6 +39,12 @@ function(_zivid_python_library) NO_EXTRAS ) + # Avoid STB_GNU_UNIQUE on guard variables for function-local statics in inline/template + # functions: when two Python extension .so files are loaded into the same process, the + # unified guard otherwise makes the second module skip initialization of its own + # (hidden, per-DSO) copy. See pybind11 PR #949. + target_compile_options(${MODULE_NAME} PRIVATE $<$:-fno-gnu-unique>) + add_library("ZividPython::${ARG_NAME}" ALIAS ${MODULE_NAME}) target_link_libraries(${MODULE_NAME} PRIVATE ${ARG_LINK_LIBRARIES}) @@ -50,6 +58,21 @@ function(_zivid_python_library) "" ) + if(DEFINED ZIVID_PYTHON_OUTPUT_DIR AND ZIVID_PYTHON_OUTPUT_DIR) + set_target_properties( + ${MODULE_NAME} + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY + "${ZIVID_PYTHON_OUTPUT_DIR}/_zivid" + RUNTIME_OUTPUT_DIRECTORY + "${ZIVID_PYTHON_OUTPUT_DIR}/_zivid" + ) + endif() + + if(TARGET zivid-python) + add_dependencies(zivid-python ${MODULE_NAME}) + endif() + target_include_directories( ${MODULE_NAME} PUBLIC diff --git a/continuous-integration/deployment/expected-version.txt b/continuous-integration/deployment/expected-version.txt index 94dc0ec9..cf869073 100644 --- a/continuous-integration/deployment/expected-version.txt +++ b/continuous-integration/deployment/expected-version.txt @@ -1 +1 @@ -2.17.2 +2.18.0 diff --git a/continuous-integration/linux/platform-dependent/fedora/install-sdk.sh b/continuous-integration/linux/platform-dependent/fedora/install-sdk.sh index 346ff4f3..62124051 100755 --- a/continuous-integration/linux/platform-dependent/fedora/install-sdk.sh +++ b/continuous-integration/linux/platform-dependent/fedora/install-sdk.sh @@ -11,4 +11,4 @@ function fedora_install_www_deb { rm -r $TMP_DIR || exit $? } -fedora_install_www_deb "https://downloads.zivid.com/sdk/releases/${ZIVID_SDK_EXACT_VERSION}/u20/zivid_${ZIVID_SDK_EXACT_VERSION}_amd64.deb" || exit $? +fedora_install_www_deb "https://downloads.zivid.com/sdk/releases/${ZIVID_SDK_EXACT_VERSION}/u20/amd64/zivid-opencl_${ZIVID_SDK_EXACT_VERSION}_amd64.deb" || exit $? diff --git a/continuous-integration/linux/platform-dependent/ubuntu/install-sdk.sh b/continuous-integration/linux/platform-dependent/ubuntu/install-sdk.sh index f872c247..46a4d8a4 100755 --- a/continuous-integration/linux/platform-dependent/ubuntu/install-sdk.sh +++ b/continuous-integration/linux/platform-dependent/ubuntu/install-sdk.sh @@ -9,4 +9,4 @@ function ubuntu_install_www_deb { rm -r $TMP_DIR || exit $? } -ubuntu_install_www_deb "https://downloads.zivid.com/sdk/releases/${ZIVID_SDK_EXACT_VERSION}/u20/zivid_${ZIVID_SDK_EXACT_VERSION}_amd64.deb" || exit $? +ubuntu_install_www_deb "https://downloads.zivid.com/sdk/releases/${ZIVID_SDK_EXACT_VERSION}/u20/amd64/zivid-opencl_${ZIVID_SDK_EXACT_VERSION}_amd64.deb" || exit $? diff --git a/continuous-integration/versions.json b/continuous-integration/versions.json index 830b3b37..0bf92ab3 100644 --- a/continuous-integration/versions.json +++ b/continuous-integration/versions.json @@ -1,3 +1,3 @@ { - "ZIVID_SDK_EXACT_VERSION": "2.17.2+440b2367-1" + "ZIVID_SDK_EXACT_VERSION": "2.18.0+1b44dbef-1" } diff --git a/modules/_zivid/__init__.py b/modules/_zivid/__init__.py index c8252ca4..265f867a 100644 --- a/modules/_zivid/__init__.py +++ b/modules/_zivid/__init__.py @@ -93,25 +93,63 @@ def __repr__(self): Array2DPointXYZW, Array2DPointZ, Array2DSNR, + BoundingBox, Camera, + CameraAddress, + CameraHealth, CameraInfo, CameraIntrinsics, CameraState, + ComputeBackend, + ComputeDevice, + CUDAContextPtr, + CUDAStreamPtr, + DeviceArrayNormalXYZ, + DeviceArrayPointXYZ, + DeviceArrayPointXYZW, + DeviceArrayPointZ, + DeviceArraySNR, + DeviceArrayViewBGR, + DeviceArrayViewBGR_SRGB, + DeviceArrayViewBGRA, + DeviceArrayViewBGRA_SRGB, + DeviceArrayViewRGB, + DeviceArrayViewRGB_SRGB, + DeviceArrayViewRGBA, + DeviceArrayViewRGBA_SRGB, + DeviceArrayViewRGBAf, Frame, Frame2D, FrameInfo, + ImageBGR, + ImageBGR_SRGB, ImageBGRA, ImageBGRA_SRGB, + ImageDeviceArrayBGR, + ImageDeviceArrayBGR_SRGB, + ImageDeviceArrayBGRA, + ImageDeviceArrayBGRA_SRGB, + ImageDeviceArrayRGB, + ImageDeviceArrayRGB_SRGB, + ImageDeviceArrayRGBA, + ImageDeviceArrayRGBA_SRGB, + ImageDeviceArrayRGBAf, + ImageRGB, + ImageRGB_SRGB, ImageRGBA, ImageRGBA_SRGB, + Mask, Matrix4x4, NetworkConfiguration, + OpenCLCommandQueuePtr, PixelMapping, PointCloud, ProjectedImage, + Resolution, SceneConditions, Settings, Settings2D, + StreamOrQueue, UnorganizedPointCloud, __version__, calibration, diff --git a/modules/zivid/__init__.py b/modules/zivid/__init__.py index f125176f..62ab3847 100644 --- a/modules/zivid/__init__.py +++ b/modules/zivid/__init__.py @@ -10,18 +10,35 @@ import zivid.presets import zivid.projection import zivid.visualization +from _zivid._zividcore import ( + ComputeBackend, + CUDAContextPtr, + CUDAStreamPtr, + FrameFileType, + OpenCLCommandQueuePtr, + StreamOrQueue, + read_frame_file_type, + synchronize_stream, +) from zivid.application import Application +from zivid.bounding_box import BoundingBox from zivid.camera import Camera +from zivid.camera_address import CameraAddress +from zivid.camera_health import CameraHealth from zivid.camera_info import CameraInfo from zivid.camera_intrinsics import CameraIntrinsics from zivid.camera_state import CameraState +from zivid.device_array_view import DeviceArrayView, create_device_array_view from zivid.frame import Frame from zivid.frame_2d import Frame2D from zivid.frame_info import FrameInfo from zivid.image import Image +from zivid.mask import Mask from zivid.matrix4x4 import Matrix4x4 from zivid.network_configuration import NetworkConfiguration +from zivid.pixel_format import PixelFormat from zivid.point_cloud import PointCloud +from zivid.resolution import Resolution from zivid.scene_conditions import SceneConditions from zivid.sdk_version import SDKVersion from zivid.settings import Settings diff --git a/modules/zivid/_calibration/detector.py b/modules/zivid/_calibration/detector.py index bd34b51e..ebe3108d 100644 --- a/modules/zivid/_calibration/detector.py +++ b/modules/zivid/_calibration/detector.py @@ -68,9 +68,14 @@ def centroid(self): def pose(self): """Get position and orientation of the top left detected corner in camera-space. + This is the top left inner corner as viewed from the board's coordinate system. + Pose calculation works for official Zivid calibration boards only. An exception will be thrown if valid() is false or if the board is not supported. + For an illustration of where the origin of the pose is located, see Zivid Calibration Object: + https://support.zivid.com/en/latest/academy/applications/hand-eye/calibration-object.html + Returns: The Pose of the top left corner (4x4 transformation matrix) """ @@ -187,6 +192,9 @@ def pose(self): The returned pose will be positioned at the center of the marker, and have an orientation such that its z-axis points perpendicularly into the face of the marker. + For an illustration of where the origin of the pose is located, see Zivid Calibration Object: + https://support.zivid.com/en/latest/academy/applications/hand-eye/calibration-object.html + Returns: The Pose of the marker center (4x4 transformation matrix) """ @@ -353,8 +361,8 @@ def detect_calibration_board(source): For further information please visit https://support.zivid.com. Args: - source: A frame containing an image of a calibration board or a camera pointed at - a calibration board + source: A Frame containing an image of a calibration board, + or a Camera pointed at a calibration board Raises: TypeError: If source is not of type Camera or Frame @@ -410,10 +418,10 @@ def detect_markers(frame, allowed_marker_ids, marker_dictionary): For more information on ArUco markers specifically, see the OpenCV documentation on ArUco markers: https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html, - Frame need not contain all markers listed in allowedMarkerIds for a successful detection. + The frame need not contain all markers listed in allowedMarkerIds for a successful detection. Args: - frame: A frame containing an image of one or several fiducial markers + frame: A Frame containing an image of one or several fiducial markers allowed_marker_ids: List of the IDs of markers to be detected marker_dictionary: The name of the marker dictionary to use. The name must be one of the values returned by MarkerDictionary.valid_values() diff --git a/modules/zivid/_calibration/hand_eye.py b/modules/zivid/_calibration/hand_eye.py index a287759c..97cb8656 100644 --- a/modules/zivid/_calibration/hand_eye.py +++ b/modules/zivid/_calibration/hand_eye.py @@ -109,6 +109,17 @@ def __str__(self): return str(self.__impl) +class HandEyeStatus: # pylint: disable=too-few-public-methods + """Enumeration of hand-eye status options. + + The HandEyeOutput.status() method will return one of these values. + """ + + ok = "ok" + insufficient_motion = "insufficient_motion" + insufficient_data_quality = "insufficient_data_quality" + + class HandEyeOutput: """Class representing the result of a hand-eye calibration process. @@ -165,6 +176,14 @@ def residuals(self): """ return [HandEyeResidual(internal_residual) for internal_residual in self.__impl.residuals()] + def status(self): + """Get the status of the calibration. + + Returns: + The calibration status as a string. See HandEyeStatus for possible values. + """ + return self.__impl.status().name + def __str__(self): return str(self.__impl) diff --git a/modules/zivid/application.py b/modules/zivid/application.py index 666fa3a7..2c2a467e 100644 --- a/modules/zivid/application.py +++ b/modules/zivid/application.py @@ -2,6 +2,7 @@ import _zivid from zivid.camera import Camera +from zivid.camera_address import _to_internal_camera_address class Application: @@ -22,35 +23,97 @@ class Application: instances of this class. """ - def __init__(self): - """Initialize application.""" - self.__impl = _zivid.Application() + def __init__(self, cuda_context=None): + """Initialize application. + + Args: + cuda_context: Optional CUDAContextPtr to use a user-provided CUDA context. + This ensures Zivid uses the same CUDA context as your application, + which is required for GPU interoperability with libraries like + PyTorch, CuPy, or other CUDA-based frameworks. + + Example: + import cupy as cp + # Get CuPy's CUDA context + cuda_ctx = cp.cuda.runtime.cudaGetDevice() + ctx_ptr = ... # Get context pointer + app = zivid.Application(zivid.CUDAContextPtr(ctx_ptr)) + """ + if cuda_context is not None: + self.__impl = _zivid.Application(cuda_context) + else: + self.__impl = _zivid.Application() def __str__(self): return str(self.__impl) - def create_file_camera(self, camera_file): - """Create a virtual camera to simulate Zivid measurements by reading data from a file. + def create_file_camera(self, frame): + """Create a virtual camera from a captured frame or a .zfc file. + + A file camera is a virtual camera that replays a previously captured frame. It holds the raw sensor + data from the original capture and reconstructs point clouds and color images from it, so it can be + used to develop and test capture pipelines without physical camera hardware. + + Because a file camera owns only the raw data from a single capture, the settings it can capture with + are more restricted than those of a physical camera: + + - Processing settings (filters, color balance, resampling, etc.) may be changed freely. + + - Engine and Sampling (Pixel and Color) must match the values used for the original capture. + If set to a value different from the original setting, the capture throws. + + - The number of acquisitions must not exceed the number stored in the file camera. Requesting fewer + acquisitions is allowed and replays the corresponding subset; requesting more throws. + + - Acquisition values (Aperture, ExposureTime, Gain, Brightness) do not affect the captured data. + The requested settings are recorded on the returned frame, but they do not change the underlying + sensor data. + + - Settings left unset are set automatically before capture: Engine and Sampling are set from the file + camera, while unset acquisition and processing fields are filled with the camera-model defaults. + + The same restrictions apply to the 2D color acquisitions of a 2D or 2D+3D capture. + + When called with a Frame, the frame must have been captured with + Settings.Diagnostics.Enabled. + + When called with a file path, the file must be a Zivid File Camera (.zfc) file. + This form is deprecated and will be removed in SDK 3.0; use the Frame overload instead. An example file camera may be found among the Sample Data at zivid.com/downloads Args: - camera_file: A pathlib.Path instance or a string specifying a Zivid File Camera (ZFC) file + frame: A Frame captured with diagnostics enabled, or a pathlib.Path / string + pointing to a .zfc file (deprecated) Returns: Zivid virtual Camera instance """ - return Camera(self.__impl.create_file_camera(str(camera_file))) + from zivid.frame import Frame # pylint: disable=import-outside-toplevel + + if isinstance(frame, Frame): + # pylint: disable=protected-access + return Camera(self.__impl.create_file_camera(frame._Frame__impl)) + return Camera(self.__impl.create_file_camera(str(frame))) - def connect_camera(self, serial_number=None): + def connect_camera(self, serial_number=None, address=None): """Connect to the next available Zivid camera. Args: serial_number: Optional serial number string for connecting to a specific camera + address: Optional CameraAddress for connecting directly by hostname or IPv4 address, + bypassing mDNS discovery Returns: Zivid Camera instance + + Raises: + ValueError: If both serial_number and address are provided """ + if serial_number is not None and address is not None: + raise ValueError("Cannot specify both serial_number and address") + if address is not None: + return Camera(self.__impl.connect_camera(_to_internal_camera_address(address))) if serial_number is not None: return Camera(self.__impl.connect_camera(serial_number)) return Camera(self.__impl.connect_camera()) @@ -64,6 +127,14 @@ def cameras(self): """ return [Camera(internal_camera) for internal_camera in self.__impl.cameras()] + def compute_device(self): + """Get the GPU compute device used by the Application. + + Returns: + ComputeDevice instance with information about the GPU being used + """ + return self.__impl.compute_device() + def release(self): """Release the underlying resources.""" try: diff --git a/modules/zivid/bounding_box.py b/modules/zivid/bounding_box.py new file mode 100644 index 00000000..d41791d1 --- /dev/null +++ b/modules/zivid/bounding_box.py @@ -0,0 +1,93 @@ +"""Contains BoundingBox class.""" + +import _zivid + + +class BoundingBox: + """Defines a 2D rectangular bounding box in image coordinates.""" + + def __init__(self, x, y, width, height): + """Construct a BoundingBox object. + + Args: + x: Top-left corner x coordinate as int + y: Top-left corner y coordinate as int + width: Width of the bounding box as int + height: Height of the bounding box as int + """ + self.__impl = _zivid.BoundingBox(x, y, width, height) + + @property + def x(self): + """Get the top-left corner x coordinate. + + Returns: + The x coordinate as int + """ + return self.__impl.x + + @x.setter + def x(self, value): + """Set the top-left corner x coordinate. + + Args: + value: The x coordinate as int + """ + self.__impl.x = value + + @property + def y(self): + """Get the top-left corner y coordinate. + + Returns: + The y coordinate as int + """ + return self.__impl.y + + @y.setter + def y(self, value): + """Set the top-left corner y coordinate. + + Args: + value: The y coordinate as int + """ + self.__impl.y = value + + @property + def width(self): + """Get the width of the bounding box. + + Returns: + The width as int + """ + return self.__impl.width + + @width.setter + def width(self, value): + """Set the width of the bounding box. + + Args: + value: The width as int + """ + self.__impl.width = value + + @property + def height(self): + """Get the height of the bounding box. + + Returns: + The height as int + """ + return self.__impl.height + + @height.setter + def height(self, value): + """Set the height of the bounding box. + + Args: + value: The height as int + """ + self.__impl.height = value + + def __str__(self): + return str(self.__impl) diff --git a/modules/zivid/calibration.py b/modules/zivid/calibration.py index 17284f73..2fc24cc2 100644 --- a/modules/zivid/calibration.py +++ b/modules/zivid/calibration.py @@ -18,6 +18,7 @@ HandEyeInput, HandEyeOutput, HandEyeResidual, + HandEyeStatus, calibrate_eye_in_hand, calibrate_eye_to_hand, ) diff --git a/modules/zivid/camera.py b/modules/zivid/camera.py index bfcfd470..a6ef5ebb 100644 --- a/modules/zivid/camera.py +++ b/modules/zivid/camera.py @@ -1,6 +1,7 @@ """Contains Camera class.""" import _zivid +from zivid.camera_health import _to_camera_health from zivid.camera_info import _to_camera_info from zivid.camera_state import _to_camera_state from zivid.frame import Frame @@ -188,6 +189,22 @@ def info(self): """ return _to_camera_info(self.__impl.info) + def check_health(self): + """Run health checks on the camera and return a severity report. + + The returned CameraHealth contains an aggregated overall severity, along with the severity of each + individual check (link, temperatures, fan, memory and infield verification). Each check uses the + four-level severity scale "OK", "Suboptimal", "Error", "Unknown". The overall value is the worst + severity across all populated checks. + + `check_health` will raise a RuntimeException if the camera status (see `CameraState.Status`) is not + "connected". + + Returns: + The current health check report as a CameraHealth instance. + """ + return _to_camera_health(self.__impl.check_health()) + @property def state(self): """Get the current camera state. diff --git a/modules/zivid/camera_address.py b/modules/zivid/camera_address.py new file mode 100644 index 00000000..ee12770c --- /dev/null +++ b/modules/zivid/camera_address.py @@ -0,0 +1,58 @@ +"""Contains the CameraAddress class.""" + +import _zivid + + +class CameraAddress: + """A hostname or IPv4 address identifying a specific Zivid camera for direct connection. + + Use this to connect to a camera at a known network address, bypassing mDNS discovery. + Accepts either an IPv4 address string (e.g. "172.28.60.5") or a resolvable hostname. + """ + + def __init__(self, value): + """Construct a CameraAddress from a hostname or IPv4 address string. + + Args: + value: A string containing the hostname or IPv4 address of the camera + + Raises: + TypeError: If value is not a string + """ + if not isinstance(value, str): + raise TypeError("value must be a string") + + self.__impl = _zivid.CameraAddress(value) + + @property + def value(self): + """Get the address value. + + Returns: + The hostname or IPv4 address string + """ + return self.__impl.value() + + def __eq__(self, other): + if not isinstance(other, CameraAddress): + return False + return self.__impl == _to_internal_camera_address(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def __str__(self): + return self.__impl.to_string() + + def __repr__(self): + return f"CameraAddress(value={self.value!r})" + + def __hash__(self): + return hash(self.value) + + def _to_internal(self): + return self.__impl + + +def _to_internal_camera_address(address): + return address._to_internal() # pylint: disable=protected-access diff --git a/modules/zivid/camera_health.py b/modules/zivid/camera_health.py new file mode 100644 index 00000000..9720eb68 --- /dev/null +++ b/modules/zivid/camera_health.py @@ -0,0 +1,1046 @@ +"""Auto generated, do not edit.""" + +# pylint: disable=too-many-lines,protected-access,too-few-public-methods,too-many-arguments,too-many-positional-arguments,line-too-long,missing-function-docstring,missing-class-docstring,redefined-builtin,too-many-branches,too-many-boolean-expressions +import datetime + +import _zivid + + +class CameraHealth: + + class Fan: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Fan.Status.Error, + "OK": _zivid.CameraHealth.Fan.Status.OK, + "Suboptimal": _zivid.CameraHealth.Fan.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.Fan.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + class Value: + + ok = "ok" + unexpectedFanStop = "unexpectedFanStop" + + _valid_values = { + "ok": _zivid.CameraHealth.Fan.Value.ok, + "unexpectedFanStop": _zivid.CameraHealth.Fan.Value.unexpectedFanStop, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.Fan.Status().value, + value=_zivid.CameraHealth.Fan.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.Fan.Status.enum): + self._status = _zivid.CameraHealth.Fan.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.Fan.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if isinstance(value, _zivid.CameraHealth.Fan.Value.enum) or value is None: + self._value = _zivid.CameraHealth.Fan.Value(value) + elif isinstance(value, str): + self._value = _zivid.CameraHealth.Fan.Value(self.Value._valid_values[value]) + else: + raise TypeError( + "Unsupported type, expected: str or None, got {value_type}".format(value_type=type(value)) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + if self._value.value is None: + return None + for key, internal_value in self.Value._valid_values.items(): + if internal_value == self._value.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._value)) + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.Fan.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Fan.Status.enum): + self._status = _zivid.CameraHealth.Fan.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if isinstance(value, str): + self._value = _zivid.CameraHealth.Fan.Value(self.Value._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Fan.Value.enum) or value is None: + self._value = _zivid.CameraHealth.Fan.Value(value) + else: + raise TypeError( + "Unsupported type, expected: str or None, got {value_type}".format(value_type=type(value)) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_fan(self)) + + class InfieldVerification: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.InfieldVerification.Status.Error, + "OK": _zivid.CameraHealth.InfieldVerification.Status.OK, + "Suboptimal": _zivid.CameraHealth.InfieldVerification.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.InfieldVerification.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.InfieldVerification.Status().value, + value=_zivid.CameraHealth.InfieldVerification.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.InfieldVerification.Status.enum): + self._status = _zivid.CameraHealth.InfieldVerification.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.InfieldVerification.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if isinstance(value, (datetime.datetime,)) or value is None: + self._value = _zivid.CameraHealth.InfieldVerification.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (datetime.datetime,) or None, got {value_type}".format( + value_type=type(value) + ) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.InfieldVerification.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.InfieldVerification.Status.enum): + self._status = _zivid.CameraHealth.InfieldVerification.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if isinstance(value, (datetime.datetime,)) or value is None: + self._value = _zivid.CameraHealth.InfieldVerification.Value(value) + else: + raise TypeError( + "Unsupported type, expected: datetime.datetime or None, got {value_type}".format( + value_type=type(value) + ) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_infield_verification(self)) + + class MaxTransferSpeed: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.MaxTransferSpeed.Status.Error, + "OK": _zivid.CameraHealth.MaxTransferSpeed.Status.OK, + "Suboptimal": _zivid.CameraHealth.MaxTransferSpeed.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.MaxTransferSpeed.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.MaxTransferSpeed.Status().value, + value=_zivid.CameraHealth.MaxTransferSpeed.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.MaxTransferSpeed.Status.enum): + self._status = _zivid.CameraHealth.MaxTransferSpeed.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.MaxTransferSpeed.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if isinstance(value, (int,)) or value is None: + self._value = _zivid.CameraHealth.MaxTransferSpeed.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (int,) or None, got {value_type}".format(value_type=type(value)) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.MaxTransferSpeed.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.MaxTransferSpeed.Status.enum): + self._status = _zivid.CameraHealth.MaxTransferSpeed.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if isinstance(value, (int,)) or value is None: + self._value = _zivid.CameraHealth.MaxTransferSpeed.Value(value) + else: + raise TypeError( + "Unsupported type, expected: int or None, got {value_type}".format(value_type=type(value)) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_max_transfer_speed(self)) + + class Memory: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Memory.Status.Error, + "OK": _zivid.CameraHealth.Memory.Status.OK, + "Suboptimal": _zivid.CameraHealth.Memory.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.Memory.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.Memory.Status().value, + value=_zivid.CameraHealth.Memory.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.Memory.Status.enum): + self._status = _zivid.CameraHealth.Memory.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.Memory.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if isinstance(value, (int,)) or value is None: + self._value = _zivid.CameraHealth.Memory.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (int,) or None, got {value_type}".format(value_type=type(value)) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.Memory.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Memory.Status.enum): + self._status = _zivid.CameraHealth.Memory.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if isinstance(value, (int,)) or value is None: + self._value = _zivid.CameraHealth.Memory.Value(value) + else: + raise TypeError( + "Unsupported type, expected: int or None, got {value_type}".format(value_type=type(value)) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_memory(self)) + + class Temperature: + + class DMD: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Temperature.DMD.Status.Error, + "OK": _zivid.CameraHealth.Temperature.DMD.Status.OK, + "Suboptimal": _zivid.CameraHealth.Temperature.DMD.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.Temperature.DMD.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.Temperature.DMD.Status().value, + value=_zivid.CameraHealth.Temperature.DMD.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.Temperature.DMD.Status.enum): + self._status = _zivid.CameraHealth.Temperature.DMD.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.Temperature.DMD.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.DMD.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (float, int,) or None, got {value_type}".format( + value_type=type(value) + ) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.Temperature.DMD.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Temperature.DMD.Status.enum): + self._status = _zivid.CameraHealth.Temperature.DMD.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.DMD.Value(value) + else: + raise TypeError( + "Unsupported type, expected: float or int or None, got {value_type}".format( + value_type=type(value) + ) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_temperature_dmd(self)) + + class LED: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Temperature.LED.Status.Error, + "OK": _zivid.CameraHealth.Temperature.LED.Status.OK, + "Suboptimal": _zivid.CameraHealth.Temperature.LED.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.Temperature.LED.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.Temperature.LED.Status().value, + value=_zivid.CameraHealth.Temperature.LED.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.Temperature.LED.Status.enum): + self._status = _zivid.CameraHealth.Temperature.LED.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.Temperature.LED.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.LED.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (float, int,) or None, got {value_type}".format( + value_type=type(value) + ) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.Temperature.LED.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Temperature.LED.Status.enum): + self._status = _zivid.CameraHealth.Temperature.LED.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.LED.Value(value) + else: + raise TypeError( + "Unsupported type, expected: float or int or None, got {value_type}".format( + value_type=type(value) + ) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_temperature_led(self)) + + class Lens: + + class Status: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Temperature.Lens.Status.Error, + "OK": _zivid.CameraHealth.Temperature.Lens.Status.OK, + "Suboptimal": _zivid.CameraHealth.Temperature.Lens.Status.Suboptimal, + "Unknown": _zivid.CameraHealth.Temperature.Lens.Status.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + status=_zivid.CameraHealth.Temperature.Lens.Status().value, + value=_zivid.CameraHealth.Temperature.Lens.Value().value, + ): + + if isinstance(status, _zivid.CameraHealth.Temperature.Lens.Status.enum): + self._status = _zivid.CameraHealth.Temperature.Lens.Status(status) + elif isinstance(status, str): + self._status = _zivid.CameraHealth.Temperature.Lens.Status(self.Status._valid_values[status]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(status))) + + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.Lens.Value(value) + else: + raise TypeError( + "Unsupported type, expected: (float, int,) or None, got {value_type}".format( + value_type=type(value) + ) + ) + + @property + def status(self): + if self._status.value is None: + return None + for key, internal_value in self.Status._valid_values.items(): + if internal_value == self._status.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._status)) + + @property + def value(self): + return self._value.value + + @status.setter + def status(self, value): + if isinstance(value, str): + self._status = _zivid.CameraHealth.Temperature.Lens.Status(self.Status._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Temperature.Lens.Status.enum): + self._status = _zivid.CameraHealth.Temperature.Lens.Status(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @value.setter + def value(self, value): + if ( + isinstance( + value, + ( + float, + int, + ), + ) + or value is None + ): + self._value = _zivid.CameraHealth.Temperature.Lens.Value(value) + else: + raise TypeError( + "Unsupported type, expected: float or int or None, got {value_type}".format( + value_type=type(value) + ) + ) + + def __eq__(self, other): + if self._status == other._status and self._value == other._value: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_temperature_lens(self)) + + def __init__( + self, + dmd=None, + led=None, + lens=None, + ): + + if dmd is None: + dmd = self.DMD() + if not isinstance(dmd, self.DMD): + raise TypeError("Unsupported type: {value}".format(value=type(dmd))) + self._dmd = dmd + + if led is None: + led = self.LED() + if not isinstance(led, self.LED): + raise TypeError("Unsupported type: {value}".format(value=type(led))) + self._led = led + + if lens is None: + lens = self.Lens() + if not isinstance(lens, self.Lens): + raise TypeError("Unsupported type: {value}".format(value=type(lens))) + self._lens = lens + + @property + def dmd(self): + return self._dmd + + @property + def led(self): + return self._led + + @property + def lens(self): + return self._lens + + @dmd.setter + def dmd(self, value): + if not isinstance(value, self.DMD): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._dmd = value + + @led.setter + def led(self, value): + if not isinstance(value, self.LED): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._led = value + + @lens.setter + def lens(self, value): + if not isinstance(value, self.Lens): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._lens = value + + def __eq__(self, other): + if self._dmd == other._dmd and self._led == other._led and self._lens == other._lens: + return True + return False + + def __str__(self): + return str(_to_internal_camera_health_temperature(self)) + + class Overall: + + Error = "Error" + OK = "OK" + Suboptimal = "Suboptimal" + Unknown = "Unknown" + + _valid_values = { + "Error": _zivid.CameraHealth.Overall.Error, + "OK": _zivid.CameraHealth.Overall.OK, + "Suboptimal": _zivid.CameraHealth.Overall.Suboptimal, + "Unknown": _zivid.CameraHealth.Overall.Unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + overall=_zivid.CameraHealth.Overall().value, + fan=None, + infield_verification=None, + max_transfer_speed=None, + memory=None, + temperature=None, + ): + + if isinstance(overall, _zivid.CameraHealth.Overall.enum): + self._overall = _zivid.CameraHealth.Overall(overall) + elif isinstance(overall, str): + self._overall = _zivid.CameraHealth.Overall(self.Overall._valid_values[overall]) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(overall))) + + if fan is None: + fan = self.Fan() + if not isinstance(fan, self.Fan): + raise TypeError("Unsupported type: {value}".format(value=type(fan))) + self._fan = fan + + if infield_verification is None: + infield_verification = self.InfieldVerification() + if not isinstance(infield_verification, self.InfieldVerification): + raise TypeError("Unsupported type: {value}".format(value=type(infield_verification))) + self._infield_verification = infield_verification + + if max_transfer_speed is None: + max_transfer_speed = self.MaxTransferSpeed() + if not isinstance(max_transfer_speed, self.MaxTransferSpeed): + raise TypeError("Unsupported type: {value}".format(value=type(max_transfer_speed))) + self._max_transfer_speed = max_transfer_speed + + if memory is None: + memory = self.Memory() + if not isinstance(memory, self.Memory): + raise TypeError("Unsupported type: {value}".format(value=type(memory))) + self._memory = memory + + if temperature is None: + temperature = self.Temperature() + if not isinstance(temperature, self.Temperature): + raise TypeError("Unsupported type: {value}".format(value=type(temperature))) + self._temperature = temperature + + @property + def overall(self): + if self._overall.value is None: + return None + for key, internal_value in self.Overall._valid_values.items(): + if internal_value == self._overall.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._overall)) + + @property + def fan(self): + return self._fan + + @property + def infield_verification(self): + return self._infield_verification + + @property + def max_transfer_speed(self): + return self._max_transfer_speed + + @property + def memory(self): + return self._memory + + @property + def temperature(self): + return self._temperature + + @overall.setter + def overall(self, value): + if isinstance(value, str): + self._overall = _zivid.CameraHealth.Overall(self.Overall._valid_values[value]) + elif isinstance(value, _zivid.CameraHealth.Overall.enum): + self._overall = _zivid.CameraHealth.Overall(value) + else: + raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + + @fan.setter + def fan(self, value): + if not isinstance(value, self.Fan): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._fan = value + + @infield_verification.setter + def infield_verification(self, value): + if not isinstance(value, self.InfieldVerification): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._infield_verification = value + + @max_transfer_speed.setter + def max_transfer_speed(self, value): + if not isinstance(value, self.MaxTransferSpeed): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._max_transfer_speed = value + + @memory.setter + def memory(self, value): + if not isinstance(value, self.Memory): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._memory = value + + @temperature.setter + def temperature(self, value): + if not isinstance(value, self.Temperature): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._temperature = value + + @classmethod + def load(cls, file_name): + return _to_camera_health(_zivid.CameraHealth(str(file_name))) + + def save(self, file_name): + _to_internal_camera_health(self).save(str(file_name)) + + @classmethod + def from_serialized(cls, value): + return _to_camera_health(_zivid.CameraHealth.from_serialized(str(value))) + + def serialize(self): + return _to_internal_camera_health(self).serialize() + + def __eq__(self, other): + if ( + self._overall == other._overall + and self._fan == other._fan + and self._infield_verification == other._infield_verification + and self._max_transfer_speed == other._max_transfer_speed + and self._memory == other._memory + and self._temperature == other._temperature + ): + return True + return False + + def __str__(self): + return str(_to_internal_camera_health(self)) + + def __deepcopy__(self, memodict): + # Create deep copy by converting to internal representation and back. + # memodict not used since conversion creates entirely new objects. + return _to_camera_health(_to_internal_camera_health(self)) + + +def _to_camera_health_fan(internal_fan): + return CameraHealth.Fan( + status=internal_fan.status.value, + value=internal_fan.value.value, + ) + + +def _to_camera_health_infield_verification(internal_infield_verification): + return CameraHealth.InfieldVerification( + status=internal_infield_verification.status.value, + value=internal_infield_verification.value.value, + ) + + +def _to_camera_health_max_transfer_speed(internal_max_transfer_speed): + return CameraHealth.MaxTransferSpeed( + status=internal_max_transfer_speed.status.value, + value=internal_max_transfer_speed.value.value, + ) + + +def _to_camera_health_memory(internal_memory): + return CameraHealth.Memory( + status=internal_memory.status.value, + value=internal_memory.value.value, + ) + + +def _to_camera_health_temperature_dmd(internal_dmd): + return CameraHealth.Temperature.DMD( + status=internal_dmd.status.value, + value=internal_dmd.value.value, + ) + + +def _to_camera_health_temperature_led(internal_led): + return CameraHealth.Temperature.LED( + status=internal_led.status.value, + value=internal_led.value.value, + ) + + +def _to_camera_health_temperature_lens(internal_lens): + return CameraHealth.Temperature.Lens( + status=internal_lens.status.value, + value=internal_lens.value.value, + ) + + +def _to_camera_health_temperature(internal_temperature): + return CameraHealth.Temperature( + dmd=_to_camera_health_temperature_dmd(internal_temperature.dmd), + led=_to_camera_health_temperature_led(internal_temperature.led), + lens=_to_camera_health_temperature_lens(internal_temperature.lens), + ) + + +def _to_camera_health(internal_camera_health): + return CameraHealth( + fan=_to_camera_health_fan(internal_camera_health.fan), + infield_verification=_to_camera_health_infield_verification(internal_camera_health.infield_verification), + max_transfer_speed=_to_camera_health_max_transfer_speed(internal_camera_health.max_transfer_speed), + memory=_to_camera_health_memory(internal_camera_health.memory), + temperature=_to_camera_health_temperature(internal_camera_health.temperature), + overall=internal_camera_health.overall.value, + ) + + +def _to_internal_camera_health_fan(fan): + internal_fan = _zivid.CameraHealth.Fan() + + internal_fan.status = _zivid.CameraHealth.Fan.Status(fan._status.value) + internal_fan.value = _zivid.CameraHealth.Fan.Value(fan._value.value) + + return internal_fan + + +def _to_internal_camera_health_infield_verification(infield_verification): + internal_infield_verification = _zivid.CameraHealth.InfieldVerification() + + internal_infield_verification.status = _zivid.CameraHealth.InfieldVerification.Status( + infield_verification._status.value + ) + internal_infield_verification.value = _zivid.CameraHealth.InfieldVerification.Value(infield_verification.value) + + return internal_infield_verification + + +def _to_internal_camera_health_max_transfer_speed(max_transfer_speed): + internal_max_transfer_speed = _zivid.CameraHealth.MaxTransferSpeed() + + internal_max_transfer_speed.status = _zivid.CameraHealth.MaxTransferSpeed.Status(max_transfer_speed._status.value) + internal_max_transfer_speed.value = _zivid.CameraHealth.MaxTransferSpeed.Value(max_transfer_speed.value) + + return internal_max_transfer_speed + + +def _to_internal_camera_health_memory(memory): + internal_memory = _zivid.CameraHealth.Memory() + + internal_memory.status = _zivid.CameraHealth.Memory.Status(memory._status.value) + internal_memory.value = _zivid.CameraHealth.Memory.Value(memory.value) + + return internal_memory + + +def _to_internal_camera_health_temperature_dmd(dmd): + internal_dmd = _zivid.CameraHealth.Temperature.DMD() + + internal_dmd.status = _zivid.CameraHealth.Temperature.DMD.Status(dmd._status.value) + internal_dmd.value = _zivid.CameraHealth.Temperature.DMD.Value(dmd.value) + + return internal_dmd + + +def _to_internal_camera_health_temperature_led(led): + internal_led = _zivid.CameraHealth.Temperature.LED() + + internal_led.status = _zivid.CameraHealth.Temperature.LED.Status(led._status.value) + internal_led.value = _zivid.CameraHealth.Temperature.LED.Value(led.value) + + return internal_led + + +def _to_internal_camera_health_temperature_lens(lens): + internal_lens = _zivid.CameraHealth.Temperature.Lens() + + internal_lens.status = _zivid.CameraHealth.Temperature.Lens.Status(lens._status.value) + internal_lens.value = _zivid.CameraHealth.Temperature.Lens.Value(lens.value) + + return internal_lens + + +def _to_internal_camera_health_temperature(temperature): + internal_temperature = _zivid.CameraHealth.Temperature() + + internal_temperature.dmd = _to_internal_camera_health_temperature_dmd(temperature.dmd) + internal_temperature.led = _to_internal_camera_health_temperature_led(temperature.led) + internal_temperature.lens = _to_internal_camera_health_temperature_lens(temperature.lens) + return internal_temperature + + +def _to_internal_camera_health(camera_health): + internal_camera_health = _zivid.CameraHealth() + + internal_camera_health.overall = _zivid.CameraHealth.Overall(camera_health._overall.value) + + internal_camera_health.fan = _to_internal_camera_health_fan(camera_health.fan) + internal_camera_health.infield_verification = _to_internal_camera_health_infield_verification( + camera_health.infield_verification + ) + internal_camera_health.max_transfer_speed = _to_internal_camera_health_max_transfer_speed( + camera_health.max_transfer_speed + ) + internal_camera_health.memory = _to_internal_camera_health_memory(camera_health.memory) + internal_camera_health.temperature = _to_internal_camera_health_temperature(camera_health.temperature) + return internal_camera_health diff --git a/modules/zivid/camera_state.py b/modules/zivid/camera_state.py index d50510cf..84972548 100644 --- a/modules/zivid/camera_state.py +++ b/modules/zivid/camera_state.py @@ -12,6 +12,78 @@ class Network: class LocalInterface: + class Ethernet: + + class LinkSpeed: + + link100Mbps = "link100Mbps" + link10Gbps = "link10Gbps" + link10Mbps = "link10Mbps" + link1Gbps = "link1Gbps" + link2_5Gbps = "link2_5Gbps" + link5Gbps = "link5Gbps" + unknown = "unknown" + + _valid_values = { + "link100Mbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link100Mbps, + "link10Gbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link10Gbps, + "link10Mbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link10Mbps, + "link1Gbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link1Gbps, + "link2_5Gbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link2_5Gbps, + "link5Gbps": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.link5Gbps, + "unknown": _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.unknown, + } + + @classmethod + def valid_values(cls): + return list(cls._valid_values.keys()) + + def __init__( + self, + link_speed=_zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed().value, + ): + + if isinstance(link_speed, _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.enum): + self._link_speed = _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed(link_speed) + elif isinstance(link_speed, str): + self._link_speed = _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed( + self.LinkSpeed._valid_values[link_speed] + ) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format(value_type=type(link_speed)) + ) + + @property + def link_speed(self): + if self._link_speed.value is None: + return None + for key, internal_value in self.LinkSpeed._valid_values.items(): + if internal_value == self._link_speed.value: + return key + raise ValueError("Unsupported value {value}".format(value=self._link_speed)) + + @link_speed.setter + def link_speed(self, value): + if isinstance(value, str): + self._link_speed = _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed( + self.LinkSpeed._valid_values[value] + ) + elif isinstance(value, _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed.enum): + self._link_speed = _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed(value) + else: + raise TypeError( + "Unsupported type, expected: str, got {value_type}".format(value_type=type(value)) + ) + + def __eq__(self, other): + if self._link_speed == other._link_speed: + return True + return False + + def __str__(self): + return str(_to_internal_camera_state_network_local_interface_ethernet(self)) + class IPV4: class Subnet: @@ -117,6 +189,7 @@ def __str__(self): def __init__( self, interface_name=_zivid.CameraState.Network.LocalInterface.InterfaceName().value, + ethernet=None, ipv4=None, ): @@ -127,6 +200,12 @@ def __init__( "Unsupported type, expected: (str,), got {value_type}".format(value_type=type(interface_name)) ) + if ethernet is None: + ethernet = self.Ethernet() + if not isinstance(ethernet, self.Ethernet): + raise TypeError("Unsupported type: {value}".format(value=type(ethernet))) + self._ethernet = ethernet + if ipv4 is None: ipv4 = self.IPV4() if not isinstance(ipv4, self.IPV4): @@ -137,6 +216,10 @@ def __init__( def interface_name(self): return self._interface_name.value + @property + def ethernet(self): + return self._ethernet + @property def ipv4(self): return self._ipv4 @@ -148,6 +231,12 @@ def interface_name(self, value): else: raise TypeError("Unsupported type, expected: str, got {value_type}".format(value_type=type(value))) + @ethernet.setter + def ethernet(self, value): + if not isinstance(value, self.Ethernet): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._ethernet = value + @ipv4.setter def ipv4(self, value): if not isinstance(value, self.IPV4): @@ -155,7 +244,11 @@ def ipv4(self, value): self._ipv4 = value def __eq__(self, other): - if self._interface_name == other._interface_name and self._ipv4 == other._ipv4: + if ( + self._interface_name == other._interface_name + and self._ethernet == other._ethernet + and self._ipv4 == other._ipv4 + ): return True return False @@ -745,6 +838,12 @@ def __deepcopy__(self, memodict): return _to_camera_state(_to_internal_camera_state(self)) +def _to_camera_state_network_local_interface_ethernet(internal_ethernet): + return CameraState.Network.LocalInterface.Ethernet( + link_speed=internal_ethernet.link_speed.value, + ) + + def _to_camera_state_network_local_interface_ipv4_subnet(internal_subnet): return CameraState.Network.LocalInterface.IPV4.Subnet( address=internal_subnet.address.value, @@ -760,6 +859,7 @@ def _to_camera_state_network_local_interface_ipv4(internal_ipv4): def _to_camera_state_network_local_interface(internal_local_interface): return CameraState.Network.LocalInterface( + ethernet=_to_camera_state_network_local_interface_ethernet(internal_local_interface.ethernet), ipv4=_to_camera_state_network_local_interface_ipv4(internal_local_interface.ipv4), interface_name=internal_local_interface.interface_name.value, ) @@ -808,6 +908,16 @@ def _to_camera_state(internal_camera_state): ) +def _to_internal_camera_state_network_local_interface_ethernet(ethernet): + internal_ethernet = _zivid.CameraState.Network.LocalInterface.Ethernet() + + internal_ethernet.link_speed = _zivid.CameraState.Network.LocalInterface.Ethernet.LinkSpeed( + ethernet._link_speed.value + ) + + return internal_ethernet + + def _to_internal_camera_state_network_local_interface_ipv4_subnet(subnet): internal_subnet = _zivid.CameraState.Network.LocalInterface.IPV4.Subnet() @@ -835,6 +945,9 @@ def _to_internal_camera_state_network_local_interface(local_interface): local_interface.interface_name ) + internal_local_interface.ethernet = _to_internal_camera_state_network_local_interface_ethernet( + local_interface.ethernet + ) internal_local_interface.ipv4 = _to_internal_camera_state_network_local_interface_ipv4(local_interface.ipv4) return internal_local_interface diff --git a/modules/zivid/device_array.py b/modules/zivid/device_array.py new file mode 100644 index 00000000..355fe9ff --- /dev/null +++ b/modules/zivid/device_array.py @@ -0,0 +1,280 @@ +"""Contains the DeviceArray class.""" + +import _zivid +import numpy + + +def _require_stream_or_queue(stream_or_queue): + """Validate that the argument is a CUDAStreamPtr, OpenCLCommandQueuePtr, or StreamOrQueue.""" + if not isinstance(stream_or_queue, (_zivid.CUDAStreamPtr, _zivid.OpenCLCommandQueuePtr, _zivid.StreamOrQueue)): + raise TypeError( + "Expected an instance of CUDAStreamPtr, OpenCLCommandQueuePtr, or StreamOrQueue, got {}".format( + type(stream_or_queue) + ) + ) + + +_CUDA_ARRAY_INTERFACE_TYPESTR = { + _zivid.ImageDeviceArrayRGBA: "|u1", + _zivid.ImageDeviceArrayBGRA: "|u1", + _zivid.ImageDeviceArrayRGBA_SRGB: "|u1", + _zivid.ImageDeviceArrayBGRA_SRGB: "|u1", + _zivid.ImageDeviceArrayRGBAf: " 1, False -> 0) + if mask_data.dtype == bool: + mask_array = mask_data.astype(np.uint8) + elif mask_data.dtype == np.uint8: + mask_array = mask_data + else: + # Convert other numeric types to uint8, treating non-zero as 1 + mask_array = (mask_data != 0).astype(np.uint8) + + height, width = mask_array.shape + + resolution = Resolution(width, height) + + # Create mask from raw data + mask_bytes = mask_array.tobytes() + self.__impl = _zivid.Mask(resolution._to_internal(), mask_bytes) # pylint: disable=protected-access + else: + raise TypeError( + f"Unsupported type for mask_data. Got {type(mask_data)}, expected numpy array or Resolution" + ) + + @property + def width(self): + """Get the width of the mask. + + Returns: + Width as an integer + """ + return self.__impl.width() + + @property + def height(self): + """Get the height of the mask. + + Returns: + Height as an integer + """ + return self.__impl.height() + + @property + def resolution(self): + """Get the resolution of the mask. + + Returns: + Resolution object with width and height + """ + return Resolution(self.__impl.width(), self.__impl.height()) + + def to_array(self): + """Convert mask to numpy array. + + Returns: + 2D numpy array of uint8 values + """ + return np.array(self.__impl) + + def __str__(self): + """Get string representation of the mask.""" + return self.__impl.to_string() + + def __repr__(self): + """Get string representation of the mask.""" + return self.__impl.to_string() + + def release(self): + """Release the underlying resources.""" + try: + impl = self.__impl + except AttributeError: + pass + else: + impl.release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, traceback): + self.release() + + def __del__(self): + self.release() + + def __copy__(self): + return Mask(self.__impl.__copy__()) + + def __deepcopy__(self, memodict): + return Mask(self.__impl.__deepcopy__(memodict)) + + def _to_internal(self): + """Get the internal implementation. + + Returns: + Internal _zivid.Mask object + """ + return self.__impl + + +def _to_internal_mask(mask): + """Convert Mask to internal implementation. + + Args: + mask: Mask object to convert + + Returns: + Internal _zivid.Mask object + """ + return mask._to_internal() # pylint: disable=protected-access diff --git a/modules/zivid/pixel_format.py b/modules/zivid/pixel_format.py new file mode 100644 index 00000000..a5e45dd2 --- /dev/null +++ b/modules/zivid/pixel_format.py @@ -0,0 +1,51 @@ +"""Contains the PixelFormat enum used to select a device-array color format.""" + +from enum import Enum + + +class PixelFormat(Enum): + """Color format of a device array or DeviceArrayView. + + The 4-channel formats are 3D (height x width x 4): ``RGBA`` / ``BGRA`` are 8-bit, ``RGBA_SRGB`` / ``BGRA_SRGB`` + are 8-bit sRGB-encoded, and ``RGBAF`` is float32. The 3-channel formats are 3D (height x width x 3, no alpha): + ``RGB`` / ``BGR`` are 8-bit, ``RGB_SRGB`` / ``BGR_SRGB`` are 8-bit sRGB-encoded. + + Not every accessor accepts every format. ``create_device_array_view`` accepts all color formats; the Frame2D color + accessors (allocate and fill) cover all color formats, while the point-cloud color accessors cover a subset. + """ + + RGBA = "rgba" + BGRA = "bgra" + RGBA_SRGB = "rgba_srgb" + BGRA_SRGB = "bgra_srgb" + RGBAF = "rgbaf" + RGB = "rgb" + RGB_SRGB = "rgb_srgb" + BGR = "bgr" + BGR_SRGB = "bgr_srgb" + + +def _resolve_color_format(color_format, supported): + """Validate color_format and map it to an accessor-specific dispatch value. + + Args: + color_format: The value to validate. Must be a zivid.PixelFormat. + supported: An ordered mapping {PixelFormat: dispatch_value} declaring which formats the + calling accessor accepts and what to dispatch to for each. + + Returns: + supported[color_format]. + + Raises: + TypeError: If color_format is not a zivid.PixelFormat. + ValueError: If color_format is a PixelFormat that this accessor does not support. + """ + if not isinstance(color_format, PixelFormat): + raise TypeError("color_format must be a zivid.PixelFormat, got {}".format(type(color_format))) + if color_format not in supported: + raise ValueError( + "{} is not a supported format here. Supported formats: {}.".format( + color_format.name, ", ".join(fmt.name for fmt in supported) + ) + ) + return supported[color_format] diff --git a/modules/zivid/point_cloud.py b/modules/zivid/point_cloud.py index 8ee54ef5..37a160a3 100644 --- a/modules/zivid/point_cloud.py +++ b/modules/zivid/point_cloud.py @@ -2,9 +2,21 @@ import _zivid import numpy +from zivid.device_array import DeviceArray, _require_stream_or_queue from zivid.image import Image +from zivid.mask import Mask, _to_internal_mask +from zivid.pixel_format import PixelFormat, _resolve_color_format +from zivid.settings import _to_internal_settings_region_of_interest_box from zivid.unorganized_point_cloud import UnorganizedPointCloud +_COLOR_FORMAT_ACCESSOR_SUFFIX = { + PixelFormat.RGBA: "rgba", + PixelFormat.RGBA_SRGB: "rgba_srgb", + PixelFormat.BGRA: "bgra", + PixelFormat.BGRA_SRGB: "bgra_srgb", + PixelFormat.RGBAF: "rgbaf", +} + class PointCloud: """Point cloud with x, y, z, RGB and color laid out on a 2D grid. @@ -269,6 +281,87 @@ def downsampled(self, downsampling): internal_downsampling = PointCloud.Downsampling._valid_values[downsampling] # pylint: disable=protected-access return PointCloud(self.__impl.downsampled(internal_downsampling)) + def mask_by_region_of_interest(self, roi_box): + """Apply a region of interest box mask to the point cloud in-place. + + Region of interest masking is used to mask out points that fall outside a specified 3D box region. + Points outside the region are set to invalid (NaN) values, effectively removing them from the point cloud + while maintaining the original dimensions and structure. + + The masking is performed on the compute device. The point cloud is modified in-place. Use + "masked_by_region_of_interest" if you want to apply ROI masking to a new PointCloud instance. + + The ROI box must be enabled (roi_box.enabled == True) for the masking to be applied. + + Args: + roi_box: A zivid.Settings.RegionOfInterest.Box instance defining the 3D region to preserve + + Returns: + Reference to the same PointCloud instance (for chaining calls) + """ + internal_roi_box = _to_internal_settings_region_of_interest_box(roi_box) + self.__impl.mask_by_region_of_interest(internal_roi_box) + return self + + def masked_by_region_of_interest(self, roi_box): + """Apply region of interest filtering to a copy of the point cloud. + + This method is identical to "mask_by_region_of_interest", except that the filtering is + performed on a copy of the original point cloud. This method does not modify the original + point cloud. + + Args: + roi_box: A zivid.Settings.RegionOfInterest.Box instance defining the 3D region to preserve + + Returns: + A new PointCloud instance + """ + internal_roi_box = _to_internal_settings_region_of_interest_box(roi_box) + return PointCloud(self.__impl.masked_by_region_of_interest(internal_roi_box)) + + def mask(self, mask): + """Apply a binary mask to the point cloud in-place. + + The mask indicates which points in the point cloud should be considered invalid (NaN). + The mask can be a 2D numpy array of booleans/uint8 or a zivid.Mask object with the same + height and width as the point cloud. A value of True/non-zero in the mask indicates that + the corresponding point in the point cloud should be set to invalid (NaN). A value of + False/zero indicates that the corresponding point should be kept unchanged. + + Args: + mask: A binary mask as a 2D numpy array of booleans/uint8 or a zivid.Mask object + + Returns: + Reference to the same PointCloud instance (for chaining calls) + """ + if isinstance(mask, Mask): + # Already a Mask object, use its internal implementation + self.__impl.mask(_to_internal_mask(mask)) + else: + # Convert numpy array or other data to Mask first + mask_obj = Mask(mask) + self.__impl.mask(_to_internal_mask(mask_obj)) + return self + + def masked(self, mask): + """Get a copy of the point cloud with a binary mask applied. + + This method is identical to "mask", except the masked point cloud is + returned as a new PointCloud instance. The current point cloud is not modified. + + Args: + mask: A binary mask as a 2D numpy array of booleans/uint8 or a zivid.Mask object + + Returns: + A new PointCloud instance with the mask applied + """ + if isinstance(mask, Mask): + # Already a Mask object, use its internal implementation + return PointCloud(self.__impl.masked(_to_internal_mask(mask))) + # Convert numpy array or other data to Mask first + mask_obj = Mask(mask) + return PointCloud(self.__impl.masked(_to_internal_mask(mask_obj))) + @property def height(self): """Get the height of the point cloud (number of rows). @@ -306,6 +399,112 @@ def to_unorganized_point_cloud(self): """ return UnorganizedPointCloud(self.__impl.to_unorganized_point_cloud()) + def device_points_xyz(self, stream_or_queue): + """Get a GPU device array containing XYZ point coordinates. + + Returns a DeviceArray providing access to point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing XYZ point coordinates. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_points_xyz(stream_or_queue)) + + def device_points_xyzw(self, stream_or_queue): + """Get a GPU device array containing XYZW point coordinates. + + Returns a DeviceArray providing access to point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing XYZW point coordinates. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_points_xyzw(stream_or_queue)) + + def device_points_z(self, stream_or_queue): + """Get a GPU device array containing Z point coordinates. + + Returns a DeviceArray providing access to point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing Z point coordinates. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_points_z(stream_or_queue)) + + def device_snrs(self, stream_or_queue): + """Get a GPU device array containing SNR values. + + Returns a DeviceArray providing access to point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing SNR values. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_snrs(stream_or_queue)) + + def device_normals_xyz(self, stream_or_queue): + """Get a GPU device array containing normal vectors. + + Returns a DeviceArray providing access to point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing normal vectors. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_normals_xyz(stream_or_queue)) + + def device_image(self, stream_or_queue, color_format): + """Get a GPU device array containing the organized color image. + + Returns a DeviceArray providing access to point cloud color data on the GPU device without + CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr the SDK records a readiness + event on before handing off the buffer. + color_format: A zivid.PixelFormat color format. Supported: RGBA, BGRA, RGBA_SRGB, + BGRA_SRGB, RGBAF. + + Returns: + A DeviceArray object containing the color image data. + """ + suffix = _resolve_color_format(color_format, _COLOR_FORMAT_ACCESSOR_SUFFIX) + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + accessor = getattr(self.__impl, "device_image_{}".format(suffix)) + return DeviceArray(accessor(stream_or_queue)) + def release(self): """Release the underlying resources.""" try: diff --git a/modules/zivid/resolution.py b/modules/zivid/resolution.py new file mode 100644 index 00000000..5fdfbea1 --- /dev/null +++ b/modules/zivid/resolution.py @@ -0,0 +1,125 @@ +"""Contains the Resolution class.""" + +import _zivid + + +class Resolution: + """Class describing a resolution with a width and a height. + + A Resolution represents the dimensions (width x height) of 2D data structures + such as images, point clouds, or masks. + """ + + def __init__(self, width, height): + """Construct a Resolution from width and height. + + Args: + width: Width as a positive integer + height: Height as a positive integer + + Raises: + TypeError: If width or height are not integers + ValueError: If width or height are not positive + """ + if not isinstance(width, int) or not isinstance(height, int): + raise TypeError("Width and height must be integers") + + if width < 0 or height < 0: + raise ValueError("Width and height must be non-negative") + + self.__impl = _zivid.Resolution(width, height) + + @property + def width(self): + """Get the width value of the resolution. + + Returns: + Width as an integer + """ + return self.__impl.width() + + @property + def height(self): + """Get the height value of the resolution. + + Returns: + Height as an integer + """ + return self.__impl.height() + + @property + def size(self): + """Get the size (area) that is the product of the width and height. + + Returns: + Area as an integer (width * height) + """ + return self.__impl.size() + + def __eq__(self, other): + """Check if two resolutions are equal. + + Args: + other: Another Resolution object + + Returns: + True if width and height are equal, False otherwise + """ + if not isinstance(other, Resolution): + return False + return self.__impl == _to_internal_resolution(other) + + def __ne__(self, other): + """Check if two resolutions are not equal. + + Args: + other: Another Resolution object + + Returns: + True if width or height are different, False otherwise + """ + return not self.__eq__(other) + + def __str__(self): + """Get string representation of the resolution. + + Returns: + String representation like "1920x1080" + """ + return self.__impl.to_string() + + def __repr__(self): + """Get string representation of the resolution. + + Returns: + String representation like "Resolution(width=1920, height=1080)" + """ + return f"Resolution(width={self.width}, height={self.height})" + + def __hash__(self): + """Get hash of the resolution for use in sets and dictionaries. + + Returns: + Hash value based on width and height + """ + return hash((self.width, self.height)) + + def _to_internal(self): + """Get the internal implementation (for use by other Zivid classes). + + Returns: + The internal _zivid.Resolution object + """ + return self.__impl + + +def _to_internal_resolution(resolution): + """Convert Resolution to internal implementation. + + Args: + resolution: Resolution object to convert + + Returns: + Internal _zivid.Resolution object + """ + return resolution._to_internal() # pylint: disable=protected-access diff --git a/modules/zivid/settings2d.py b/modules/zivid/settings2d.py index b0e1ae35..83f0a43f 100644 --- a/modules/zivid/settings2d.py +++ b/modules/zivid/settings2d.py @@ -174,6 +174,41 @@ def __eq__(self, other): def __str__(self): return str(_to_internal_settings2d_acquisition(self)) + class Diagnostics: + + def __init__( + self, + enabled=_zivid.Settings2D.Diagnostics.Enabled().value, + ): + + if isinstance(enabled, (bool,)) or enabled is None: + self._enabled = _zivid.Settings2D.Diagnostics.Enabled(enabled) + else: + raise TypeError( + "Unsupported type, expected: (bool,) or None, got {value_type}".format(value_type=type(enabled)) + ) + + @property + def enabled(self): + return self._enabled.value + + @enabled.setter + def enabled(self, value): + if isinstance(value, (bool,)) or value is None: + self._enabled = _zivid.Settings2D.Diagnostics.Enabled(value) + else: + raise TypeError( + "Unsupported type, expected: bool or None, got {value_type}".format(value_type=type(value)) + ) + + def __eq__(self, other): + if self._enabled == other._enabled: + return True + return False + + def __str__(self): + return str(_to_internal_settings2d_diagnostics(self)) + class Processing: class Color: @@ -699,6 +734,7 @@ def __str__(self): def __init__( self, acquisitions=None, + diagnostics=None, processing=None, sampling=None, ): @@ -719,6 +755,12 @@ def __init__( ) ) + if diagnostics is None: + diagnostics = self.Diagnostics() + if not isinstance(diagnostics, self.Diagnostics): + raise TypeError("Unsupported type: {value}".format(value=type(diagnostics))) + self._diagnostics = diagnostics + if processing is None: processing = self.Processing() if not isinstance(processing, self.Processing): @@ -735,6 +777,10 @@ def __init__( def acquisitions(self): return self._acquisitions + @property + def diagnostics(self): + return self._diagnostics + @property def processing(self): return self._processing @@ -754,6 +800,12 @@ def acquisitions(self, value): else: raise TypeError("Unsupported type {item_type}".format(item_type=type(item))) + @diagnostics.setter + def diagnostics(self, value): + if not isinstance(value, self.Diagnostics): + raise TypeError("Unsupported type {value}".format(value=type(value))) + self._diagnostics = value + @processing.setter def processing(self, value): if not isinstance(value, self.Processing): @@ -783,6 +835,7 @@ def serialize(self): def __eq__(self, other): if ( self._acquisitions == other._acquisitions + and self._diagnostics == other._diagnostics and self._processing == other._processing and self._sampling == other._sampling ): @@ -807,6 +860,12 @@ def _to_settings2d_acquisition(internal_acquisition): ) +def _to_settings2d_diagnostics(internal_diagnostics): + return Settings2D.Diagnostics( + enabled=internal_diagnostics.enabled.value, + ) + + def _to_settings2d_processing_color_balance(internal_balance): return Settings2D.Processing.Color.Balance( blue=internal_balance.blue.value, @@ -853,6 +912,7 @@ def _to_settings2d_sampling(internal_sampling): def _to_settings2d(internal_settings2d): return Settings2D( acquisitions=[_to_settings2d_acquisition(value) for value in internal_settings2d.acquisitions.value], + diagnostics=_to_settings2d_diagnostics(internal_settings2d.diagnostics), processing=_to_settings2d_processing(internal_settings2d.processing), sampling=_to_settings2d_sampling(internal_settings2d.sampling), ) @@ -869,6 +929,14 @@ def _to_internal_settings2d_acquisition(acquisition): return internal_acquisition +def _to_internal_settings2d_diagnostics(diagnostics): + internal_diagnostics = _zivid.Settings2D.Diagnostics() + + internal_diagnostics.enabled = _zivid.Settings2D.Diagnostics.Enabled(diagnostics.enabled) + + return internal_diagnostics + + def _to_internal_settings2d_processing_color_balance(balance): internal_balance = _zivid.Settings2D.Processing.Color.Balance() @@ -931,6 +999,7 @@ def _to_internal_settings2d(settings2d): temp_acquisitions.append(_to_internal_settings2d_acquisition(value)) internal_settings2d.acquisitions = temp_acquisitions + internal_settings2d.diagnostics = _to_internal_settings2d_diagnostics(settings2d.diagnostics) internal_settings2d.processing = _to_internal_settings2d_processing(settings2d.processing) internal_settings2d.sampling = _to_internal_settings2d_sampling(settings2d.sampling) return internal_settings2d diff --git a/modules/zivid/unorganized_point_cloud.py b/modules/zivid/unorganized_point_cloud.py index e99e6e0f..6d02f071 100644 --- a/modules/zivid/unorganized_point_cloud.py +++ b/modules/zivid/unorganized_point_cloud.py @@ -2,6 +2,15 @@ import _zivid import numpy +from zivid.device_array import DeviceArray, _require_stream_or_queue +from zivid.pixel_format import PixelFormat, _resolve_color_format + +_COLOR_FORMAT_ACCESSOR_SUFFIX = { + PixelFormat.RGBA: "rgba", + PixelFormat.RGBA_SRGB: "rgba_srgb", + PixelFormat.BGRA: "bgra", + PixelFormat.BGRA_SRGB: "bgra_srgb", +} class UnorganizedPointCloud: @@ -151,6 +160,78 @@ def painted_uniform_color(self, color): """ return UnorganizedPointCloud(self.__impl.painted_uniform_color(color)) + def device_points_xyz(self, stream_or_queue): + """Get a GPU device array containing XYZ point coordinates. + + Returns a DeviceArray (with shape [1, N, 3]) providing access to unorganized point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing XYZ point coordinates. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_array_xyz(stream_or_queue)) + + def device_points_xyzw(self, stream_or_queue): + """Get a GPU device array containing XYZW point coordinates. + + Returns a DeviceArray providing access to unorganized point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing XYZW point coordinates. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_array_xyzw(stream_or_queue)) + + def device_snrs(self, stream_or_queue): + """Get a GPU device array containing SNR values. + + Returns a DeviceArray providing access to unorganized point cloud data + on the GPU device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr to synchronize SDK + operations with before handing off the buffer. + + Returns: + A DeviceArray object containing SNR values. + """ + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + return DeviceArray(self.__impl.device_array_snr(stream_or_queue)) + + def device_colors(self, stream_or_queue, color_format): + """Get a GPU device array containing the unorganized point cloud colors. + + Returns a DeviceArray providing access to unorganized point cloud color data on the GPU + device without CPU transfers. + + Args: + stream_or_queue: A CUDAStreamPtr or OpenCLCommandQueuePtr the SDK records a readiness + event on before handing off the buffer. + color_format: A zivid.PixelFormat color format. Supported: RGBA, BGRA, RGBA_SRGB, + BGRA_SRGB. + + Returns: + A DeviceArray object containing the color data. + """ + suffix = _resolve_color_format(color_format, _COLOR_FORMAT_ACCESSOR_SUFFIX) + self.__impl.assert_not_released() + _require_stream_or_queue(stream_or_queue) + accessor = getattr(self.__impl, "device_array_{}".format(suffix)) + return DeviceArray(accessor(stream_or_queue)) + def copy_data(self, data_format): """Copy point cloud data from GPU to numpy array. diff --git a/samples/sample_read_barcodes.py b/samples/sample_read_barcodes.py index 756f02e7..df0333b1 100644 --- a/samples/sample_read_barcodes.py +++ b/samples/sample_read_barcodes.py @@ -13,6 +13,7 @@ def _main(): LinearBarcodeFormat.ean8, LinearBarcodeFormat.upcA, LinearBarcodeFormat.upcE, + LinearBarcodeFormat.itf, } matrix_barcode_formats = { diff --git a/sdk_version.json b/sdk_version.json index a799f563..bad3a67d 100644 --- a/sdk_version.json +++ b/sdk_version.json @@ -1,6 +1,6 @@ { "major": 2, - "minor": 17, - "patch": 2, + "minor": 18, + "patch": 0, "pre_release": "" } diff --git a/src/Core/BoundingBox.cpp b/src/Core/BoundingBox.cpp new file mode 100644 index 00000000..5d615302 --- /dev/null +++ b/src/Core/BoundingBox.cpp @@ -0,0 +1,18 @@ +#include "ZividPython/BoundingBox.h" + +#include + +#include +#include + +namespace py = pybind11; +using Zivid::BoundingBox; + +void ZividPython::wrapClass(py::class_ pyClass) +{ + pyClass.def(py::init(), py::arg("x"), py::arg("y"), py::arg("width"), py::arg("height")) + .def_readwrite("x", &BoundingBox::x) + .def_readwrite("y", &BoundingBox::y) + .def_readwrite("width", &BoundingBox::width) + .def_readwrite("height", &BoundingBox::height); +} diff --git a/src/Core/CMakeLists.txt b/src/Core/CMakeLists.txt index f7ddca7a..26e15e6f 100644 --- a/src/Core/CMakeLists.txt +++ b/src/Core/CMakeLists.txt @@ -6,6 +6,8 @@ project( ) set(SOURCES + BoundingBox.cpp + CameraAddress.cpp Calibration/Calibration.cpp Calibration/Detector.cpp Calibration/HandEye.cpp @@ -23,12 +25,19 @@ set(SOURCES ReleasableArray1D.cpp ReleasableArray2D.cpp ReleasableCamera.cpp + FrameFileType.cpp + ReleasableComputeDevice.cpp ReleasableFrame.cpp ReleasableFrame2D.cpp + ReleasableMask.cpp ReleasableImage.cpp + ReleasableImageDeviceArray.cpp + ReleasableDeviceArrayView.cpp ReleasablePointCloud.cpp + ReleasablePointCloudDeviceArray.cpp ReleasableUnorganizedPointCloud.cpp ReleasableProjectedImage.cpp + Resolution.cpp SingletonApplication.cpp Toolbox/PointCloudRegistration.cpp Toolbox/Barcode.cpp diff --git a/src/Core/Calibration/Calibration.cpp b/src/Core/Calibration/Calibration.cpp index f8c961a7..68a9e7bd 100644 --- a/src/Core/Calibration/Calibration.cpp +++ b/src/Core/Calibration/Calibration.cpp @@ -30,6 +30,7 @@ namespace ZividPython::Calibration using namespace Zivid::Experimental::Calibration::HandEyeLowDOF; ZIVID_PYTHON_WRAP_ENUM_CLASS(dest, CalibrationBoardDetectionStatus); + ZIVID_PYTHON_WRAP_ENUM_CLASS(dest, HandEyeStatus); ZIVID_PYTHON_WRAP_CLASS(dest, Pose); ZIVID_PYTHON_WRAP_CLASS(dest, HandEyeOutput); diff --git a/src/Core/Calibration/HandEye.cpp b/src/Core/Calibration/HandEye.cpp index a410c7a7..01dc676f 100644 --- a/src/Core/Calibration/HandEye.cpp +++ b/src/Core/Calibration/HandEye.cpp @@ -11,6 +11,13 @@ namespace py = pybind11; namespace ZividPython { + void wrapEnum(pybind11::enum_ pyEnum) + { + pyEnum.value("ok", Zivid::Calibration::HandEyeStatus::ok) + .value("insufficient_motion", Zivid::Calibration::HandEyeStatus::insufficientMotion) + .value("insufficient_data_quality", Zivid::Calibration::HandEyeStatus::insufficientDataQuality); + } + void wrapClass(pybind11::class_ pyClass) { pyClass.def("rotation", &Zivid::Calibration::HandEyeResidual::rotation) @@ -20,6 +27,7 @@ namespace ZividPython void wrapClass(pybind11::class_ pyClass) { pyClass.def("valid", &Zivid::Calibration::HandEyeOutput::valid) + .def("status", &Zivid::Calibration::HandEyeOutput::status) .def( "transform", [](const Zivid::Calibration::HandEyeOutput &calibrationOutput) { diff --git a/src/Core/CameraAddress.cpp b/src/Core/CameraAddress.cpp new file mode 100644 index 00000000..d263ffd2 --- /dev/null +++ b/src/Core/CameraAddress.cpp @@ -0,0 +1,23 @@ +#include "ZividPython/CameraAddress.h" + +#include + +namespace py = pybind11; + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass) + { + pyClass + .def( + py::init(), + "Construct a CameraAddress from a hostname or IPv4 address string", + py::arg("value")) + .def("value", &Zivid::CameraAddress::value, "Get the address value") + .def("to_string", &Zivid::CameraAddress::toString, "Get a string representation of the address") + .def("__str__", &Zivid::CameraAddress::toString) + .def("__repr__", &Zivid::CameraAddress::toString) + .def("__eq__", &Zivid::CameraAddress::operator==, "Check if two addresses are equal") + .def("__ne__", &Zivid::CameraAddress::operator!=, "Check if two addresses are not equal"); + } +} // namespace ZividPython diff --git a/src/Core/FrameFileType.cpp b/src/Core/FrameFileType.cpp new file mode 100644 index 00000000..b51eb75d --- /dev/null +++ b/src/Core/FrameFileType.cpp @@ -0,0 +1,9 @@ +#include + +namespace ZividPython +{ + void wrapEnum(pybind11::enum_ pyEnum) + { + pyEnum.value("frame", Zivid::FrameFileType::frame).value("frame_2d", Zivid::FrameFileType::frame2D); + } +} // namespace ZividPython diff --git a/src/Core/ReleasableArray2D.cpp b/src/Core/ReleasableArray2D.cpp index f2505f01..fb220e3d 100644 --- a/src/Core/ReleasableArray2D.cpp +++ b/src/Core/ReleasableArray2D.cpp @@ -66,6 +66,17 @@ namespace pyClass.def(py::init<>(&pointCloudDataCopier)) .def_buffer(pointCloudDataBuffer); } + + template + void wrapThreeChannelImageClass(pybind11::class_> pyClass) + { + using WrapperType = uint8_t; + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + pyClass.def_buffer(pointCloudDataBuffer); + } } // namespace namespace ZividPython @@ -100,6 +111,26 @@ namespace ZividPython wrapImageClass(pyClass); } + void wrapClass(pybind11::class_> pyClass) + { + wrapThreeChannelImageClass(pyClass); + } + + void wrapClass(pybind11::class_> pyClass) + { + wrapThreeChannelImageClass(pyClass); + } + + void wrapClass(pybind11::class_> pyClass) + { + wrapThreeChannelImageClass(pyClass); + } + + void wrapClass(pybind11::class_> pyClass) + { + wrapThreeChannelImageClass(pyClass); + } + void wrapClass(pybind11::class_> pyClass) { using WrapperType = float; diff --git a/src/Core/ReleasableCamera.cpp b/src/Core/ReleasableCamera.cpp index ac6ad695..0708035a 100644 --- a/src/Core/ReleasableCamera.cpp +++ b/src/Core/ReleasableCamera.cpp @@ -38,6 +38,7 @@ namespace ZividPython .def_property_readonly("user_data", &ReleasableCamera::userData) .def_property_readonly("network_configuration", &ReleasableCamera::networkConfiguration) .def("apply_network_configuration", &ReleasableCamera::applyNetworkConfiguration) - .def("measure_scene_conditions", &ReleasableCamera::measureSceneConditions); + .def("measure_scene_conditions", &ReleasableCamera::measureSceneConditions) + .def("check_health", &ReleasableCamera::checkHealth); } } // namespace ZividPython diff --git a/src/Core/ReleasableComputeDevice.cpp b/src/Core/ReleasableComputeDevice.cpp new file mode 100644 index 00000000..0296f5d1 --- /dev/null +++ b/src/Core/ReleasableComputeDevice.cpp @@ -0,0 +1,155 @@ +#include + +#include + +namespace py = pybind11; + +namespace ZividPython +{ + void wrapEnum(pybind11::enum_ pyEnum) + { + pyEnum.value("cuda", Zivid::ComputeBackend::cuda).value("opencl", Zivid::ComputeBackend::opencl); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def(py::init<>()) + .def( + py::init( + [](std::uintptr_t context) { return Zivid::CUDAContextPtr{ reinterpret_cast(context) }; }), + py::arg("context")) + .def_property( + "context", + [](const Zivid::CUDAContextPtr &self) { return reinterpret_cast(self.context); }, + [](Zivid::CUDAContextPtr &self, std::uintptr_t value) { + self.context = reinterpret_cast(value); + }); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def(py::init<>()) + .def( + py::init( + [](std::uintptr_t stream) { return Zivid::CUDAStreamPtr{ reinterpret_cast(stream) }; }), + py::arg("stream")) + .def_property( + "stream", + [](const Zivid::CUDAStreamPtr &self) { return reinterpret_cast(self.stream); }, + [](Zivid::CUDAStreamPtr &self, std::uintptr_t value) { + self.stream = reinterpret_cast(value); + }); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def(py::init<>()) + .def( + py::init([](std::uintptr_t commandQueue) { + return Zivid::OpenCLCommandQueuePtr{ reinterpret_cast(commandQueue) }; + }), + py::arg("command_queue")) + .def_property( + "command_queue", + [](const Zivid::OpenCLCommandQueuePtr &self) { + return reinterpret_cast(self.commandQueue); + }, + [](Zivid::OpenCLCommandQueuePtr &self, std::uintptr_t value) { + self.commandQueue = reinterpret_cast(value); + }); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def(py::init(), py::arg("stream")) + .def(py::init(), py::arg("command_queue")) + .def_readwrite("stream", &Zivid::StreamOrQueue::stream) + .def_readwrite("command_queue", &Zivid::StreamOrQueue::commandQueue); + py::implicitly_convertible(); + py::implicitly_convertible(); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def_property_readonly("model", &ReleasableComputeDevice::model) + .def_property_readonly("vendor", &ReleasableComputeDevice::vendor) + .def_property_readonly("backend", &ReleasableComputeDevice::backend) + .def( + "native_context", + [](const ReleasableComputeDevice &self) { + return reinterpret_cast(self.nativeContext()); + }) + .def( + "native_stream_handle", + [](const ReleasableComputeDevice &self) { + return reinterpret_cast(self.nativeStreamHandle()); + }) + .def("sdk_stream_or_queue", &ReleasableComputeDevice::sdkStreamOrQueue) + .def( + "create_device_array_view_rgba", + &ReleasableComputeDevice::createDeviceArrayViewRGBA, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_bgra", + &ReleasableComputeDevice::createDeviceArrayViewBGRA, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_rgba_srgb", + &ReleasableComputeDevice::createDeviceArrayViewRGBA_SRGB, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_bgra_srgb", + &ReleasableComputeDevice::createDeviceArrayViewBGRA_SRGB, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_rgbaf", + &ReleasableComputeDevice::createDeviceArrayViewRGBAf, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_rgb", + &ReleasableComputeDevice::createDeviceArrayViewRGB, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_rgb_srgb", + &ReleasableComputeDevice::createDeviceArrayViewRGB_SRGB, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_bgr", + &ReleasableComputeDevice::createDeviceArrayViewBGR, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def( + "create_device_array_view_bgr_srgb", + &ReleasableComputeDevice::createDeviceArrayViewBGR_SRGB, + py::arg("cuda_pointer"), + py::arg("width"), + py::arg("height"), + py::arg("stream_or_queue")) + .def_property_readonly("cuda_runtime_library_name", &ReleasableComputeDevice::cudaRuntimeLibraryName) + .def("__str__", &ReleasableComputeDevice::toString) + .def("__repr__", &ReleasableComputeDevice::toString); + } +} // namespace ZividPython diff --git a/src/Core/ReleasableDeviceArrayView.cpp b/src/Core/ReleasableDeviceArrayView.cpp new file mode 100644 index 00000000..a6924865 --- /dev/null +++ b/src/Core/ReleasableDeviceArrayView.cpp @@ -0,0 +1,90 @@ +#include + +#include + +#include + +namespace ZividPython +{ + namespace + { + // Helper template to add common DeviceArrayView methods to a pybind11 class + template + void addCommonDeviceArrayViewMethods(pybind11::class_ &pyClass) + { + pyClass.def_property_readonly("shape", &ReleasableType::shape) + .def_property_readonly("strides", &ReleasableType::strides) + .def_property_readonly("strides_in_bytes", &ReleasableType::stridesInBytes) + .def_property_readonly("size_bytes", &ReleasableType::sizeInBytes) + .def_property_readonly("backend", &ReleasableType::backend) + .def_property_readonly("is_valid", &ReleasableType::isValid) + .def_property_readonly("is_empty", &ReleasableType::isEmpty) + .def("device_pointer", [](const ReleasableType &self) { + return reinterpret_cast(self.devicePointer()); + }); + } + } // namespace + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewRGBA::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewRGBA::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewBGRA::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewBGRA::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewRGBA_SRGB::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewRGBA_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewBGRA_SRGB::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewBGRA_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewRGBAf::toArray2D, pybind11::arg("stream_or_queue")); + // Note: Float format buffer does not have to_image() - use device_pointer() for GPU interop + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewRGB::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewRGB_SRGB::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewRGB_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewBGR::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewBGR::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayViewMethods(pyClass); + pyClass.def("to_array_2d", &ReleasableDeviceArrayViewBGR_SRGB::toArray2D, pybind11::arg("stream_or_queue")); + pyClass.def("to_image", &ReleasableDeviceArrayViewBGR_SRGB::toImage, pybind11::arg("stream_or_queue")); + } +} // namespace ZividPython diff --git a/src/Core/ReleasableFrame.cpp b/src/Core/ReleasableFrame.cpp index 9a36393e..118ab1eb 100644 --- a/src/Core/ReleasableFrame.cpp +++ b/src/Core/ReleasableFrame.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -18,6 +19,26 @@ namespace ZividPython .def_property_readonly("camera_info", &ReleasableFrame::cameraInfo) .def("point_cloud", &ReleasableFrame::pointCloud) .def("frame_2d", &ReleasableFrame::frame2D) - .def("clone", &ReleasableFrame::clone); + .def("clone", &ReleasableFrame::clone) + .def( + "mask", + &ReleasableFrame::mask, + "Apply a binary mask to the frame's point cloud in-place", + py::arg("mask")) + .def( + "mask", + [](ReleasableFrame &frame, const ReleasableMask &mask) { frame.mask(mask.impl()); }, + "Apply a binary mask to the frame's point cloud in-place", + py::arg("mask")) + .def( + "masked", + &ReleasableFrame::masked, + "Get a copy of the frame with a binary mask applied to its point cloud", + py::arg("mask")) + .def( + "masked", + [](ReleasableFrame &frame, const ReleasableMask &mask) { return frame.masked(mask.impl()); }, + "Get a copy of the frame with a binary mask applied to its point cloud", + py::arg("mask")); } } // namespace ZividPython diff --git a/src/Core/ReleasableFrame2D.cpp b/src/Core/ReleasableFrame2D.cpp index 34981007..d79c97fd 100644 --- a/src/Core/ReleasableFrame2D.cpp +++ b/src/Core/ReleasableFrame2D.cpp @@ -8,7 +8,11 @@ namespace ZividPython { void wrapClass(pybind11::class_ pyClass) { - pyClass.def_property_readonly("settings", &ReleasableFrame2D::settings) + pyClass.def(py::init()) + .def(py::init(), py::arg("file_name")) + .def("save", &ReleasableFrame2D::save, py::arg("file_name")) + .def("load", &ReleasableFrame2D::load, py::arg("file_name")) + .def_property_readonly("settings", &ReleasableFrame2D::settings) .def_property_readonly("state", &ReleasableFrame2D::state) .def_property_readonly("info", &ReleasableFrame2D::info) .def_property_readonly("camera_info", &ReleasableFrame2D::cameraInfo) @@ -16,6 +20,72 @@ namespace ZividPython .def("image_bgra", &ReleasableFrame2D::imageBGRA) .def("image_rgba_srgb", &ReleasableFrame2D::imageRGBA_SRGB) .def("image_bgra_srgb", &ReleasableFrame2D::imageBGRA_SRGB) - .def("clone", &ReleasableFrame2D::clone); + .def("image_rgb", &ReleasableFrame2D::imageRGB) + .def("image_rgb_srgb", &ReleasableFrame2D::imageRGB_SRGB) + .def("image_bgr", &ReleasableFrame2D::imageBGR) + .def("image_bgr_srgb", &ReleasableFrame2D::imageBGR_SRGB) + .def("clone", &ReleasableFrame2D::clone) + .def("image_device_array_rgba", &ReleasableFrame2D::imageDeviceArrayRGBA, py::arg("stream_or_queue")) + .def("image_device_array_bgra", &ReleasableFrame2D::imageDeviceArrayBGRA, py::arg("stream_or_queue")) + .def( + "image_device_array_rgba_srgb", + &ReleasableFrame2D::imageDeviceArrayRGBA_SRGB, + py::arg("stream_or_queue")) + .def( + "image_device_array_bgra_srgb", + &ReleasableFrame2D::imageDeviceArrayBGRA_SRGB, + py::arg("stream_or_queue")) + .def("image_device_array_rgba_float", &ReleasableFrame2D::imageDeviceArrayRGBAf, py::arg("stream_or_queue")) + .def("image_device_array_rgb", &ReleasableFrame2D::imageDeviceArrayRGB, py::arg("stream_or_queue")) + .def( + "image_device_array_rgb_srgb", &ReleasableFrame2D::imageDeviceArrayRGB_SRGB, py::arg("stream_or_queue")) + .def("image_device_array_bgr", &ReleasableFrame2D::imageDeviceArrayBGR, py::arg("stream_or_queue")) + .def( + "image_device_array_bgr_srgb", &ReleasableFrame2D::imageDeviceArrayBGR_SRGB, py::arg("stream_or_queue")) + .def( + "image_device_array_rgba_fill", + &ReleasableFrame2D::imageDeviceArrayRGBAFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_bgra_fill", + &ReleasableFrame2D::imageDeviceArrayBGRAFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_rgba_srgb_fill", + &ReleasableFrame2D::imageDeviceArrayRGBA_SRGBFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_bgra_srgb_fill", + &ReleasableFrame2D::imageDeviceArrayBGRA_SRGBFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_rgba_float_fill", + &ReleasableFrame2D::imageDeviceArrayRGBAfFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_rgb_fill", + &ReleasableFrame2D::imageDeviceArrayRGBFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_rgb_srgb_fill", + &ReleasableFrame2D::imageDeviceArrayRGB_SRGBFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_bgr_fill", + &ReleasableFrame2D::imageDeviceArrayBGRFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")) + .def( + "image_device_array_bgr_srgb_fill", + &ReleasableFrame2D::imageDeviceArrayBGR_SRGBFill, + py::arg("stream_or_queue"), + py::arg("destination_buffer")); } } // namespace ZividPython diff --git a/src/Core/ReleasableImage.cpp b/src/Core/ReleasableImage.cpp index 0ad64471..f2749c5d 100644 --- a/src/Core/ReleasableImage.cpp +++ b/src/Core/ReleasableImage.cpp @@ -55,6 +55,52 @@ namespace { return imageDataBuffer(image); } + + template + py::buffer_info imageData3ChannelBuffer(ImageType &image) + { + using WrapperType = uint8_t; + + constexpr py::ssize_t dim = 3; + constexpr py::ssize_t depth = 3; + constexpr py::ssize_t dataSize = sizeof(WrapperType); + + static_assert(dataSize * depth == sizeof(NativeType)); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + + auto *dataPtr = static_cast(const_cast(image.impl().data())); + const auto height = static_cast(image.impl().height()); + const auto width = static_cast(image.impl().width()); + + return py::buffer_info{ dataPtr, + dataSize, + py::format_descriptor::format(), + dim, + { height, width, depth }, + { dataSize * width * depth, dataSize * depth, dataSize } }; + } + + py::buffer_info imageRGBDataBuffer(ZividPython::ReleasableImageRGB &image) + { + return imageData3ChannelBuffer(image); + } + + py::buffer_info imageRGB_SRGBDataBuffer(ZividPython::ReleasableImageRGB_SRGB &image) + { + return imageData3ChannelBuffer(image); + } + + py::buffer_info imageBGRDataBuffer(ZividPython::ReleasableImageBGR &image) + { + return imageData3ChannelBuffer(image); + } + + py::buffer_info imageBGR_SRGBDataBuffer(ZividPython::ReleasableImageBGR_SRGB &image) + { + return imageData3ChannelBuffer(image); + } } // namespace namespace ZividPython @@ -94,4 +140,40 @@ namespace ZividPython .def("height", &ReleasableImageBGRA_SRGB::height) .def(py::init(), "Load image from file"); } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def_buffer(imageRGBDataBuffer) + .def("save", &ReleasableImageRGB::save, py::arg("file_name")) + .def("width", &ReleasableImageRGB::width) + .def("height", &ReleasableImageRGB::height) + .def(py::init(), "Load image from file"); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def_buffer(imageRGB_SRGBDataBuffer) + .def("save", &ReleasableImageRGB_SRGB::save, py::arg("file_name")) + .def("width", &ReleasableImageRGB_SRGB::width) + .def("height", &ReleasableImageRGB_SRGB::height) + .def(py::init(), "Load image from file"); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def_buffer(imageBGRDataBuffer) + .def("save", &ReleasableImageBGR::save, py::arg("file_name")) + .def("width", &ReleasableImageBGR::width) + .def("height", &ReleasableImageBGR::height) + .def(py::init(), "Load image from file"); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def_buffer(imageBGR_SRGBDataBuffer) + .def("save", &ReleasableImageBGR_SRGB::save, py::arg("file_name")) + .def("width", &ReleasableImageBGR_SRGB::width) + .def("height", &ReleasableImageBGR_SRGB::height) + .def(py::init(), "Load image from file"); + } } // namespace ZividPython diff --git a/src/Core/ReleasableImageDeviceArray.cpp b/src/Core/ReleasableImageDeviceArray.cpp new file mode 100644 index 00000000..86da14a6 --- /dev/null +++ b/src/Core/ReleasableImageDeviceArray.cpp @@ -0,0 +1,81 @@ +#include + +#include + +namespace ZividPython +{ + namespace + { + // Helper template to add common ImageDeviceArray methods to a pybind11 class + template + void addCommonImageDeviceArrayMethods(pybind11::class_ &pyClass) + { + pyClass.def_property_readonly("shape", &ReleasableType::shape) + .def_property_readonly("strides", &ReleasableType::strides) + .def_property_readonly("strides_in_bytes", &ReleasableType::stridesInBytes) + .def_property_readonly("size_bytes", &ReleasableType::sizeInBytes) + .def_property_readonly("backend", &ReleasableType::backend) + .def_property_readonly("is_valid", &ReleasableType::isValid) + .def_property_readonly("is_empty", &ReleasableType::isEmpty) + .def("to_array_2d", &ReleasableType::toArray2D, pybind11::arg("stream_or_queue")) + .def("to_array_1d", &ReleasableType::toArray1D, pybind11::arg("stream_or_queue")) + .def("device_pointer", [](const ReleasableType &self) { + return reinterpret_cast(self.devicePointer()); + }); + } + } // namespace + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayRGBA::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayBGRA::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayRGBA_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayBGRA_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + // Note: Float format buffer does not have toImage() - use device_pointer() for GPU interop + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayRGB_SRGB::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayBGR::toImage, pybind11::arg("stream_or_queue")); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonImageDeviceArrayMethods(pyClass); + pyClass.def("to_image", &ReleasableImageDeviceArrayBGR_SRGB::toImage, pybind11::arg("stream_or_queue")); + } +} // namespace ZividPython diff --git a/src/Core/ReleasableMask.cpp b/src/Core/ReleasableMask.cpp new file mode 100644 index 00000000..7234f77b --- /dev/null +++ b/src/Core/ReleasableMask.cpp @@ -0,0 +1,61 @@ +#include + +#include + +#include + +namespace py = pybind11; + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def(py::init<>(), "Create an empty mask") + .def( + py::init(), + "Create a zero-filled mask with the specified resolution", + py::arg("resolution")) + .def( + py::init(), + "Create a mask from raw data", + py::arg("resolution"), + py::arg("data_begin"), + py::arg("data_end")) + .def( + py::init([](const Zivid::Resolution &resolution, py::bytes data) { + py::buffer_info info(py::buffer(data).request()); + if(info.format != py::format_descriptor::format()) + { + throw std::invalid_argument("Data must be uint8 bytes"); + } + return ReleasableMask( + Zivid::Mask( + resolution, + static_cast(info.ptr), + static_cast(info.ptr) + info.size)); + }), + "Create a mask from Python bytes data", + py::arg("resolution"), + py::arg("data")) + .def("width", &ReleasableMask::width, "Get the width of the mask") + .def("height", &ReleasableMask::height, "Get the height of the mask") + .def("size", &ReleasableMask::size, "Get the size of the mask (width * height)") + .def("resolution", &ReleasableMask::resolution, "Get the resolution of the mask") + .def("to_string", &ReleasableMask::toString, "Get string representation of the mask") + .def( + "impl", + static_cast(&ReleasableMask::impl), + "Get the underlying native Mask implementation", + py::return_value_policy::reference_internal) + .def_buffer([](ReleasableMask &mask) -> py::buffer_info { + return py::buffer_info( + const_cast(mask.impl().data()), // Pointer to buffer + sizeof(uint8_t), // Size of one scalar + py::format_descriptor::format(), // Python struct-style format descriptor + 2, // Number of dimensions + { mask.impl().height(), mask.impl().width() }, // Buffer dimensions + { sizeof(uint8_t) * mask.impl().width(), sizeof(uint8_t) } // Strides (in bytes) for each index + ); + }); + } +} // namespace ZividPython diff --git a/src/Core/ReleasablePointCloud.cpp b/src/Core/ReleasablePointCloud.cpp index 7112ea20..1ffcb747 100644 --- a/src/Core/ReleasablePointCloud.cpp +++ b/src/Core/ReleasablePointCloud.cpp @@ -1,7 +1,9 @@ #include +#include #include #include +#include #include @@ -35,12 +37,44 @@ namespace ZividPython [](ReleasablePointCloud &pointCloud, Zivid::PointCloud::Downsampling downsampling) { return pointCloud.downsampled(downsampling); }) + .def( + "mask_by_region_of_interest", + [](ReleasablePointCloud &pointCloud, const Zivid::Settings::RegionOfInterest::Box &roiSettings) { + pointCloud.maskByRegionOfInterest(roiSettings); + }) + .def( + "masked_by_region_of_interest", + [](ReleasablePointCloud &pointCloud, const Zivid::Settings::RegionOfInterest::Box &roiSettings) { + return pointCloud.maskedByRegionOfInterest(roiSettings); + }) + .def("mask", [](ReleasablePointCloud &pointCloud, const Zivid::Mask &mask) { pointCloud.mask(mask); }) + .def( + "mask", + [](ReleasablePointCloud &pointCloud, const ReleasableMask &mask) { pointCloud.mask(mask.impl()); }) + .def( + "masked", + [](ReleasablePointCloud &pointCloud, const Zivid::Mask &mask) { return pointCloud.masked(mask); }) + .def( + "masked", + [](ReleasablePointCloud &pointCloud, const ReleasableMask &mask) { + return pointCloud.masked(mask.impl()); + }) .def("copy_image_rgba", &ReleasablePointCloud::copyImageRGBA) .def("copy_image_bgra", &ReleasablePointCloud::copyImageBGRA) .def("copy_image_rgba_srgb", &ReleasablePointCloud::copyImageRGBA_SRGB) .def("copy_image_bgra_srgb", &ReleasablePointCloud::copyImageBGRA_SRGB) .def("to_unorganized_point_cloud", &ReleasablePointCloud::toUnorganizedPointCloud) - .def("clone", &ReleasablePointCloud::clone); + .def("clone", &ReleasablePointCloud::clone) + .def("device_points_xyz", &ReleasablePointCloud::devicePointsXYZ, py::arg("stream_or_queue")) + .def("device_points_xyzw", &ReleasablePointCloud::devicePointsXYZW, py::arg("stream_or_queue")) + .def("device_points_z", &ReleasablePointCloud::devicePointsZ, py::arg("stream_or_queue")) + .def("device_snrs", &ReleasablePointCloud::deviceSNRs, py::arg("stream_or_queue")) + .def("device_normals_xyz", &ReleasablePointCloud::deviceNormalsXYZ, py::arg("stream_or_queue")) + .def("device_image_rgba", &ReleasablePointCloud::deviceImageRGBA, py::arg("stream_or_queue")) + .def("device_image_rgba_srgb", &ReleasablePointCloud::deviceImageRGBA_SRGB, py::arg("stream_or_queue")) + .def("device_image_bgra", &ReleasablePointCloud::deviceImageBGRA, py::arg("stream_or_queue")) + .def("device_image_bgra_srgb", &ReleasablePointCloud::deviceImageBGRA_SRGB, py::arg("stream_or_queue")) + .def("device_image_rgbaf", &ReleasablePointCloud::deviceImageRGBAf, py::arg("stream_or_queue")); py::enum_{ pyClass, "Downsampling" } .value("by2x2", Zivid::PointCloud::Downsampling::by2x2) diff --git a/src/Core/ReleasablePointCloudDeviceArray.cpp b/src/Core/ReleasablePointCloudDeviceArray.cpp new file mode 100644 index 00000000..fdf885f0 --- /dev/null +++ b/src/Core/ReleasablePointCloudDeviceArray.cpp @@ -0,0 +1,52 @@ +#include + +#include + +namespace ZividPython +{ + namespace + { + // Helper template to add common DeviceArray methods to a pybind11 class + template + void addCommonDeviceArrayMethods(pybind11::class_ &pyClass) + { + pyClass.def_property_readonly("shape", &ReleasableType::shape) + .def_property_readonly("strides", &ReleasableType::strides) + .def_property_readonly("strides_in_bytes", &ReleasableType::stridesInBytes) + .def_property_readonly("size_bytes", &ReleasableType::sizeInBytes) + .def_property_readonly("backend", &ReleasableType::backend) + .def_property_readonly("is_valid", &ReleasableType::isValid) + .def_property_readonly("is_empty", &ReleasableType::isEmpty) + .def("to_array_2d", &ReleasableType::toArray2D, pybind11::arg("stream_or_queue")) + .def("to_array_1d", &ReleasableType::toArray1D, pybind11::arg("stream_or_queue")) + .def("device_pointer", [](const ReleasableType &self) { + return reinterpret_cast(self.devicePointer()); + }); + } + } // namespace + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayMethods(pyClass); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayMethods(pyClass); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayMethods(pyClass); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayMethods(pyClass); + } + + void wrapClass(pybind11::class_ pyClass) + { + addCommonDeviceArrayMethods(pyClass); + } +} // namespace ZividPython diff --git a/src/Core/ReleasableUnorganizedPointCloud.cpp b/src/Core/ReleasableUnorganizedPointCloud.cpp index 4e5141da..e4112315 100644 --- a/src/Core/ReleasableUnorganizedPointCloud.cpp +++ b/src/Core/ReleasableUnorganizedPointCloud.cpp @@ -121,6 +121,22 @@ namespace ZividPython const auto color = toCppColor(pyColor); return pointCloud.paintedUniformColor(Zivid::ColorRGBA{ color[0], color[1], color[2], color[3] }); }) - .def("clone", &ReleasableUnorganizedPointCloud::clone); + .def("clone", &ReleasableUnorganizedPointCloud::clone) + .def("device_array_xyz", &ReleasableUnorganizedPointCloud::deviceArrayPointXYZ, py::arg("stream_or_queue")) + .def( + "device_array_xyzw", &ReleasableUnorganizedPointCloud::deviceArrayPointXYZW, py::arg("stream_or_queue")) + .def( + "device_array_rgba", &ReleasableUnorganizedPointCloud::deviceArrayColorRGBA, py::arg("stream_or_queue")) + .def( + "device_array_bgra", &ReleasableUnorganizedPointCloud::deviceArrayColorBGRA, py::arg("stream_or_queue")) + .def( + "device_array_rgba_srgb", + &ReleasableUnorganizedPointCloud::deviceArrayColorRGBA_SRGB, + py::arg("stream_or_queue")) + .def( + "device_array_bgra_srgb", + &ReleasableUnorganizedPointCloud::deviceArrayColorBGRA_SRGB, + py::arg("stream_or_queue")) + .def("device_array_snr", &ReleasableUnorganizedPointCloud::deviceArraySNR, py::arg("stream_or_queue")); } } // namespace ZividPython diff --git a/src/Core/Resolution.cpp b/src/Core/Resolution.cpp new file mode 100644 index 00000000..191f781d --- /dev/null +++ b/src/Core/Resolution.cpp @@ -0,0 +1,26 @@ +#include "ZividPython/Resolution.h" + +#include + +namespace py = pybind11; + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass) + { + pyClass + .def( + py::init(), + "Construct a Resolution from width and height", + py::arg("width"), + py::arg("height")) + .def("width", &Zivid::Resolution::width, "Get the width value of the resolution") + .def("height", &Zivid::Resolution::height, "Get the height value of the resolution") + .def("size", &Zivid::Resolution::size, "Get the size (area) that is the product of the width and height") + .def("to_string", &Zivid::Resolution::toString, "Get a string representation of the resolution") + .def("__str__", &Zivid::Resolution::toString) + .def("__repr__", &Zivid::Resolution::toString) + .def("__eq__", &Zivid::Resolution::operator==, "Check if two resolutions are equal") + .def("__ne__", &Zivid::Resolution::operator!=, "Check if two resolutions are not equal"); + } +} // namespace ZividPython diff --git a/src/Core/SingletonApplication.cpp b/src/Core/SingletonApplication.cpp index 069eb1dc..ea6bac7d 100644 --- a/src/Core/SingletonApplication.cpp +++ b/src/Core/SingletonApplication.cpp @@ -1,6 +1,9 @@ +#include #include +#include #include +#include #include #include @@ -15,9 +18,28 @@ namespace ZividPython .def(py::init([] { // This method constructs a Zivid::Application and identifies the wrapper as the Zivid Python wrapper. // For users of the SDK: please do not use this method and construct the Zivid::Application directly instead. - return SingletonApplication{ Zivid::Detail::createApplicationForWrapper, + return SingletonApplication{ [](Zivid::Detail::EnvironmentInfo::Wrapper wrapper) { + return Zivid::Detail::createApplicationForWrapper(wrapper); + }, Zivid::Detail::EnvironmentInfo::Wrapper::python }; })) + .def( + py::init([](Zivid::CUDAContextPtr cudaContext) { + // Construct Application with user-provided CUDA context + return SingletonApplication{ + [](Zivid::Detail::EnvironmentInfo::Wrapper wrapper, + Zivid::OpenCLContextPtr openclContext, + Zivid::CUDAContextPtr cudaCtx) { + return Zivid::Detail::createApplicationForWrapper(wrapper, openclContext, cudaCtx); + }, + Zivid::Detail::EnvironmentInfo::Wrapper::python, + Zivid::OpenCLContextPtr{}, + cudaContext + }; + }), + py::arg("cuda_context"), + "Construct Application with a user-provided CUDA context. " + "Use this to ensure Zivid uses the same CUDA context as your application.") .def("cameras", &SingletonApplication::cameras) .def("connect_camera", [](SingletonApplication &application) { return application.connectCamera(); }) .def( @@ -26,6 +48,20 @@ namespace ZividPython return application.connectCamera(Zivid::CameraInfo::SerialNumber{ serialNumber }); }, py::arg("serial_number")) - .def("create_file_camera", &SingletonApplication::createFileCamera, py::arg("frame_file")); + .def( + "connect_camera", + [](SingletonApplication &application, const Zivid::CameraAddress &address) { + return application.connectCamera(address); + }, + py::arg("address")) + .def( + "create_file_camera", + py::overload_cast(&SingletonApplication::createFileCamera), + py::arg("frame_file")) + .def( + "create_file_camera", + py::overload_cast(&SingletonApplication::createFileCamera), + py::arg("frame")) + .def("compute_device", &SingletonApplication::computeDevice); } } // namespace ZividPython diff --git a/src/Core/Toolbox/Barcode.cpp b/src/Core/Toolbox/Barcode.cpp index 3780487b..af927ce4 100644 --- a/src/Core/Toolbox/Barcode.cpp +++ b/src/Core/Toolbox/Barcode.cpp @@ -29,7 +29,8 @@ namespace ZividPython .value("ean13", LinearBarcodeFormat::ean13) .value("ean8", LinearBarcodeFormat::ean8) .value("upcA", LinearBarcodeFormat::upcA) - .value("upcE", LinearBarcodeFormat::upcE); + .value("upcE", LinearBarcodeFormat::upcE) + .value("itf", LinearBarcodeFormat::itf); } void wrapEnum(pybind11::enum_ pyEnum) @@ -46,6 +47,26 @@ namespace ZividPython return detector.suggestSettings(camera.impl()); }, py::arg("camera")) + .def( + "detect_linear_codes", + [](ReleasableBarcodeDetector &detector, const ReleasableFrame2D &frame2d) { + return detector.detectLinearCodes(frame2d.impl()); + }, + py::arg("frame2d")) + .def( + "decode_linear_codes", + [](ReleasableBarcodeDetector &detector, + const std::vector &detectionResults, + const std::set &formats) { + const auto filter = formatSetToFilter(formats); + if(filter.has_value()) + { + return detector.decodeLinearCodes(detectionResults, filter.value()); + } + return detector.decodeLinearCodes(detectionResults, LinearBarcodeFormatFilter::all()); + }, + py::arg("detection_results"), + py::arg("formats")) .def( "read_linear_codes", [](ReleasableBarcodeDetector &detector, @@ -78,19 +99,36 @@ namespace ZividPython void wrapClass(pybind11::class_ pyClass) { - pyClass.def("code", &LinearBarcodeDetectionResult::code) - .def("code_format", [](LinearBarcodeDetectionResult &result) { return toString(result.codeFormat()); }) - .def("center_position", [](LinearBarcodeDetectionResult &result) { - return std::array{ result.centerPosition().x, result.centerPosition().y }; - }); + pyClass + .def( + "center_position", + [](LinearBarcodeDetectionResult &result) { + return std::array{ result.centerPosition().x, result.centerPosition().y }; + }) + .def("bounding_box", &LinearBarcodeDetectionResult::boundingBox); + } + + void wrapClass(pybind11::class_ pyClass) + { + pyClass.def("code", &LinearBarcodeDecodingResult::code) + .def("code_format", [](LinearBarcodeDecodingResult &result) { return toString(result.codeFormat()); }) + .def( + "center_position", + [](LinearBarcodeDecodingResult &result) { + return std::array{ result.centerPosition().x, result.centerPosition().y }; + }) + .def("bounding_box", &LinearBarcodeDecodingResult::boundingBox); } - void wrapClass(pybind11::class_ pyClass) + void wrapClass(pybind11::class_ pyClass) { - pyClass.def("code", &MatrixBarcodeDetectionResult::code) - .def("code_format", [](MatrixBarcodeDetectionResult &result) { return toString(result.codeFormat()); }) - .def("center_position", [](MatrixBarcodeDetectionResult &result) { - return std::array{ result.centerPosition().x, result.centerPosition().y }; - }); + pyClass.def("code", &MatrixBarcodeDecodingResult::code) + .def("code_format", [](MatrixBarcodeDecodingResult &result) { return toString(result.codeFormat()); }) + .def( + "center_position", + [](MatrixBarcodeDecodingResult &result) { + return std::array{ result.centerPosition().x, result.centerPosition().y }; + }) + .def("bounding_box", &MatrixBarcodeDecodingResult::boundingBox); } } // namespace ZividPython diff --git a/src/Core/Toolbox/Toolbox.cpp b/src/Core/Toolbox/Toolbox.cpp index bd2ff0ed..1c8755b5 100644 --- a/src/Core/Toolbox/Toolbox.cpp +++ b/src/Core/Toolbox/Toolbox.cpp @@ -35,7 +35,8 @@ namespace ZividPython::Toolbox ZIVID_PYTHON_WRAP_ENUM_CLASS(dest, LinearBarcodeFormat); ZIVID_PYTHON_WRAP_ENUM_CLASS(dest, MatrixBarcodeFormat); ZIVID_PYTHON_WRAP_CLASS(dest, LinearBarcodeDetectionResult); - ZIVID_PYTHON_WRAP_CLASS(dest, MatrixBarcodeDetectionResult); + ZIVID_PYTHON_WRAP_CLASS(dest, LinearBarcodeDecodingResult); + ZIVID_PYTHON_WRAP_CLASS(dest, MatrixBarcodeDecodingResult); ZIVID_PYTHON_WRAP_CLASS_AS_RELEASABLE(dest, BarcodeDetector); } } // namespace ZividPython::Toolbox diff --git a/src/Core/Wrapper.cpp b/src/Core/Wrapper.cpp index 005b20eb..eb71eeec 100644 --- a/src/Core/Wrapper.cpp +++ b/src/Core/Wrapper.cpp @@ -1,9 +1,10 @@ -#include - #include +#include #include +#include #include +#include #include #include #include @@ -16,11 +17,17 @@ #include #include #include +#include +#include #include #include +#include +#include #include +#include #include #include +#include #include #include #include @@ -31,16 +38,146 @@ #include #include +#include "Zivid/CameraIntrinsics.h" + ZIVID_PYTHON_MODULE // NOLINT { module.attr("__version__") = pybind11::str(ZIVID_PYTHON_VERSION); using namespace Zivid; + ZIVID_PYTHON_WRAP_ENUM_CLASS(module, FrameFileType); + module.def("read_frame_file_type", &Zivid::readFrameFileType, py::arg("file_name")); + + // GPU compute device enums + ZIVID_PYTHON_WRAP_ENUM_CLASS(module, ComputeBackend); + + // GPU context/stream/queue structs - manually registered (no toString method) + { + auto cudaContextPtr = pybind11::class_(module, "CUDAContextPtr"); + ZividPython::wrapClass(cudaContextPtr); + + auto cudaStreamPtr = pybind11::class_(module, "CUDAStreamPtr"); + ZividPython::wrapClass(cudaStreamPtr); + + auto openCLCommandQueuePtr = pybind11::class_(module, "OpenCLCommandQueuePtr"); + ZividPython::wrapClass(openCLCommandQueuePtr); + + auto streamOrQueue = pybind11::class_(module, "StreamOrQueue"); + ZividPython::wrapClass(streamOrQueue); + + module.def( + "synchronize_stream", + &Zivid::synchronizeStream, + pybind11::arg("stream_or_queue"), + "Block the host thread until all work previously enqueued on stream_or_queue has completed. " + "Equivalent to cudaStreamSynchronize on CUDA builds and clFinish on OpenCL builds. " + "Use this after the sync-free DeviceArray host accessors (copy_to_host_organized_array, " + "copy_to_host_unorganized_array, to_image) to make their results safe to read."); + } + + // GPU compute device and image device buffer classes - manually registered (no toString method) + { + auto computeDevice = pybind11::class_(module, "ComputeDevice"); + ZividPython::wrapClass(computeDevice); + + auto imageDeviceArrayRGBA = + pybind11::class_(module, "ImageDeviceArrayRGBA"); + ZividPython::wrapClass(imageDeviceArrayRGBA); + + auto imageDeviceArrayBGRA = + pybind11::class_(module, "ImageDeviceArrayBGRA"); + ZividPython::wrapClass(imageDeviceArrayBGRA); + + auto imageDeviceArrayRGBA_SRGB = + pybind11::class_(module, "ImageDeviceArrayRGBA_SRGB"); + ZividPython::wrapClass(imageDeviceArrayRGBA_SRGB); + + auto imageDeviceArrayBGRA_SRGB = + pybind11::class_(module, "ImageDeviceArrayBGRA_SRGB"); + ZividPython::wrapClass(imageDeviceArrayBGRA_SRGB); + + auto imageDeviceArrayRGBAf = + pybind11::class_(module, "ImageDeviceArrayRGBAf"); + ZividPython::wrapClass(imageDeviceArrayRGBAf); + + auto imageDeviceArrayRGB = + pybind11::class_(module, "ImageDeviceArrayRGB"); + ZividPython::wrapClass(imageDeviceArrayRGB); + + auto imageDeviceArrayRGB_SRGB = + pybind11::class_(module, "ImageDeviceArrayRGB_SRGB"); + ZividPython::wrapClass(imageDeviceArrayRGB_SRGB); + + auto imageDeviceArrayBGR = + pybind11::class_(module, "ImageDeviceArrayBGR"); + ZividPython::wrapClass(imageDeviceArrayBGR); + + auto imageDeviceArrayBGR_SRGB = + pybind11::class_(module, "ImageDeviceArrayBGR_SRGB"); + ZividPython::wrapClass(imageDeviceArrayBGR_SRGB); + + auto deviceArrayViewRGBA = + pybind11::class_(module, "DeviceArrayViewRGBA"); + ZividPython::wrapClass(deviceArrayViewRGBA); + + auto deviceArrayViewBGRA = + pybind11::class_(module, "DeviceArrayViewBGRA"); + ZividPython::wrapClass(deviceArrayViewBGRA); + + auto deviceArrayViewRGBA_SRGB = + pybind11::class_(module, "DeviceArrayViewRGBA_SRGB"); + ZividPython::wrapClass(deviceArrayViewRGBA_SRGB); + + auto deviceArrayViewBGRA_SRGB = + pybind11::class_(module, "DeviceArrayViewBGRA_SRGB"); + ZividPython::wrapClass(deviceArrayViewBGRA_SRGB); + + auto deviceArrayViewRGBAf = + pybind11::class_(module, "DeviceArrayViewRGBAf"); + ZividPython::wrapClass(deviceArrayViewRGBAf); + + auto deviceArrayViewRGB = + pybind11::class_(module, "DeviceArrayViewRGB"); + ZividPython::wrapClass(deviceArrayViewRGB); + + auto deviceArrayViewRGB_SRGB = + pybind11::class_(module, "DeviceArrayViewRGB_SRGB"); + ZividPython::wrapClass(deviceArrayViewRGB_SRGB); + + auto deviceArrayViewBGR = + pybind11::class_(module, "DeviceArrayViewBGR"); + ZividPython::wrapClass(deviceArrayViewBGR); + + auto deviceArrayViewBGR_SRGB = + pybind11::class_(module, "DeviceArrayViewBGR_SRGB"); + ZividPython::wrapClass(deviceArrayViewBGR_SRGB); + + auto deviceArrayPointXYZ = + pybind11::class_(module, "DeviceArrayPointXYZ"); + ZividPython::wrapClass(deviceArrayPointXYZ); + + auto deviceArrayPointXYZW = + pybind11::class_(module, "DeviceArrayPointXYZW"); + ZividPython::wrapClass(deviceArrayPointXYZW); + + auto deviceArrayPointZ = + pybind11::class_(module, "DeviceArrayPointZ"); + ZividPython::wrapClass(deviceArrayPointZ); + + auto deviceArraySNR = pybind11::class_(module, "DeviceArraySNR"); + ZividPython::wrapClass(deviceArraySNR); + + auto deviceArrayNormalXYZ = + pybind11::class_(module, "DeviceArrayNormalXYZ"); + ZividPython::wrapClass(deviceArrayNormalXYZ); + } + ZIVID_PYTHON_WRAP_NAMESPACE_AS_SUBMODULE(module, DataModel); ZIVID_PYTHON_WRAP_DATA_MODEL(module, Settings); ZIVID_PYTHON_WRAP_DATA_MODEL(module, Settings2D); + ZIVID_PYTHON_WRAP_DATA_MODEL(module, CameraHealth); ZIVID_PYTHON_WRAP_DATA_MODEL(module, CameraState); ZIVID_PYTHON_WRAP_DATA_MODEL(module, CameraInfo); ZIVID_PYTHON_WRAP_DATA_MODEL(module, FrameInfo); @@ -54,12 +191,20 @@ ZIVID_PYTHON_MODULE // NOLINT ZIVID_PYTHON_WRAP_CLASS_AS_RELEASABLE(module, Frame2D); ZIVID_PYTHON_WRAP_CLASS_AS_RELEASABLE(module, ProjectedImage); + ZIVID_PYTHON_WRAP_CLASS(module, BoundingBox); + ZIVID_PYTHON_WRAP_CLASS(module, CameraAddress); + ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, Mask); + ZIVID_PYTHON_WRAP_CLASS(module, Resolution); ZIVID_PYTHON_WRAP_CLASS_BUFFER(module, Matrix4x4); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageRGBA); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageBGRA); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageRGBA_SRGB); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageBGRA_SRGB); + ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageRGB); + ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageRGB_SRGB); + ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageBGR); + ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, ImageBGR_SRGB); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, PointCloud); ZIVID_PYTHON_WRAP_CLASS_BUFFER_AS_RELEASABLE(module, UnorganizedPointCloud); @@ -67,6 +212,10 @@ ZIVID_PYTHON_MODULE // NOLINT ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorBGRA); ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorRGBA_SRGB); ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorBGRA_SRGB); + ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorRGB); + ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorRGB_SRGB); + ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorBGR); + ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, ColorBGR_SRGB); ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, NormalXYZ); ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, PointXYZ); ZIVID_PYTHON_WRAP_ARRAY2D_BUFFER_AS_RELEASABLE(module, PointXYZW); diff --git a/src/Core/include/ZividPython/BoundingBox.h b/src/Core/include/ZividPython/BoundingBox.h new file mode 100644 index 00000000..fc15f8ac --- /dev/null +++ b/src/Core/include/ZividPython/BoundingBox.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass); +} diff --git a/src/Core/include/ZividPython/Calibration/HandEye.h b/src/Core/include/ZividPython/Calibration/HandEye.h index 6b2a7273..ee8944ee 100644 --- a/src/Core/include/ZividPython/Calibration/HandEye.h +++ b/src/Core/include/ZividPython/Calibration/HandEye.h @@ -7,6 +7,7 @@ namespace ZividPython { + void wrapEnum(pybind11::enum_ pyEnum); void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/CameraAddress.h b/src/Core/include/ZividPython/CameraAddress.h new file mode 100644 index 00000000..8785e3f6 --- /dev/null +++ b/src/Core/include/ZividPython/CameraAddress.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass); +} diff --git a/src/Core/include/ZividPython/FrameFileType.h b/src/Core/include/ZividPython/FrameFileType.h new file mode 100644 index 00000000..73db4a84 --- /dev/null +++ b/src/Core/include/ZividPython/FrameFileType.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + +namespace ZividPython +{ + void wrapEnum(pybind11::enum_ pyEnum); +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableArray2D.h b/src/Core/include/ZividPython/ReleasableArray2D.h index 41452e50..a5538121 100644 --- a/src/Core/include/ZividPython/ReleasableArray2D.h +++ b/src/Core/include/ZividPython/ReleasableArray2D.h @@ -16,6 +16,10 @@ namespace ZividPython void wrapClass(pybind11::class_> pyClass); void wrapClass(pybind11::class_> pyClass); void wrapClass(pybind11::class_> pyClass); + void wrapClass(pybind11::class_> pyClass); + void wrapClass(pybind11::class_> pyClass); + void wrapClass(pybind11::class_> pyClass); + void wrapClass(pybind11::class_> pyClass); void wrapClass(pybind11::class_> pyClass); void wrapClass(pybind11::class_> pyClass); void wrapClass(pybind11::class_> pyClass); diff --git a/src/Core/include/ZividPython/ReleasableCamera.h b/src/Core/include/ZividPython/ReleasableCamera.h index 32db12d1..3be3db74 100644 --- a/src/Core/include/ZividPython/ReleasableCamera.h +++ b/src/Core/include/ZividPython/ReleasableCamera.h @@ -7,8 +7,11 @@ #include #include +#include #include +#include + namespace ZividPython { class ReleasableCamera : public Releasable @@ -36,6 +39,7 @@ namespace ZividPython const Zivid::NetworkConfiguration &, networkConfiguration) ZIVID_PYTHON_FORWARD_0_ARGS(measureSceneConditions) + ZIVID_PYTHON_FORWARD_0_ARGS(checkHealth) }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/ReleasableComputeDevice.h b/src/Core/include/ZividPython/ReleasableComputeDevice.h new file mode 100644 index 00000000..f48453d7 --- /dev/null +++ b/src/Core/include/ZividPython/ReleasableComputeDevice.h @@ -0,0 +1,163 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace ZividPython +{ + class ReleasableComputeDevice : public Releasable + { + public: + using Releasable::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableComputeDevice) + + ZIVID_PYTHON_FORWARD_0_ARGS(model, const) + ZIVID_PYTHON_FORWARD_0_ARGS(vendor, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(nativeContext, const) + ZIVID_PYTHON_FORWARD_0_ARGS(nativeStreamHandle, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sdkStreamOrQueue, const) + ZIVID_PYTHON_FORWARD_0_ARGS(cudaRuntimeLibraryName, const) + ZIVID_PYTHON_FORWARD_0_ARGS(toString, const) + + ReleasableDeviceArrayViewRGBA createDeviceArrayViewRGBA( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewRGBA{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewBGRA createDeviceArrayViewBGRA( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewBGRA{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewRGBA_SRGB createDeviceArrayViewRGBA_SRGB( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewRGBA_SRGB{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewBGRA_SRGB createDeviceArrayViewBGRA_SRGB( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewBGRA_SRGB{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewRGBAf createDeviceArrayViewRGBAf( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewRGBAf{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewRGB createDeviceArrayViewRGB( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewRGB{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewRGB_SRGB createDeviceArrayViewRGB_SRGB( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewRGB_SRGB{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewBGR createDeviceArrayViewBGR( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewBGR{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + + ReleasableDeviceArrayViewBGR_SRGB createDeviceArrayViewBGR_SRGB( + std::uintptr_t cudaPointer, + size_t width, + size_t height, + const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayViewBGR_SRGB{ WITH_GIL_UNLOCKED( + impl().createDeviceArrayView( + Zivid::CUDADevicePointer{ reinterpret_cast(cudaPointer) }, + width, + height, + streamOrQueue.stream)) }; + } + }; + + void wrapEnum(pybind11::enum_ pyEnum); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableDeviceArrayView.h b/src/Core/include/ZividPython/ReleasableDeviceArrayView.h new file mode 100644 index 00000000..6d8fd17f --- /dev/null +++ b/src/Core/include/ZividPython/ReleasableDeviceArrayView.h @@ -0,0 +1,298 @@ +#pragma once + +#include + +#include "ZividPython/Releasable.h" +#include "ZividPython/ReleasableArray1D.h" +#include "ZividPython/ReleasableArray2D.h" +#include "ZividPython/ReleasableImage.h" +#include "ZividPython/Wrappers.h" + +namespace ZividPython +{ + // Releasable wrappers for non-owning DeviceArrayView over caller-owned GPU memory. + // Created via ComputeDevice.create_device_array_view_*, mirroring the owning ImageDeviceArray wrappers. + class ReleasableDeviceArrayViewRGBA : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewRGBA) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGBA toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGBA{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewBGRA : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewBGRA) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGRA toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGRA{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewRGBA_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewRGBA_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGBA_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGBA_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewBGRA_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewBGRA_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGRA_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGRA_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewRGBAf : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewRGBAf) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewRGB_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewRGB_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGB_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGB_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewBGR : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewBGR) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGR toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGR{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayViewBGR_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayViewBGR_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGR_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGR_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableFrame.h b/src/Core/include/ZividPython/ReleasableFrame.h index 0723020f..3a6f285c 100644 --- a/src/Core/include/ZividPython/ReleasableFrame.h +++ b/src/Core/include/ZividPython/ReleasableFrame.h @@ -33,6 +33,8 @@ namespace ZividPython ZIVID_PYTHON_FORWARD_0_ARGS(info) ZIVID_PYTHON_FORWARD_0_ARGS(cameraInfo) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableFrame, clone, const) + ZIVID_PYTHON_FORWARD_1_ARGS(mask, const Zivid::Mask &, mask, const) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableFrame, masked, const Zivid::Mask &, mask, const) }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/ReleasableFrame2D.h b/src/Core/include/ZividPython/ReleasableFrame2D.h index c0e26041..d3625c78 100644 --- a/src/Core/include/ZividPython/ReleasableFrame2D.h +++ b/src/Core/include/ZividPython/ReleasableFrame2D.h @@ -4,7 +4,9 @@ #include #include +#include #include +#include #include namespace ZividPython @@ -20,11 +22,134 @@ namespace ZividPython ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGRA, imageBGRA) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageRGBA_SRGB, imageRGBA_SRGB) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGRA_SRGB, imageBGRA_SRGB) + ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageRGB, imageRGB) + ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageRGB_SRGB, imageRGB_SRGB) + ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGR, imageBGR) + ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGR_SRGB, imageBGR_SRGB) ZIVID_PYTHON_FORWARD_0_ARGS(settings) ZIVID_PYTHON_FORWARD_0_ARGS(state) ZIVID_PYTHON_FORWARD_0_ARGS(info) ZIVID_PYTHON_FORWARD_0_ARGS(cameraInfo) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableFrame2D, clone, const) + ZIVID_PYTHON_FORWARD_1_ARGS(save, const std::string &, fileName) + ZIVID_PYTHON_FORWARD_1_ARGS(load, const std::string &, fileName) + + ReleasableImageDeviceArrayRGBA imageDeviceArrayRGBA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA imageDeviceArrayBGRA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBA_SRGB imageDeviceArrayRGBA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA_SRGB imageDeviceArrayBGRA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBAf imageDeviceArrayRGBAf(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBAf{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGB imageDeviceArrayRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGB_SRGB imageDeviceArrayRGB_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGB_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGR imageDeviceArrayBGR(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGR{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGR_SRGB imageDeviceArrayBGR_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGR_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + void imageDeviceArrayRGBAFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewRGBA &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayBGRAFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewBGRA &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayRGBA_SRGBFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewRGBA_SRGB &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayBGRA_SRGBFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewBGRA_SRGB &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayRGBAfFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewRGBAf &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayRGBFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewRGB &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayRGB_SRGBFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewRGB_SRGB &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayBGRFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewBGR &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } + + void imageDeviceArrayBGR_SRGBFill( + const Zivid::StreamOrQueue &streamOrQueue, + const ReleasableDeviceArrayViewBGR_SRGB &destinationBuffer) const + { + WITH_GIL_UNLOCKED(impl().imageDeviceArray(destinationBuffer.impl(), streamOrQueue)); + } }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/ReleasableImage.h b/src/Core/include/ZividPython/ReleasableImage.h index 7237ab22..b3524385 100644 --- a/src/Core/include/ZividPython/ReleasableImage.h +++ b/src/Core/include/ZividPython/ReleasableImage.h @@ -52,8 +52,60 @@ namespace ZividPython ZIVID_PYTHON_FORWARD_0_ARGS(height) }; + class ReleasableImageRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageRGB) + + ZIVID_PYTHON_FORWARD_1_ARGS(save, const std::string &, fileName) + ZIVID_PYTHON_FORWARD_0_ARGS(width) + ZIVID_PYTHON_FORWARD_0_ARGS(height) + }; + + class ReleasableImageRGB_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageRGB_SRGB) + + ZIVID_PYTHON_FORWARD_1_ARGS(save, const std::string &, fileName) + ZIVID_PYTHON_FORWARD_0_ARGS(width) + ZIVID_PYTHON_FORWARD_0_ARGS(height) + }; + + class ReleasableImageBGR : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageBGR) + + ZIVID_PYTHON_FORWARD_1_ARGS(save, const std::string &, fileName) + ZIVID_PYTHON_FORWARD_0_ARGS(width) + ZIVID_PYTHON_FORWARD_0_ARGS(height) + }; + + class ReleasableImageBGR_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageBGR_SRGB) + + ZIVID_PYTHON_FORWARD_1_ARGS(save, const std::string &, fileName) + ZIVID_PYTHON_FORWARD_0_ARGS(width) + ZIVID_PYTHON_FORWARD_0_ARGS(height) + }; + void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); } // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableImageDeviceArray.h b/src/Core/include/ZividPython/ReleasableImageDeviceArray.h new file mode 100644 index 00000000..8839a980 --- /dev/null +++ b/src/Core/include/ZividPython/ReleasableImageDeviceArray.h @@ -0,0 +1,348 @@ +#pragma once + +#include + +#include "ZividPython/Releasable.h" +#include "ZividPython/ReleasableArray1D.h" +#include "ZividPython/ReleasableArray2D.h" +#include "ZividPython/ReleasableImage.h" +#include "ZividPython/Wrappers.h" + +namespace ZividPython +{ + // Releasable wrappers for each ImageDeviceArray type + class ReleasableImageDeviceArrayRGBA : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayRGBA) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGBA toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGBA{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableImageDeviceArrayBGRA : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayBGRA) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGRA toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGRA{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableImageDeviceArrayRGBA_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayRGBA_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGBA_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGBA_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableImageDeviceArrayBGRA_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayBGRA_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGRA_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGRA_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // DeviceArray (float format) + class ReleasableImageDeviceArrayRGBAf : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayRGBAf) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // DeviceArray (3-channel linear u8, no alpha) + class ReleasableImageDeviceArrayRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // DeviceArray (3-channel sRGB u8, no alpha) + class ReleasableImageDeviceArrayRGB_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayRGB_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageRGB_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageRGB_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // DeviceArray (3-channel linear u8, no alpha, channel-swapped) + class ReleasableImageDeviceArrayBGR : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayBGR) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGR toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGR{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // DeviceArray (3-channel sRGB u8, no alpha, channel-swapped) + class ReleasableImageDeviceArrayBGR_SRGB : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableImageDeviceArrayBGR_SRGB) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableImageBGR_SRGB toImage(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableImageBGR_SRGB{ WITH_GIL_UNLOCKED(Zivid::toImage(impl(), streamOrQueue)) }; + } + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // Function declarations for wrapping + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableMask.h b/src/Core/include/ZividPython/ReleasableMask.h new file mode 100644 index 00000000..0ec761b6 --- /dev/null +++ b/src/Core/include/ZividPython/ReleasableMask.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +namespace ZividPython +{ + class ReleasableMask : public Releasable + { + public: + using Releasable::Releasable; + + ZIVID_PYTHON_FORWARD_0_ARGS(width) + ZIVID_PYTHON_FORWARD_0_ARGS(height) + ZIVID_PYTHON_FORWARD_0_ARGS(size) + ZIVID_PYTHON_FORWARD_0_ARGS(resolution) + ZIVID_PYTHON_FORWARD_0_ARGS(toString) + }; + + void wrapClass(pybind11::class_ pyClass); + +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasablePointCloud.h b/src/Core/include/ZividPython/ReleasablePointCloud.h index e01f1e29..aa8b229a 100644 --- a/src/Core/include/ZividPython/ReleasablePointCloud.h +++ b/src/Core/include/ZividPython/ReleasablePointCloud.h @@ -1,9 +1,12 @@ #pragma once #include +#include #include #include #include +#include +#include #include #include @@ -31,12 +34,77 @@ namespace ZividPython downsampled, Zivid::PointCloud::Downsampling, downsampling) + ZIVID_PYTHON_FORWARD_1_ARGS(mask, const Zivid::Mask &, mask) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasablePointCloud, masked, const Zivid::Mask &, mask) + ZIVID_PYTHON_FORWARD_1_ARGS(maskByRegionOfInterest, const Zivid::Settings::RegionOfInterest::Box &, roiSettings) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN( + ReleasablePointCloud, + maskedByRegionOfInterest, + const Zivid::Settings::RegionOfInterest::Box &, + roiSettings) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageRGBA, copyImageRGBA) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGRA, copyImageBGRA) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageRGBA_SRGB, copyImageRGBA_SRGB) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableImageBGRA_SRGB, copyImageBGRA_SRGB) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableUnorganizedPointCloud, toUnorganizedPointCloud) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasablePointCloud, clone, const) + + // Device buffer methods for point cloud data + ReleasableDeviceArrayPointXYZ devicePointsXYZ(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayPointXYZ{ WITH_GIL_UNLOCKED(impl().devicePointsXYZ(streamOrQueue)) }; + } + + ReleasableDeviceArrayPointXYZW devicePointsXYZW(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayPointXYZW{ WITH_GIL_UNLOCKED(impl().devicePointsXYZW(streamOrQueue)) }; + } + + ReleasableDeviceArrayPointZ devicePointsZ(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayPointZ{ WITH_GIL_UNLOCKED(impl().devicePointsZ(streamOrQueue)) }; + } + + ReleasableDeviceArraySNR deviceSNRs(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArraySNR{ WITH_GIL_UNLOCKED(impl().deviceSNRs(streamOrQueue)) }; + } + + ReleasableDeviceArrayNormalXYZ deviceNormalsXYZ(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayNormalXYZ{ WITH_GIL_UNLOCKED(impl().deviceNormalsXYZ(streamOrQueue)) }; + } + + // Device buffer methods for image data + ReleasableImageDeviceArrayRGBA deviceImageRGBA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBA_SRGB deviceImageRGBA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA deviceImageBGRA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA_SRGB deviceImageBGRA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA_SRGB{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBAf deviceImageRGBAf(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBAf{ WITH_GIL_UNLOCKED( + impl().imageDeviceArray(streamOrQueue)) }; + } }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/ReleasablePointCloudDeviceArray.h b/src/Core/include/ZividPython/ReleasablePointCloudDeviceArray.h new file mode 100644 index 00000000..86488656 --- /dev/null +++ b/src/Core/include/ZividPython/ReleasablePointCloudDeviceArray.h @@ -0,0 +1,173 @@ +#pragma once + +#include + +#include "ZividPython/Releasable.h" +#include "ZividPython/ReleasableArray1D.h" +#include "ZividPython/ReleasableArray2D.h" +#include "ZividPython/Wrappers.h" + +namespace ZividPython +{ + class ReleasableDeviceArrayPointXYZ : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayPointXYZ) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayPointXYZW : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayPointXYZW) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayPointZ : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayPointZ) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArraySNR : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArraySNR) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + class ReleasableDeviceArrayNormalXYZ : public Releasable> + { + public: + using Releasable>::Releasable; + + ZIVID_PYTHON_ADD_COPY_CONSTRUCTOR(ReleasableDeviceArrayNormalXYZ) + + ZIVID_PYTHON_FORWARD_0_ARGS(shape, const) + ZIVID_PYTHON_FORWARD_0_ARGS(strides, const) + ZIVID_PYTHON_FORWARD_0_ARGS(stridesInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(sizeInBytes, const) + ZIVID_PYTHON_FORWARD_0_ARGS(backend, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isValid, const) + ZIVID_PYTHON_FORWARD_0_ARGS(isEmpty, const) + + ReleasableArray2D toArray2D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray2D{ WITH_GIL_UNLOCKED(impl().toArray2D(streamOrQueue)) }; + } + + ReleasableArray1D toArray1D(Zivid::StreamOrQueue streamOrQueue) const + { + return ReleasableArray1D{ WITH_GIL_UNLOCKED(impl().toArray1D(streamOrQueue)) }; + } + + void *devicePointer() const + { + return WITH_GIL_UNLOCKED(impl().devicePointer()); + } + }; + + // Function declarations for wrapping + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); +} // namespace ZividPython diff --git a/src/Core/include/ZividPython/ReleasableUnorganizedPointCloud.h b/src/Core/include/ZividPython/ReleasableUnorganizedPointCloud.h index efcaf3cc..96b238f1 100644 --- a/src/Core/include/ZividPython/ReleasableUnorganizedPointCloud.h +++ b/src/Core/include/ZividPython/ReleasableUnorganizedPointCloud.h @@ -1,7 +1,10 @@ #pragma once +#include #include #include +#include +#include #include namespace ZividPython @@ -39,6 +42,45 @@ namespace ZividPython const Zivid::ColorRGBA &, color) ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableUnorganizedPointCloud, clone, const) + + ReleasableDeviceArrayPointXYZ deviceArrayPointXYZ(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayPointXYZ{ WITH_GIL_UNLOCKED(impl().devicePointsXYZ(streamOrQueue)) }; + } + + ReleasableDeviceArrayPointXYZW deviceArrayPointXYZW(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArrayPointXYZW{ WITH_GIL_UNLOCKED(impl().devicePointsXYZW(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBA deviceArrayColorRGBA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA{ WITH_GIL_UNLOCKED( + impl().deviceColors(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA deviceArrayColorBGRA(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA{ WITH_GIL_UNLOCKED( + impl().deviceColors(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayRGBA_SRGB deviceArrayColorRGBA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayRGBA_SRGB{ WITH_GIL_UNLOCKED( + impl().deviceColors(streamOrQueue)) }; + } + + ReleasableImageDeviceArrayBGRA_SRGB deviceArrayColorBGRA_SRGB(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableImageDeviceArrayBGRA_SRGB{ WITH_GIL_UNLOCKED( + impl().deviceColors(streamOrQueue)) }; + } + + ReleasableDeviceArraySNR deviceArraySNR(const Zivid::StreamOrQueue &streamOrQueue) const + { + return ReleasableDeviceArraySNR{ WITH_GIL_UNLOCKED(impl().deviceSNRs(streamOrQueue)) }; + } }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/Resolution.h b/src/Core/include/ZividPython/Resolution.h new file mode 100644 index 00000000..72de5232 --- /dev/null +++ b/src/Core/include/ZividPython/Resolution.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include + +namespace ZividPython +{ + void wrapClass(pybind11::class_ pyClass); +} diff --git a/src/Core/include/ZividPython/SingletonApplication.h b/src/Core/include/ZividPython/SingletonApplication.h index 6dcf78b2..31560cc0 100644 --- a/src/Core/include/ZividPython/SingletonApplication.h +++ b/src/Core/include/ZividPython/SingletonApplication.h @@ -1,8 +1,11 @@ #pragma once #include +#include #include #include +#include +#include #include namespace ZividPython @@ -22,7 +25,16 @@ namespace ZividPython const Zivid::CameraInfo::SerialNumber &, serialNumber) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableCamera, connectCamera, const Zivid::CameraAddress &, address) + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN(ReleasableCamera, createFileCamera, const std::string &, fileName) + + ReleasableCamera createFileCamera(const ReleasableFrame &frame) + { + return ReleasableCamera{ WITH_GIL_UNLOCKED(impl().createFileCamera(frame.impl())) }; + } + + ZIVID_PYTHON_FORWARD_0_ARGS_WRAP_RETURN(ReleasableComputeDevice, computeDevice) }; void wrapClass(pybind11::class_ pyClass); diff --git a/src/Core/include/ZividPython/Toolbox/Barcode.h b/src/Core/include/ZividPython/Toolbox/Barcode.h index 8a1c334a..c260be3a 100644 --- a/src/Core/include/ZividPython/Toolbox/Barcode.h +++ b/src/Core/include/ZividPython/Toolbox/Barcode.h @@ -11,15 +11,27 @@ namespace ZividPython public: using Releasable::Releasable; ZIVID_PYTHON_FORWARD_1_ARGS(suggestSettings, Zivid::Camera &, camera) - ZIVID_PYTHON_FORWARD_2_ARGS_WRAP_RETURN( + ZIVID_PYTHON_FORWARD_1_ARGS_WRAP_RETURN( std::vector, + detectLinearCodes, + const Zivid::Frame2D &, + frame2d) + ZIVID_PYTHON_FORWARD_2_ARGS_WRAP_RETURN( + std::vector>, + decodeLinearCodes, + const std::vector &, + detectionResults, + const Zivid::Experimental::Toolbox::LinearBarcodeFormatFilter &, + filter) + ZIVID_PYTHON_FORWARD_2_ARGS_WRAP_RETURN( + std::vector, readLinearCodes, const Zivid::Frame2D &, frame2d, const Zivid::Experimental::Toolbox::LinearBarcodeFormatFilter &, filter) ZIVID_PYTHON_FORWARD_2_ARGS_WRAP_RETURN( - std::vector, + std::vector, readMatrixCodes, const Zivid::Frame2D &, frame2d, @@ -30,6 +42,7 @@ namespace ZividPython void wrapEnum(pybind11::enum_ pyEnum); void wrapEnum(pybind11::enum_ pyEnum); void wrapClass(pybind11::class_ pyClass); - void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); + void wrapClass(pybind11::class_ pyClass); void wrapClass(pybind11::class_ pyClass); } // namespace ZividPython diff --git a/test/calibration/test_calibration_board_detection.py b/test/calibration/test_calibration_board_detection.py index 34f05949..7186e4a4 100644 --- a/test/calibration/test_calibration_board_detection.py +++ b/test/calibration/test_calibration_board_detection.py @@ -31,8 +31,8 @@ def test_detect_calibration_board_file_camera(file_camera_calibration_board): _check_detection_result(detection_result) -def test_detect_calibration_board_invalid_file_camera(shared_file_camera): - detection_result = calibration.detect_calibration_board(shared_file_camera) +def test_detect_calibration_board_invalid_frame(frame): + detection_result = calibration.detect_calibration_board(frame) assert not detection_result.valid() assert not detection_result assert detection_result.status() == calibration.CalibrationBoardDetectionStatus.no_valid_fiducial_marker_detected diff --git a/test/calibration/test_hand_eye.py b/test/calibration/test_hand_eye.py index aa0cc163..ccf6aa76 100644 --- a/test/calibration/test_hand_eye.py +++ b/test/calibration/test_hand_eye.py @@ -53,6 +53,7 @@ def test_eyetohand_calibration(handeye_eth_frames, handeye_eth_poses, handeye_et # Perform eye-to-hand calibration handeye_output = zivid.calibration.calibrate_eye_to_hand(inputs) pytest.helpers.check_handeye_output(inputs, handeye_output, handeye_eth_transform) + assert handeye_output.status() == zivid.calibration.HandEyeStatus.ok def test_marker_eyetohand_calibration(handeye_eth_frames, handeye_eth_poses, handeye_marker_eth_transform): @@ -69,6 +70,7 @@ def test_marker_eyetohand_calibration(handeye_eth_frames, handeye_eth_poses, han # Perform eye-to-hand calibration handeye_output = zivid.calibration.calibrate_eye_to_hand(inputs) pytest.helpers.check_handeye_output(inputs, handeye_output, handeye_marker_eth_transform) + assert handeye_output.status() == zivid.calibration.HandEyeStatus.ok def test_eyetohand_calibration_save_load(handeye_eth_frames, handeye_eth_poses): diff --git a/test/calibration/test_hand_eye_status.py b/test/calibration/test_hand_eye_status.py new file mode 100644 index 00000000..5f4ff919 --- /dev/null +++ b/test/calibration/test_hand_eye_status.py @@ -0,0 +1,7 @@ +import _zivid +import zivid + + +def test_hand_eye_status(): + for value in _zivid.calibration.HandEyeStatus.__members__.values(): + assert getattr(zivid.calibration.HandEyeStatus, value.name) == value.name diff --git a/test/conftest.py b/test/conftest.py index 65b52554..cb862ae0 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -145,6 +145,11 @@ def point_cloud_fixture(frame): yield point_cloud +@pytest.fixture(name="sdk_stream_or_queue", scope="module") +def sdk_stream_or_queue_fixture(application): + return application.compute_device().sdk_stream_or_queue() + + @pytest.fixture(name="handeye_eth_poses", scope="function") def handeye_eth_poses_fixture(): path = _testdata_dir() / "handeye" / "eth" @@ -359,7 +364,7 @@ def run_sample(name, working_directory=None): @pytest.fixture( name="color_format", scope="function", - params=["rgba", "bgra", "rgba_srgb", "bgra_srgb", "srgb"], + params=["rgba", "bgra", "rgba_srgb", "bgra_srgb", "srgb", "rgb", "rgb_srgb", "bgr", "bgr_srgb"], ) def color_format_fixture(request): return request.param diff --git a/test/experimental/test_barcode.py b/test/experimental/test_barcode.py index 51b10d3c..4f4589dd 100644 --- a/test/experimental/test_barcode.py +++ b/test/experimental/test_barcode.py @@ -2,14 +2,37 @@ import zivid from zivid.experimental.toolbox.barcode import ( BarcodeDetector, + LinearBarcodeDecodingResult, LinearBarcodeDetectionResult, LinearBarcodeFormat, - MatrixBarcodeDetectionResult, + MatrixBarcodeDecodingResult, MatrixBarcodeFormat, ) +def _check_bounding_box(bb, center_position): + assert isinstance(str(bb), str) + assert isinstance(bb, zivid.BoundingBox) + assert isinstance(bb.x, int) + assert isinstance(bb.y, int) + assert isinstance(bb.width, int) + assert isinstance(bb.height, int) + assert center_position[0] >= bb.x + assert center_position[0] <= bb.x + bb.width + assert center_position[1] >= bb.y + assert center_position[1] <= bb.y + bb.height + + def _check_barcode_detection_result(result): + assert isinstance(str(result), str) + assert isinstance(result.center_position(), tuple) + assert len(result.center_position()) == 2 + assert isinstance(result.center_position()[0], float) + assert isinstance(result.center_position()[1], float) + _check_bounding_box(result.bounding_box(), result.center_position()) + + +def _check_barcode_decoding_result(result): assert isinstance(str(result), str) assert isinstance(result.code(), str) assert isinstance(result.code_format(), str) @@ -17,6 +40,7 @@ def _check_barcode_detection_result(result): assert len(result.center_position()) == 2 assert isinstance(result.center_position()[0], float) assert isinstance(result.center_position()[1], float) + _check_bounding_box(result.bounding_box(), result.center_position()) @pytest.mark.barcode_license @@ -28,8 +52,51 @@ def test_read_linear_codes(barcodes_frame): assert isinstance(results, list) assert len(results) == 15 for result in results: - assert isinstance(result, LinearBarcodeDetectionResult) - _check_barcode_detection_result(result) + assert isinstance(result, LinearBarcodeDecodingResult) + _check_barcode_decoding_result(result) + + +@pytest.mark.barcode_license +def test_separate_detect_and_decode_linear_codes(barcodes_frame): + frame_2d = barcodes_frame.frame_2d() + detector = BarcodeDetector() + + # Run detection only + detection_results = detector.detect_linear_codes(frame_2d) + assert isinstance(detection_results, list) + for detection_result in detection_results: + assert isinstance(detection_result, LinearBarcodeDetectionResult) + _check_barcode_detection_result(detection_result) + + # Run decoding based on the detection results + decoding_results = detector.decode_linear_codes(detection_results) + assert isinstance(decoding_results, list) + assert len(decoding_results) == len(detection_results) + for decoding_result in decoding_results: + if decoding_result is not None: + assert isinstance(decoding_result, LinearBarcodeDecodingResult) + _check_barcode_decoding_result(decoding_result) + + decoding_results_not_none = [res for res in decoding_results if res is not None] + + # For reference, run combined detection and decoding + decoding_results_combined = detector.read_linear_codes(frame_2d) + + # Not-None results from decode function should match results from combined function + assert len(decoding_results_not_none) == len(decoding_results_combined) + for res_decode, res_combined in zip(decoding_results_not_none, decoding_results_combined, strict=True): + assert res_decode.code() == res_combined.code() + assert res_decode.code_format() == res_combined.code_format() + assert res_decode.center_position() == res_combined.center_position() + + # Try decoding only a subset of detection results + subset_detection_results = [detection_results[0], detection_results[-1]] + subset_decoding_results = detector.decode_linear_codes(subset_detection_results) + assert len(subset_decoding_results) == 2 + for decoding_result in subset_decoding_results: + if decoding_result is not None: + assert isinstance(decoding_result, LinearBarcodeDecodingResult) + _check_barcode_decoding_result(decoding_result) @pytest.mark.barcode_license @@ -41,8 +108,8 @@ def test_read_matrix_codes(barcodes_frame): assert isinstance(results, list) assert len(results) == 15 for result in results: - assert isinstance(result, MatrixBarcodeDetectionResult) - _check_barcode_detection_result(result) + assert isinstance(result, MatrixBarcodeDecodingResult) + _check_barcode_decoding_result(result) @pytest.mark.barcode_license diff --git a/test/experimental/test_hand_eye_low_dof.py b/test/experimental/test_hand_eye_low_dof.py index ff1685a7..90e6d923 100644 --- a/test/experimental/test_hand_eye_low_dof.py +++ b/test/experimental/test_hand_eye_low_dof.py @@ -98,7 +98,7 @@ def test_eth_transform_low_dof_approximate_match( # Ensure that the low DOF calibration transforms are approximately the same as their # full 6-DOF calibration transform counterparts. np.testing.assert_allclose(handeye_eth_transform, handeye_eth_low_dof_transform, rtol=2.5e-2) - np.testing.assert_allclose(handeye_marker_eth_transform, handeye_eth_low_dof_markers_transform, rtol=2.5e-2) + np.testing.assert_allclose(handeye_marker_eth_transform, handeye_eth_low_dof_markers_transform, rtol=3.0e-2) def test_eye_to_hand_low_dof_calibration_with_calibration_board( @@ -179,5 +179,7 @@ def test_eye_in_hand_low_dof_calibration_with_eye_to_hand_data( ) fixed_objects = zivid.experimental.hand_eye_low_dof.FixedPlacementOfCalibrationObjects(fixed_calibration_board) - with pytest.raises(RuntimeError): - _ = zivid.experimental.hand_eye_low_dof.calibrate_eye_in_hand_low_dof(inputs, fixed_objects) + # Should not throw as of ZIVID-12106. We should instead test the status enum. + output = zivid.experimental.hand_eye_low_dof.calibrate_eye_in_hand_low_dof(inputs, fixed_objects) + assert output.valid() + assert output.status() != zivid.calibration.HandEyeStatus.ok diff --git a/test/test_application.py b/test/test_application.py index 096e6969..93c0aae8 100644 --- a/test/test_application.py +++ b/test/test_application.py @@ -46,3 +46,18 @@ def test_to_string(application): string = str(application) assert string assert isinstance(string, str) + + +def test_connect_camera_raises_when_serial_number_and_address_both_given(application): + with pytest.raises(ValueError): + application.connect_camera(serial_number="ABC123", address=zivid.CameraAddress("192.168.0.1")) + + +@pytest.mark.physical_camera +def test_connect_camera_with_address(application): + with application.connect_camera() as cam: + ip_address = cam.state.network.ipv4.address + + with application.connect_camera(address=zivid.CameraAddress(ip_address)) as cam: + assert cam + assert isinstance(cam, zivid.Camera) diff --git a/test/test_bounding_box.py b/test/test_bounding_box.py new file mode 100644 index 00000000..9dcdae45 --- /dev/null +++ b/test/test_bounding_box.py @@ -0,0 +1,21 @@ +import zivid + + +def test_bounding_box_basics(): + + bb = zivid.BoundingBox(x=10, y=20, width=300, height=400) + assert bb.x == 10 + assert bb.y == 20 + assert bb.width == 300 + assert bb.height == 400 + assert str(bb) == "{ x: 10, y: 20, width: 300, height: 400 }" + + bb.x = 15 + bb.y = 25 + bb.width = 350 + bb.height = 450 + assert bb.x == 15 + assert bb.y == 25 + assert bb.width == 350 + assert bb.height == 450 + assert str(bb) == "{ x: 15, y: 25, width: 350, height: 450 }" diff --git a/test/test_camera_capture.py b/test/test_camera_capture.py index 89d8f9dc..38b1958c 100644 --- a/test/test_camera_capture.py +++ b/test/test_camera_capture.py @@ -21,12 +21,12 @@ def test_capture_2d_3d_one_2d_and_one_3d(shared_file_camera): assert isinstance(frame.frame_2d(), zivid.Frame2D) -def test_capture_2d_3d_two_2d_and_one_3d(shared_file_camera): +def test_capture_2d_3d_two_2d_and_one_3d(file_camera_calibration_board): acquisitions3d = [zivid.Settings.Acquisition()] acquisitions2d = [zivid.Settings2D.Acquisition(), zivid.Settings2D.Acquisition()] settings = zivid.Settings(acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d)) - with shared_file_camera.capture_2d_3d(settings) as frame: + with file_camera_calibration_board.capture_2d_3d(settings) as frame: assert frame assert isinstance(frame, zivid.frame.Frame) assert len(frame.settings.acquisitions) == 1 @@ -36,12 +36,12 @@ def test_capture_2d_3d_two_2d_and_one_3d(shared_file_camera): assert isinstance(frame.frame_2d(), zivid.Frame2D) -def test_capture_2d_3d_one_2d_and_two_3d(shared_file_camera): +def test_capture_2d_3d_one_2d_and_two_3d(file_camera_calibration_board): acquisitions3d = [zivid.Settings.Acquisition(), zivid.Settings.Acquisition()] acquisitions2d = [zivid.Settings2D.Acquisition()] settings = zivid.Settings(acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d)) - with shared_file_camera.capture_2d_3d(settings) as frame: + with file_camera_calibration_board.capture_2d_3d(settings) as frame: assert frame assert isinstance(frame, zivid.frame.Frame) assert len(frame.settings.acquisitions) == 2 @@ -51,12 +51,12 @@ def test_capture_2d_3d_one_2d_and_two_3d(shared_file_camera): assert isinstance(frame.frame_2d(), zivid.Frame2D) -def test_capture_2d_3d_two_2d_and_two_3d(shared_file_camera): +def test_capture_2d_3d_two_2d_and_two_3d(file_camera_calibration_board): acquisitions3d = [zivid.Settings.Acquisition(), zivid.Settings.Acquisition()] acquisitions2d = [zivid.Settings2D.Acquisition(), zivid.Settings2D.Acquisition()] settings = zivid.Settings(acquisitions=acquisitions3d, color=zivid.Settings2D(acquisitions=acquisitions2d)) - with shared_file_camera.capture_2d_3d(settings) as frame: + with file_camera_calibration_board.capture_2d_3d(settings) as frame: assert frame assert isinstance(frame, zivid.frame.Frame) assert len(frame.settings.acquisitions) == 2 @@ -104,17 +104,14 @@ def test_one_acquisition_in_list(shared_file_camera): assert isinstance(frame, zivid.frame.Frame) -def test_five_acquisitions_in_list(shared_file_camera): +def test_multiple_acquisitions_in_list(file_camera_calibration_board): acquisitions = [ zivid.Settings.Acquisition(), zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), ] settings = zivid.Settings(acquisitions=acquisitions) assert isinstance(acquisitions, list) - with shared_file_camera.capture(settings) as frame: + with file_camera_calibration_board.capture(settings) as frame: assert frame assert isinstance(frame, zivid.frame.Frame) @@ -128,17 +125,14 @@ def test_one_acquisition_in_tuple(shared_file_camera): assert isinstance(frame, zivid.frame.Frame) -def test_five_acquisition_in_tuple(shared_file_camera): +def test_multiple_acquisition_in_tuple(file_camera_calibration_board): acquisitions = ( zivid.Settings.Acquisition(), zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), - zivid.Settings.Acquisition(), ) settings = zivid.Settings(acquisitions=acquisitions) assert isinstance(acquisitions, tuple) - with shared_file_camera.capture(settings) as frame: + with file_camera_calibration_board.capture(settings) as frame: assert frame assert isinstance(frame, zivid.frame.Frame) diff --git a/test/test_data/calibration_board.zfc b/test/test_data/calibration_board.zfc index 91cd4b03..341f48a0 100644 --- a/test/test_data/calibration_board.zfc +++ b/test/test_data/calibration_board.zfc @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:037c785634db01a7cad764084546f216477a3204191df7384d773d65e16bc80d -size 31382179 +oid sha256:e4691d67fa0231b2dd850bd67673dd6f79b697668af1d47d8c2e8f4826c9f5e2 +size 110923843 diff --git a/test/test_data/handeye/eth/eth_transform.csv b/test/test_data/handeye/eth/eth_transform.csv index b976e45c..672ab08d 100644 --- a/test/test_data/handeye/eth/eth_transform.csv +++ b/test/test_data/handeye/eth/eth_transform.csv @@ -1,4 +1,4 @@ -0.983483,-0.125840,0.130096,-155.676971 --0.165186,-0.917843,0.360939,-1041.600952 -0.073988,-0.376468,-0.923471,1078.035522 +0.9834803,-0.12545618,0.13048853,-156.07708740 +-0.16502444,-0.91764396,0.36151980,-1042.68200684 +0.07438712,-0.37708140,-0.92318810,1078.21350098 0.000000,0.000000,0.000000,1.000000 diff --git a/test/test_data/handeye/eth/eth_transform_marker.csv b/test/test_data/handeye/eth/eth_transform_marker.csv index a65c88e3..bca01dde 100644 --- a/test/test_data/handeye/eth/eth_transform_marker.csv +++ b/test/test_data/handeye/eth/eth_transform_marker.csv @@ -1,4 +1,4 @@ -0.983497,-0.125464,0.130352,-155.667160 --0.164819,-0.918463,0.359528,-1041.161377 -0.074616,-0.375079,-0.923985,1079.506104 +0.983474,-0.125484,0.130506,-155.768890 +-0.164926,-0.918299,0.359896,-1041.281006 +0.074682,-0.375473,-0.923820,1079.107666 0.000000,0.000000,0.000000,1.000000 diff --git a/test/test_data/handeye/eth/low_dof/eth_low_dof_transform_marker.csv b/test/test_data/handeye/eth/low_dof/eth_low_dof_transform_marker.csv index af3acb9a..3c3045c9 100644 --- a/test/test_data/handeye/eth/low_dof/eth_low_dof_transform_marker.csv +++ b/test/test_data/handeye/eth/low_dof/eth_low_dof_transform_marker.csv @@ -1,4 +1,4 @@ -0.983436, -0.127217, 0.129111, -155.160049 --0.165995, -0.918205, 0.359645, -1040.022095 -0.072797, -0.375119, -0.924114, 1079.166382 -0.000000, 0.000000, 0.000000, 1.000000 +0.983411,-0.127356,0.129161,-155.174927 +-0.166149,-0.918152,0.359710,-1040.049805 +0.072779,-0.375203,-0.924081,1079.151367 +0.000000,0.000000,0.000000,1.000000 diff --git a/test/test_device_array.py b/test/test_device_array.py new file mode 100644 index 00000000..828b7fe9 --- /dev/null +++ b/test/test_device_array.py @@ -0,0 +1,240 @@ +import logging + +import numpy as np +import pytest +import zivid + +logger = logging.getLogger(__name__) + +_FOUR_CHANNEL_BYTE_FORMATS = [ + zivid.PixelFormat.RGBA, + zivid.PixelFormat.BGRA, + zivid.PixelFormat.RGBA_SRGB, + zivid.PixelFormat.BGRA_SRGB, +] + +_THREE_CHANNEL_FORMATS = [ + zivid.PixelFormat.RGB, + zivid.PixelFormat.RGB_SRGB, + zivid.PixelFormat.BGR, + zivid.PixelFormat.BGR_SRGB, +] + + +@pytest.fixture(name="cuda_compute_device") +def cuda_compute_device_fixture(application): + device = application.compute_device() + if device.backend != zivid.ComputeBackend.cuda: + pytest.skip("Test requires the CUDA compute backend") + return device + + +@pytest.fixture(name="torch_cuda") +def torch_cuda_fixture(): + torch = pytest.importorskip("torch") + if not torch.cuda.is_available(): + pytest.skip("Test requires PyTorch with CUDA") + return torch + + +def test_image(frame_2d, sdk_stream_or_queue): + for color_format in _FOUR_CHANNEL_BYTE_FORMATS + [zivid.PixelFormat.RGBAF]: + device_array = frame_2d.image_device_array(sdk_stream_or_queue, color_format) + assert isinstance(device_array, zivid.device_array.DeviceArray) + assert device_array.shape[2] == 4 + + for color_format in _THREE_CHANNEL_FORMATS: + device_array = frame_2d.image_device_array(sdk_stream_or_queue, color_format) + assert isinstance(device_array, zivid.device_array.DeviceArray) + assert device_array.shape[2] == 3 + + +def test_image_copy_to_host_all_formats(frame_2d, sdk_stream_or_queue): + for color_format in _FOUR_CHANNEL_BYTE_FORMATS + _THREE_CHANNEL_FORMATS: + device_array = frame_2d.image_device_array(sdk_stream_or_queue, color_format) + host_array = device_array.copy_to_host_organized_array(sdk_stream_or_queue) + zivid.synchronize_stream(sdk_stream_or_queue) + assert isinstance(host_array, np.ndarray) + assert host_array.ndim == 3 + assert host_array.shape[0] == device_array.shape[0] + assert host_array.shape[1] == device_array.shape[1] + assert host_array.shape[2] == device_array.shape[2] + assert host_array.strides[0] == device_array.strides_in_bytes[0] + assert host_array.strides[1] == device_array.strides_in_bytes[1] + assert host_array.strides[2] == device_array.strides_in_bytes[2] + + +def test_point_cloud_copy_to_host_organized(point_cloud, sdk_stream_or_queue): + device_arrays = [ + getattr(point_cloud, method_name)(sdk_stream_or_queue) + for method_name in [ + "device_points_xyz", + "device_points_xyzw", + "device_points_z", + "device_snrs", + "device_normals_xyz", + ] + ] + device_arrays += [ + point_cloud.device_image(sdk_stream_or_queue, color_format) for color_format in _FOUR_CHANNEL_BYTE_FORMATS + ] + for device_array in device_arrays: + assert isinstance(device_array, zivid.device_array.DeviceArray) + host_array = device_array.copy_to_host_organized_array(sdk_stream_or_queue) + zivid.synchronize_stream(sdk_stream_or_queue) + assert isinstance(host_array, np.ndarray) + assert host_array.ndim >= 2 + assert host_array.shape[0] == device_array.shape[0] + assert host_array.shape[1] == device_array.shape[1] + if host_array.ndim > 2: + assert host_array.shape[2] == device_array.shape[2] + assert host_array.strides[0] == device_array.strides_in_bytes[0] + assert host_array.strides[1] == device_array.strides_in_bytes[1] + if host_array.ndim > 2: + assert host_array.strides[2] == device_array.strides_in_bytes[2] + + +def test_point_cloud_copy_to_host_unorganized(point_cloud, sdk_stream_or_queue): + upc = point_cloud.to_unorganized_point_cloud() + device_arrays = [ + getattr(upc, method_name)(sdk_stream_or_queue) for method_name in ["device_points_xyz", "device_points_xyzw"] + ] + device_arrays += [ + upc.device_colors(sdk_stream_or_queue, color_format) for color_format in _FOUR_CHANNEL_BYTE_FORMATS + ] + for device_array in device_arrays: + assert isinstance(device_array, zivid.device_array.DeviceArray) + host_array = device_array.copy_to_host_unorganized_array(sdk_stream_or_queue) + zivid.synchronize_stream(sdk_stream_or_queue) + assert isinstance(host_array, np.ndarray) + assert host_array.ndim == 2 + assert host_array.shape[0] == device_array.shape[0] + assert host_array.shape[1] == device_array.shape[1] + assert host_array.strides[0] == device_array.strides_in_bytes[0] + assert host_array.strides[1] == device_array.strides_in_bytes[1] + + device_snrs = upc.device_snrs(sdk_stream_or_queue) + assert isinstance(device_snrs, zivid.device_array.DeviceArray) + host_snrs = device_snrs.copy_to_host_unorganized_array(sdk_stream_or_queue) + zivid.synchronize_stream(sdk_stream_or_queue) + assert isinstance(host_snrs, np.ndarray) + assert host_snrs.ndim == 1 + assert host_snrs.shape[0] == device_snrs.shape[0] + assert host_snrs.strides[0] == device_snrs.strides_in_bytes[0] + + +def test_organized_device_array_rejects_copy_to_host_unorganized(point_cloud, sdk_stream_or_queue): + device_arrays = [ + getattr(point_cloud, method_name)(sdk_stream_or_queue) + for method_name in [ + "device_points_xyz", + "device_points_xyzw", + "device_points_z", + "device_snrs", + "device_normals_xyz", + ] + ] + device_arrays += [ + point_cloud.device_image(sdk_stream_or_queue, color_format) for color_format in _FOUR_CHANNEL_BYTE_FORMATS + ] + for device_array in device_arrays: + with pytest.raises(RuntimeError): + device_array.copy_to_host_unorganized_array(sdk_stream_or_queue) + + +@pytest.mark.parametrize("color_format", _FOUR_CHANNEL_BYTE_FORMATS + _THREE_CHANNEL_FORMATS) +def test_image_device_array_fill_matches_allocate( + frame_2d, cuda_compute_device, sdk_stream_or_queue, torch_cuda, color_format +): + _ = cuda_compute_device + reference = frame_2d.image_device_array(sdk_stream_or_queue, color_format) + reference_host = reference.copy_to_host_organized_array(sdk_stream_or_queue) + zivid.synchronize_stream(sdk_stream_or_queue) + + height, width, channels = reference.shape + user_stream = zivid.CUDAStreamPtr(torch_cuda.cuda.current_stream().cuda_stream) + destination = torch_cuda.zeros((height, width, channels), dtype=torch_cuda.uint8, device="cuda") + destination_view = zivid.create_device_array_view(destination, user_stream, color_format) + frame_2d.image_device_array_fill(user_stream, destination_view) + zivid.synchronize_stream(user_stream) + + np.testing.assert_array_equal(reference_host, destination.cpu().numpy()) + + +def test_image_device_array_fill_rgbaf_writes_image(frame_2d, cuda_compute_device, sdk_stream_or_queue, torch_cuda): + _ = cuda_compute_device + height, width = frame_2d.image_device_array(sdk_stream_or_queue, zivid.PixelFormat.RGBAF).shape[:2] + user_stream = zivid.CUDAStreamPtr(torch_cuda.cuda.current_stream().cuda_stream) + destination = torch_cuda.zeros((height, width, 4), dtype=torch_cuda.float32, device="cuda") + destination_view = zivid.create_device_array_view(destination, user_stream, zivid.PixelFormat.RGBAF) + frame_2d.image_device_array_fill(user_stream, destination_view) + zivid.synchronize_stream(user_stream) + assert bool(destination.any()) + + +def test_image_device_array_rejects_non_pixel_format(frame_2d, sdk_stream_or_queue): + with pytest.raises(TypeError): + frame_2d.image_device_array(sdk_stream_or_queue, "rgba") + + +def test_three_channel_device_array_to_image(frame_2d, sdk_stream_or_queue): + """to_image() on 3-channel device arrays returns a host-side Image with matching pixels.""" + cases = [ + (zivid.PixelFormat.RGB, "image_rgb"), + (zivid.PixelFormat.RGB_SRGB, "image_rgb_srgb"), + (zivid.PixelFormat.BGR, "image_bgr"), + (zivid.PixelFormat.BGR_SRGB, "image_bgr_srgb"), + ] + for color_format, host_method in cases: + device_array = frame_2d.image_device_array(sdk_stream_or_queue, color_format) + image_via_device = zivid.Image( + device_array._impl.to_image(sdk_stream_or_queue) # pylint: disable=protected-access + ) + zivid.synchronize_stream(sdk_stream_or_queue) + image_via_host = getattr(frame_2d, host_method)() + assert image_via_device.height == image_via_host.height, color_format + assert image_via_device.width == image_via_host.width, color_format + np.testing.assert_array_equal(image_via_device.copy_data(), image_via_host.copy_data()) + + +def test_three_channel_image_save_load_roundtrip(frame_2d, tmp_path): + """Image and Image save and load round-trip via PNG.""" + cases = [ + ("image_rgb", "rgb"), + ("image_rgb_srgb", "rgb_srgb"), + ("image_bgr", "bgr"), + ("image_bgr_srgb", "bgr_srgb"), + ] + for host_method, color_format in cases: + image = getattr(frame_2d, host_method)() + path = tmp_path / f"{host_method}.png" + image.save(path) + loaded = zivid.Image.load(path, color_format=color_format) + np.testing.assert_array_equal(image.copy_data(), loaded.copy_data()) + + +def test_unorganized_device_array_rejects_copy_to_host_organized(point_cloud, sdk_stream_or_queue): + upc = point_cloud.to_unorganized_point_cloud() + device_arrays = [ + getattr(upc, method_name)(sdk_stream_or_queue) + for method_name in ["device_points_xyz", "device_points_xyzw", "device_snrs"] + ] + device_arrays += [ + upc.device_colors(sdk_stream_or_queue, color_format) for color_format in _FOUR_CHANNEL_BYTE_FORMATS + ] + for device_array in device_arrays: + with pytest.raises(RuntimeError): + device_array.copy_to_host_organized_array(sdk_stream_or_queue) + + +def test_point_cloud_device_image_rgbaf(point_cloud, sdk_stream_or_queue): + device_array = point_cloud.device_image(sdk_stream_or_queue, zivid.PixelFormat.RGBAF) + assert isinstance(device_array, zivid.device_array.DeviceArray) + assert device_array.shape[2] == 4 + assert device_array.is_valid + + +def test_unorganized_device_colors_rejects_rgbaf(point_cloud, sdk_stream_or_queue): + upc = point_cloud.to_unorganized_point_cloud() + with pytest.raises(ValueError): + upc.device_colors(sdk_stream_or_queue, zivid.PixelFormat.RGBAF) diff --git a/test/test_frame.py b/test/test_frame.py index 2d2613fd..589703fe 100644 --- a/test/test_frame.py +++ b/test/test_frame.py @@ -2,10 +2,13 @@ import tempfile from pathlib import Path +import numpy as np import pytest import zivid from assertions import assert_point_clouds_equal, assert_point_clouds_not_equal from zivid import CameraInfo, CameraState, FrameInfo, Settings +from zivid.mask import Mask +from zivid.resolution import Resolution def test_illegal_init( @@ -150,3 +153,153 @@ def test_clone(frame, transform): frame.release() assert isinstance(frame_clone.point_cloud(), zivid.PointCloud) + + +def test_mask_with_zivid_mask(frame): + """Test masking with zivid.Mask object.""" + height, width = frame.point_cloud().height, frame.point_cloud().width + + # Create mask using zivid.Mask + mask_array = np.ones((height, width), dtype=np.uint8) + mask_array[height // 3 : 2 * height // 3, width // 3 : 2 * width // 3] = False # Only keep center region + + mask = zivid.Mask(mask_array) + + # Apply mask + frame.mask(mask) + + # Check results + masked_xyz = frame.point_cloud().copy_data("xyz") + + # Outside center region should be NaN + # Top region + top_region = masked_xyz[: height // 3, :, 0] + assert np.all(np.isnan(top_region)) + + # Bottom region + bottom_region = masked_xyz[2 * height // 3 :, :, 0] + assert np.all(np.isnan(bottom_region)) + + # Left and right regions + left_region = masked_xyz[:, : width // 3, 0] + assert np.all(np.isnan(left_region)) + + right_region = masked_xyz[:, 2 * width // 3 :, 0] + assert np.all(np.isnan(right_region)) + + +def test_masked_returns_new_instance(frame): + """Test that masked() returns a new PointCloud instance.""" + height, width = frame.point_cloud().height, frame.point_cloud().width + + # Create mask + mask_array = np.zeros((height, width), dtype=bool) + mask_array[10:20, 10:20] = True + + # Get original data for comparison + original_xyz = frame.point_cloud().copy_data("xyz").copy() + + # Apply masked() - should return new instance + masked_point_cloud = frame.point_cloud().masked(mask_array) + + # Should be different instances + assert masked_point_cloud is not frame.point_cloud() + assert isinstance(masked_point_cloud, zivid.PointCloud) + + # Original should be unchanged + current_xyz = frame.point_cloud().copy_data("xyz") + np.testing.assert_array_equal(original_xyz, current_xyz) + + # New instance should have mask applied + masked_xyz = masked_point_cloud.copy_data("xyz") + masked_region = masked_xyz[10:20, 10:20, 0] + assert np.all(np.isnan(masked_region)) + + # Clean up + masked_point_cloud.release() + + +def test_mask_frame_with_numpy_array(frame): + """Test masking frame with numpy array.""" + height, width = frame.point_cloud().height, frame.point_cloud().width + + # Create mask as numpy array + mask_array = np.ones((height, width), dtype=bool) + mask_array[height // 4 : 3 * height // 4, width // 4 : 3 * width // 4] = False # Keep center region + + # Apply mask to frame + frame.mask(mask_array) + + # Check results + masked_xyz = frame.point_cloud().copy_data("xyz") + + # Outside center region should be NaN + # Top region should be NaN + top_region = masked_xyz[: height // 4, :, 0] + assert np.all(np.isnan(top_region)) + + # Bottom region should be NaN + bottom_region = masked_xyz[3 * height // 4 :, :, 0] + assert np.all(np.isnan(bottom_region)) + + +def test_masked_frame_returns_new_instance(frame): + """Test that frame.masked() returns a new Frame instance.""" + height, width = frame.point_cloud().height, frame.point_cloud().width + + # Create mask + mask_array = np.zeros((height, width), dtype=bool) + mask_array[5:15, 5:15] = True + + # Get original data for comparison + original_xyz = frame.point_cloud().copy_data("xyz").copy() + + # Apply masked() - should return new instance + masked_frame = frame.masked(mask_array) + + # Should be different instances + assert masked_frame is not frame + assert isinstance(masked_frame, zivid.Frame) + + # Original should be unchanged + current_xyz = frame.point_cloud().copy_data("xyz") + np.testing.assert_array_equal(original_xyz, current_xyz) + + # New instance should have mask applied + masked_xyz = masked_frame.point_cloud().copy_data("xyz") + masked_region = masked_xyz[5:15, 5:15, 0] + assert np.all(np.isnan(masked_region)) + + # Clean up + masked_frame.release() + + +def test_mask_can_be_released(): + """Test that Mask objects can be released.""" + # Create a mask + resolution = Resolution(100, 80) + mask = Mask(resolution) + + # Should be able to access properties + assert mask.width == 100 + assert mask.height == 80 + + # Release the mask + mask.release() + + # After release, should raise RuntimeError when accessing properties + with pytest.raises(RuntimeError): + _ = mask.width + + +def test_mask_context_manager(): + """Test that Mask works as context manager.""" + resolution = Resolution(50, 40) + + with Mask(resolution) as mask: + assert mask.width == 50 + assert mask.height == 40 + + # After exiting context, should raise RuntimeError + with pytest.raises(RuntimeError): + _ = mask.width diff --git a/test/test_frame_2d.py b/test/test_frame_2d.py index 40daf139..6aa5cbcc 100644 --- a/test/test_frame_2d.py +++ b/test/test_frame_2d.py @@ -38,6 +38,16 @@ def test_image(frame_2d): assert isinstance(image_srgb, zivid.Image) +def test_image_three_channel(frame_2d): + for method_name in ["image_rgb", "image_rgb_srgb", "image_bgr", "image_bgr_srgb"]: + image = getattr(frame_2d, method_name)() + assert image is not None, method_name + assert isinstance(image, zivid.Image), method_name + data = image.copy_data() + assert data.ndim == 3, method_name + assert data.shape[2] == 3, method_name + + def test_deprecated_srgb(frame_2d): image_rgba_srgb = frame_2d.image_rgba_srgb() image_srgb = frame_2d.image_srgb() @@ -58,6 +68,39 @@ def test_image_rgba_bgra_correspondence(frame_2d): np.testing.assert_array_equal(bgra[:, :, 3], rgba[:, :, 3]) +def test_image_rgb_bgr_correspondence(frame_2d): + """3-channel RGB and BGR are the same pixels with channels 0 and 2 swapped.""" + rgb_linear = frame_2d.image_rgb().copy_data() + bgr_linear = frame_2d.image_bgr().copy_data() + + rgb_srgb = frame_2d.image_rgb_srgb().copy_data() + bgr_srgb = frame_2d.image_bgr_srgb().copy_data() + + for rgb, bgr in [(rgb_linear, bgr_linear), (rgb_srgb, bgr_srgb)]: + np.testing.assert_array_equal(bgr[:, :, 0], rgb[:, :, 2]) + np.testing.assert_array_equal(bgr[:, :, 1], rgb[:, :, 1]) + np.testing.assert_array_equal(bgr[:, :, 2], rgb[:, :, 0]) + + +def test_image_three_channel_matches_four_channel(frame_2d): + """The 3-channel formats carry the same RGB/BGR pixels as their 4-channel siblings.""" + rgba = frame_2d.image_rgba().copy_data() + rgb = frame_2d.image_rgb().copy_data() + np.testing.assert_array_equal(rgb, rgba[:, :, :3]) + + bgra = frame_2d.image_bgra().copy_data() + bgr = frame_2d.image_bgr().copy_data() + np.testing.assert_array_equal(bgr, bgra[:, :, :3]) + + rgba_srgb = frame_2d.image_rgba_srgb().copy_data() + rgb_srgb = frame_2d.image_rgb_srgb().copy_data() + np.testing.assert_array_equal(rgb_srgb, rgba_srgb[:, :, :3]) + + bgra_srgb = frame_2d.image_bgra_srgb().copy_data() + bgr_srgb = frame_2d.image_bgr_srgb().copy_data() + np.testing.assert_array_equal(bgr_srgb, bgra_srgb[:, :, :3]) + + def test_state(frame_2d): state = frame_2d.state assert state is not None diff --git a/test/test_image.py b/test/test_image.py index 633afb5d..8be153e8 100644 --- a/test/test_image.py +++ b/test/test_image.py @@ -7,11 +7,12 @@ import zivid -def test_copy_data(image_2d): +def test_copy_data(image_2d, color_format): data = image_2d.copy_data() assert data is not None assert isinstance(data, np.ndarray) - assert data.shape == (image_2d.height, image_2d.width, 4) + expected_channels = 3 if color_format in {"rgb", "rgb_srgb", "bgr", "bgr_srgb"} else 4 + assert data.shape == (image_2d.height, image_2d.width, expected_channels) assert data.dtype == np.uint8 @@ -71,9 +72,7 @@ def test_load_invalid_color_format(frame_2d): image_file = Path(tmpdir) / "saved_image.png" frame_2d.image_rgba().save(image_file) with pytest.raises(ValueError): - zivid.Image.load(image_file, "bgr") - with pytest.raises(ValueError): - zivid.Image.load(image_file, "rgb") + zivid.Image.load(image_file, "rgbz") with pytest.raises(ValueError): zivid.Image.load(image_file, "asdf") diff --git a/test/test_mask.py b/test/test_mask.py new file mode 100644 index 00000000..50994fc2 --- /dev/null +++ b/test/test_mask.py @@ -0,0 +1,199 @@ +"""Tests for zivid.Mask class.""" + +import numpy as np +import pytest +import zivid + + +def test_mask_init_from_numpy_boolean(): + """Test creating Mask from numpy boolean array.""" + mask_data = np.array([[True, False, True], [False, True, False]], dtype=bool) + + mask = zivid.Mask(mask_data) + + assert mask.width == 3 + assert mask.height == 2 + + # Convert back to array and check values + result_array = mask.to_array() + expected = np.array([[1, 0, 1], [0, 1, 0]], dtype=np.uint8) + + np.testing.assert_array_equal(result_array, expected) + + +def test_mask_init_from_numpy_uint8(): + """Test creating Mask from numpy uint8 array.""" + mask_data = np.array([[1, 0, 255], [0, 128, 0]], dtype=np.uint8) + + mask = zivid.Mask(mask_data) + + assert mask.width == 3 + assert mask.height == 2 + + # Should preserve uint8 values exactly + result_array = mask.to_array() + np.testing.assert_array_equal(result_array, mask_data) + + +def test_mask_init_from_numpy_int(): + """Test creating Mask from numpy integer array.""" + mask_data = np.array([[1, 0, -1], [0, 5, 0]], dtype=int) + + mask = zivid.Mask(mask_data) + + # Should convert non-zero to 1, zero to 0 + result_array = mask.to_array() + expected = np.array([[1, 0, 1], [0, 1, 0]], dtype=np.uint8) + + np.testing.assert_array_equal(result_array, expected) + + +def test_mask_init_from_numpy_float(): + """Test creating Mask from numpy float array.""" + mask_data = np.array([[1.5, 0.0, -0.1], [0.0, 0.001, 0.0]], dtype=float) + + mask = zivid.Mask(mask_data) + + # Should convert non-zero to 1, zero to 0 + result_array = mask.to_array() + expected = np.array([[1, 0, 1], [0, 1, 0]], dtype=np.uint8) + + np.testing.assert_array_equal(result_array, expected) + + +def test_mask_init_from_resolution(): + """Test creating Mask from Resolution object.""" + resolution = zivid.Resolution(width=5, height=3) + + mask = zivid.Mask(resolution) + + assert mask.width == 5 + assert mask.height == 3 + + # Should be filled with ones (no masking) + result_array = mask.to_array() + expected = np.ones((3, 5), dtype=np.uint8) + + np.testing.assert_array_equal(result_array, expected) + + +def test_mask_properties(): + """Test Mask properties.""" + mask_data = np.ones((10, 15), dtype=bool) + mask = zivid.Mask(mask_data) + + assert mask.width == 15 + assert mask.height == 10 + + resolution = mask.resolution + assert resolution.width == 15 + assert resolution.height == 10 + + +def test_mask_string_representation(): + """Test string representation of Mask.""" + mask_data = np.ones((2, 3), dtype=bool) + mask = zivid.Mask(mask_data) + + str_repr = str(mask) + repr_repr = repr(mask) + + # Should contain dimensions + assert "Width" in str_repr or "width" in str_repr.lower() + assert "Height" in str_repr or "height" in str_repr.lower() + assert "3" in str_repr # width + assert "2" in str_repr # height + + # __str__ and __repr__ should be the same + assert str_repr == repr_repr + + +def test_mask_invalid_dimensions(): + """Test error handling for invalid array dimensions.""" + # 1D array should fail + with pytest.raises(ValueError, match="2D array"): + zivid.Mask(np.array([1, 0, 1])) + + # 3D array should fail + with pytest.raises(ValueError, match="2D array"): + zivid.Mask(np.ones((2, 3, 4))) + + +def test_mask_invalid_type(): + """Test error handling for invalid input types.""" + with pytest.raises(TypeError): + zivid.Mask("invalid") + + with pytest.raises(TypeError): + zivid.Mask([1, 2, 3]) + + with pytest.raises(TypeError): + zivid.Mask(123) + + +def test_mask_empty_array(): + """Test creating Mask from empty array.""" + # Should handle empty arrays gracefully + empty_array = np.array([], dtype=bool).reshape(0, 0) + mask = zivid.Mask(empty_array) + + assert mask.width == 0 + assert mask.height == 0 + + +def test_mask_large_array(): + """Test creating Mask from large array.""" + # Test with reasonably large array + large_array = np.random.choice([True, False], size=(100, 200)) + mask = zivid.Mask(large_array) + + assert mask.width == 200 + assert mask.height == 100 + + # Verify data integrity + result_array = mask.to_array() + expected = large_array.astype(np.uint8) + + np.testing.assert_array_equal(result_array, expected) + + +def test_mask_boolean_conversion_edge_cases(): + """Test edge cases in boolean conversion.""" + # Test various falsy and truthy values + test_cases = [ + (np.array([[True, False]]), np.array([[1, 0]], dtype=np.uint8)), + (np.array([[1, 0]]), np.array([[1, 0]], dtype=np.uint8)), + (np.array([[1.0, 0.0]]), np.array([[1, 0]], dtype=np.uint8)), + (np.array([[-1, 2]]), np.array([[1, 1]], dtype=np.uint8)), + ] + + for input_array, expected in test_cases: + mask = zivid.Mask(input_array) + result = mask.to_array() + np.testing.assert_array_equal(result, expected) + + +def test_mask_roundtrip(): + """Test roundtrip: numpy -> Mask -> numpy.""" + original = np.random.choice([0, 1], size=(50, 75)).astype(np.uint8) + + # numpy -> Mask -> numpy + mask = zivid.Mask(original) + roundtrip = mask.to_array() + + np.testing.assert_array_equal(original, roundtrip) + assert original.dtype == roundtrip.dtype + assert original.shape == roundtrip.shape + + +def test_mask_internal_access(): + """Test that internal implementation is accessible.""" + mask_data = np.ones((5, 5), dtype=bool) + mask = zivid.Mask(mask_data) + + internal_impl = zivid.mask._to_internal_mask(mask) # pylint: disable=protected-access + + # Should be the underlying C++ object + assert internal_impl is not None + # The exact type depends on the C++ binding, but it should exist + assert hasattr(internal_impl, "width") or hasattr(internal_impl, "to_string") diff --git a/test/test_point_cloud.py b/test/test_point_cloud.py index 875a258d..28556102 100644 --- a/test/test_point_cloud.py +++ b/test/test_point_cloud.py @@ -448,3 +448,184 @@ def test_clone_point_cloud(frame, transform): point_cloud.transform(transform) # clone, transform should not have affected the clone assert_point_clouds_not_equal(point_cloud, point_cloud_clone) + + +def test_mask_with_numpy_boolean(point_cloud): + """Test masking with numpy boolean array.""" + height, width = point_cloud.height, point_cloud.width + + # Create a boolean mask that masks out center region + mask_array = np.ones((height, width), dtype=bool) + mask_array[height // 4 : 3 * height // 4, width // 4 : 3 * width // 4] = False + + # Get original point count + original_xyz = point_cloud.copy_data("xyz") + original_valid_points = np.sum(~np.isnan(original_xyz[:, :, 0])) + + # Apply mask in-place + result = point_cloud.mask(mask_array) + + # Should return self for chaining + assert result is point_cloud + + # Count valid points after masking + masked_xyz = point_cloud.copy_data("xyz") + masked_valid_points = np.sum(~np.isnan(masked_xyz[:, :, 0])) + + # Should have fewer valid points after masking + assert masked_valid_points <= original_valid_points + + # Points where mask was True should now be NaN + outside_center_slice_top = masked_xyz[: height // 4, :, 0] + outside_center_slice_bottom = masked_xyz[3 * height // 4 :, :, 0] + outside_center_slice_left = masked_xyz[:, : width // 4, 0] + outside_center_slice_right = masked_xyz[:, 3 * width // 4 :, 0] + assert np.all(np.isnan(outside_center_slice_top)) + assert np.all(np.isnan(outside_center_slice_bottom)) + assert np.all(np.isnan(outside_center_slice_left)) + assert np.all(np.isnan(outside_center_slice_right)) + + +def test_mask_with_numpy_uint8(point_cloud): + """Test masking with numpy uint8 array.""" + height, width = point_cloud.height, point_cloud.width + + # Create a uint8 mask (0 = keep, 1 = mask out) + mask_array = np.zeros((height, width), dtype=np.uint8) + mask_array[: height // 2, :] = 1 # Mask out top half + + # Get original data for comparison + original_xyz = point_cloud.copy_data("xyz").copy() + + # Apply mask + point_cloud.mask(mask_array) + + # Check results + masked_xyz = point_cloud.copy_data("xyz") + + # Top half should be NaN + top_half_masked = masked_xyz[: height // 2, :, 0] + assert np.all(np.isnan(top_half_masked)) + + # Bottom half should be unchanged where originally valid + bottom_half_original = original_xyz[height // 2 :, :, 0] + bottom_half_masked = masked_xyz[height // 2 :, :, 0] + + # Where original was valid, masked should be the same + valid_original = ~np.isnan(bottom_half_original) + np.testing.assert_array_equal(bottom_half_original[valid_original], bottom_half_masked[valid_original]) + + +def test_mask_with_zivid_mask(point_cloud): + """Test masking with zivid.Mask object.""" + height, width = point_cloud.height, point_cloud.width + + # Create mask using zivid.Mask (0 = keep, 1 = mask out) + mask_array = np.ones((height, width), dtype=np.uint8) + mask_array[height // 3 : 2 * height // 3, width // 3 : 2 * width // 3] = 0 # Only keep center region + + mask = zivid.Mask(mask_array) + + # Apply mask + point_cloud.mask(mask) + + # Check results + masked_xyz = point_cloud.copy_data("xyz") + + # Outside center region should be NaN + # Top region + top_region = masked_xyz[: height // 3, :, 0] + assert np.all(np.isnan(top_region)) + + # Bottom region + bottom_region = masked_xyz[2 * height // 3 :, :, 0] + assert np.all(np.isnan(bottom_region)) + + # Left and right regions + left_region = masked_xyz[:, : width // 3, 0] + assert np.all(np.isnan(left_region)) + + right_region = masked_xyz[:, 2 * width // 3 :, 0] + assert np.all(np.isnan(right_region)) + + +def test_masked_returns_new_instance(point_cloud): + """Test that masked() returns a new PointCloud instance.""" + height, width = point_cloud.height, point_cloud.width + + # Create mask + mask_array = np.ones((height, width), dtype=bool) + mask_array[10:20, 10:20] = False + + # Get original data for comparison + original_xyz = point_cloud.copy_data("xyz").copy() + + # Apply masked() - should return new instance + masked_point_cloud = point_cloud.masked(mask_array) + + # Should be different instances + assert masked_point_cloud is not point_cloud + assert isinstance(masked_point_cloud, zivid.PointCloud) + + # Original should be unchanged + current_xyz = point_cloud.copy_data("xyz") + np.testing.assert_array_equal(original_xyz, current_xyz) + + # New instance should have mask applied + masked_xyz = masked_point_cloud.copy_data("xyz") + outside_masked_region_top = masked_xyz[:10, :, 0] + outside_masked_region_bottom = masked_xyz[20:, :, 0] + outside_masked_region_left = masked_xyz[:, :10, 0] + outside_masked_region_right = masked_xyz[:, 20:, 0] + assert np.all(np.isnan(outside_masked_region_top)) + assert np.all(np.isnan(outside_masked_region_bottom)) + assert np.all(np.isnan(outside_masked_region_left)) + assert np.all(np.isnan(outside_masked_region_right)) + + # Clean up + masked_point_cloud.release() + + +def test_mask_chaining(point_cloud): + """Test that mask() returns self for method chaining.""" + height, width = point_cloud.height, point_cloud.width + + # Create simple masks + mask1 = np.ones((height, width), dtype=bool) + mask1[:10, :] = False # Mask top 10 rows + + mask2 = np.ones((height, width), dtype=bool) + mask2[-10:, :] = False # Mask bottom 10 rows + + # Test chaining + result = point_cloud.mask(mask1).mask(mask2) + + # Should return self + assert result is point_cloud + + # Check that both masks were applied + masked_xyz = point_cloud.copy_data("xyz") + + # Top and bottom should be NaN + top_region = masked_xyz[:10, :, 0] + bottom_region = masked_xyz[-10:, :, 0] + + assert np.all(np.isnan(top_region)) + assert np.all(np.isnan(bottom_region)) + + +def test_mask_invalid_dimensions(point_cloud): + """Test error handling for invalid mask dimensions.""" + height, width = point_cloud.height, point_cloud.width + + # Wrong dimensions + wrong_mask = np.ones((height + 1, width), dtype=bool) + + with pytest.raises((ValueError, RuntimeError)): + point_cloud.mask(wrong_mask) + + # Wrong number of dimensions + wrong_dims = np.ones((height, width, 3), dtype=bool) + + with pytest.raises((ValueError, TypeError)): + point_cloud.mask(wrong_dims) diff --git a/test/test_resolution.py b/test/test_resolution.py new file mode 100644 index 00000000..a8702f36 --- /dev/null +++ b/test/test_resolution.py @@ -0,0 +1,122 @@ +"""Tests for zivid.Resolution class.""" + +import pytest +import zivid + + +def test_resolution_init(): + """Test creating Resolution with width and height.""" + resolution = zivid.Resolution(2448, 2048) + + assert resolution.width == 2448 + assert resolution.height == 2048 + assert resolution.size == 2448 * 2048 + + +def test_resolution_init_zero_dimensions(): + """Test creating Resolution with zero dimensions.""" + resolution = zivid.Resolution(0, 0) + + assert resolution.width == 0 + assert resolution.height == 0 + assert resolution.size == 0 + + +def test_resolution_init_invalid_types(): + """Test error handling for invalid types.""" + with pytest.raises(TypeError, match="Width and height must be integers"): + zivid.Resolution(2448.5, 2048) + + with pytest.raises(TypeError, match="Width and height must be integers"): + zivid.Resolution("2448", 2048) + + with pytest.raises(TypeError, match="Width and height must be integers"): + zivid.Resolution(2448, "2048") + + +def test_resolution_init_negative_values(): + """Test error handling for negative values.""" + with pytest.raises(ValueError, match="Width and height must be non-negative"): + zivid.Resolution(-1, 2048) + + with pytest.raises(ValueError, match="Width and height must be non-negative"): + zivid.Resolution(2448, -1) + + with pytest.raises(ValueError, match="Width and height must be non-negative"): + zivid.Resolution(-1, -1) + + +def test_resolution_properties(): + """Test Resolution properties.""" + res1 = zivid.Resolution(2448, 2048) + + # Test properties are read-only and correct + assert res1.width == 2448 + assert res1.height == 2048 + assert res1.size == 2448 * 2048 + + +def test_resolution_equality(): + """Test Resolution equality comparison.""" + res1 = zivid.Resolution(2448, 2048) + res2 = zivid.Resolution(2448, 2048) + res3 = zivid.Resolution(2816, 2816) + + # Test equality + assert res1 == res2 + assert res1 != res3 + assert res2 != res3 + + # Test with non-Resolution objects + assert res1 != "2448x2048" + assert res1 != (2448, 2048) + assert res1 is not None + + +def test_resolution_string_representation(): + """Test string representation of Resolution.""" + resolution = zivid.Resolution(1944, 1200) + + str_repr = str(resolution) + repr_repr = repr(resolution) + + # Check str format (from C++ toString()) + assert "1944" in str_repr + assert "1200" in str_repr + + # Check repr format + assert repr_repr == "Resolution(width=1944, height=1200)" + + +def test_resolution_hash(): + """Test Resolution hashing for use in sets/dicts.""" + res1 = zivid.Resolution(2448, 2048) + res2 = zivid.Resolution(2448, 2048) + res3 = zivid.Resolution(2816, 2816) + + # Equal resolutions should have equal hashes + assert hash(res1) == hash(res2) + + # Can be used in sets + resolution_set = {res1, res2, res3} + assert len(resolution_set) == 2 # res1 and res2 are the same + + # Can be used as dict keys + resolution_dict = {res1: "Full HD", res3: "HD"} + assert len(resolution_dict) == 2 + assert resolution_dict[res2] == "Full HD" # res2 == res1 + + +def test_resolution_immutability(): + """Test that Resolution properties cannot be modified.""" + resolution = zivid.Resolution(2448, 2048) + + # Properties should be read-only + with pytest.raises(AttributeError): + resolution.width = 2448 # pylint: disable=attribute-defined-outside-init + + with pytest.raises(AttributeError): + resolution.height = 2048 # pylint: disable=attribute-defined-outside-init + + with pytest.raises(AttributeError): + resolution.size = 2448 * 2048 # pylint: disable=attribute-defined-outside-init